《模塊化系列》徹底理清 AMD,CommonJS,CDM,UMD,ES6

本文你將學到:

1.Rollup 是什麼2.CommonJS、AMD、CMD、UMD、ES6 分別的介紹3.ES6 模塊與 CommonJS 模塊的區別4.模塊演進的產物 —— Tree Shaking5.Tree Shaking 應該注意什麼

本文所有例子都存放於 https://github.com/hua1995116/packaging-example

引言

今天在使用 rollup 打包的時候遇到了一個問題

Error: 'Map' is not exported by node_modules/immutable/dist/immutable.js
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  typeof define === 'function' && define.amd ? define(factory) :
  global.Immutable = factory();

發現 immutable 是以 UMD 的形式暴露。查閱資料後發現 Rollup 並不支持 CommonJS 和 AMD 的打包方式,想要成功引入 commonJS 的模塊,必須要加載插件 https://github.com/rollup/plugins/tree/master/packages/commonjs。當然並不是對所有的 CommonJS 都自動支持,只針對類似於靜態的寫法才能導出,例如針動態模塊導出,以及隱式地導出將無法自動導出,這樣的場景下需要手動指定導出模塊。以上的例子就是一個動態的方式,只有當 factory 函數執行了才能知道導出的模塊,需要手動指定。

commonjs({
  namedExports: {
    // left-hand side can be an absolute path, a path
    // relative to the current directory, or the name
    // of a module in node_modules
    'immutable': ['Map']
  }
});

當然上述只是我寫這篇文章的一個起因,就是因爲我對這一塊的迷惑,所以使得我想重新複習一下這一塊知識,上面將的可能你完全聽不懂我在說什麼,沒有關係,下面開始切入正題。

Rollup 是什麼?

因爲在最一開始,是我引入了這個概念,所以由我出來填坑,當然對這個工具非常熟悉的朋友可以跳過。不熟悉的朋友你只需要知道,這個是一個打包 ES Module 的工具。

Rollup 是一個 JavaScript 模塊打包器,可以將小塊代碼編譯成大塊複雜的代碼,例如 library 或應用程序。Rollup 對代碼模塊使用新的標準化格式,這些標準都包含在 JavaScript 的 ES6 版本中,而不是以前的特殊解決方案,如 CommonJS 和 AMD。ES6 模塊可以使你自由、無縫地使用你最喜愛的 library 中那些最有用獨立函數,而你的項目不必攜帶其他未使用的代碼。ES6 模塊最終還是要由瀏覽器原生實現,但當前 Rollup 可以使你提前體驗。

CommonJS

CommonJS規範[1]

CommonJS 主要運行於服務器端,該規範指出,一個單獨的文件就是一個模塊。Node.js爲主要實踐者,它有四個重要的環境變量爲模塊化的實現提供支持:moduleexportsrequireglobalrequire 命令用於輸入其他模塊提供的功能,module.exports命令用於規範模塊的對外接口,輸出的是一個值的拷貝,輸出之後就不能改變了,會緩存起來。

// 模塊 a.js
const name = 'qiufeng'


module.exports = {
    name,
    github: 'https://github.com/hua1995116'
}
// 模塊 b.js
// 引用核心模塊或者第三方包模塊,不需要寫完整路徑
const path = require('path');
// 引用自定義模塊可以省略.js
const { name, github } = require('./a');


console.log(name, github, path.basename(github));
// 輸出 qiufeng https://github.com/hua1995116 hua1995116

代碼地址: https://github.com/hua1995116/packaging-example/tree/master/modules-introduction/CommonJS

CommonJS 採用同步加載模塊,而加載的文件資源大多數在本地服務器,所以執行速度或時間沒問題。但是在瀏覽器端,限於網絡原因,更合理的方案是使用異步加載。

AMD

AMD規範[2]

AMD是"Asynchronous Module Definition"的縮寫,意思就是"異步模塊定義"。它採用異步方式加載模塊,模塊的加載不影響它後面語句的運行。所有依賴這個模塊的語句,都定義在一個回調函數中,等到加載完成之後,這個回調函數纔會運行。其中 RequireJS 是最佳實踐者。

模塊功能主要的幾個命令:definerequirereturndefine.amddefine是全局函數,用來定義模塊,define(id?, dependencies?, factory)。require命令用於輸入其他模塊提供的功能,return命令用於規範模塊的對外接口,define.amd屬性是一個對象,此屬性的存在來表明函數遵循AMD規範。

// model1.js
define(function () {
    console.log('model1 entry');
    return {
        getHello: function () {
            return 'model1';
        }
    };
});
// model2.js
define(function () {
    console.log('model2 entry');
    return {
        getHello: function () {
            return 'model2';
        }
    };
});
// main.js
define(function (require) {
    var model1 = require('./model1');
    console.log(model1.getHello());
    var model2 = require('./model2');
    console.log(model2.getHello());
});
<script src="https://cdn.bootcss.com/require.js/2.3.6/require.min.js"></script>
<script>
    requirejs(['main']);
</script>
// 輸出結果  
// model1 entry
// model2 entry
// model1
// model2

代碼地址: https://github.com/hua1995116/packaging-example/tree/master/modules-introduction/AMD

在這裏,我們使用define來定義模塊,return來輸出接口, require來加載模塊,這是AMD官方推薦用法。

CMD

CMD規範[3]

CMD(Common Module Definition - 通用模塊定義)規範主要是Sea.js推廣中形成的,一個文件就是一個模塊,可以像Node.js一般書寫模塊代碼。主要在瀏覽器中運行,當然也可以在Node.js中運行。

它與AMD很類似,不同點在於:AMD 推崇依賴前置、提前執行,CMD推崇依賴就近、延遲執行。

不懂 依賴就近、延遲執行 的可以比較下面和上面的例子。

// model1.js
define(function (require, exports, module) {
    console.log('model1 entry');
    exports.getHello = function () {
        return 'model1';
    }
});
// model2.js
define(function (require, exports, module) {
    console.log('model2 entry');
    exports.getHello = function () {
        return 'model2';
    }
});
// main.js
define(function(require, exports, module) {
    var model1 = require('./model1'); //在需要時申明
    console.log(model1.getHello());
    var model2 = require('./model2'); //在需要時申明
    console.log(model2.getHello());
});
<script src="https://cdn.bootcss.com/seajs/3.0.3/sea.js"></script>
<script>
    seajs.use('./main.js')
</script>
// 輸出 
// model1 entry
// model1
// model2 entry
// model2

https://github.com/hua1995116/packaging-example/tree/master/modules-introduction/CMD

總結: 對比和 AMD 的寫法就可以看出 AMD 和 CMD 的區別。雖然現在 CMD 已經涼了。但是 CMD 更加接近於 CommonJS 的寫法,但是 AMD 更加接近於瀏覽器的異步的執行方式。

UMD

UMD文檔[4]

UMD(Universal Module Definition - 通用模塊定義)模式,該模式主要用來解決CommonJS模式和AMD模式代碼不能通用的問題,並同時還支持老式的全局變量規範。

示例展示

// bundle.js
(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
    typeof define === 'function' && define.amd ? define(factory) :
    (global = global || self, global.myBundle = factory());
}(this, (function () { 'use strict';


    var main = () => {
        return 'hello world';
    };


    return main;


})));


// index.html
<script src="bundle.js"></script>
<script>
  console.log(myBundle());
</script>

1.判斷define爲函數,並且是否存在define.amd,來判斷是否爲AMD規範,2.判斷module是否爲一個對象,並且是否存在module.exports來判斷是否爲CommonJS規範3.如果以上兩種都沒有,設定爲原始的代碼規範。

代碼地址:https://github.com/hua1995116/packaging-example/tree/master/modules-introduction/UMD

ES Modules

ES Modules 文檔[5]

ES modules(ESM)是 JavaScript 官方的標準化模塊系統。

1.它因爲是標準,所以未來很多瀏覽器會支持,可以很方便的在瀏覽器中使用。(瀏覽器默認加載不能省略.js)2.它同時兼容在node環境下運行。3.模塊的導入導出,通過importexport來確定。可以和Commonjs模塊混合使用。4.ES modules 輸出的是值的引用,輸出接口動態綁定,而 CommonJS 輸出的是值的拷貝5.ES modules 模塊編譯時執行,而 CommonJS 模塊總是在運行時加載

使用方式

// index.js
import { name, github } from './demo.js';


console.log(name(), github());


document.body.innerHTML = `<h1>${name()} ${github()}</h1>`
export function name() {
    return 'qiufeng';
}


export function github() {
    return 'https://github.com/hua1995116';
}
<script src="./index.js" type="module"></script>

代碼地址: https://github.com/hua1995116/packaging-example/tree/master/modules-introduction/ES-Modules

詳細可以查看 深入理解 ES6 模塊機制[6]

CommonJS 的值拷貝

// a.js
const b = require('./b');
console.log(b.age);
setTimeout(() => {
  console.log(b.age);
  console.log(require('./b').age);
}, 100);
// b.js
let age = 1;
setTimeout(() => {
  age = 18;
}, 10);
module.exports = {
  age
}
// 執行:node a.js
// 執行結果:
// 1
// 1
// 1

CommonJS 主要有執行主要有以下兩個特點

1.CommonJS 模塊中 require 引入模塊的位置不同會對輸出結果產生影響,並且會生成值的拷貝2.CommonJS 模塊重複引入的模塊並不會重複執行,再次獲取模塊只會獲得之前獲取到的模塊的緩存

ES modules 的值的引用

// a.js
import { age } from './b.js';


console.log(age);
setTimeout(() => {
    console.log(age);
    import('./b.js').then(({ age }) => {
        console.log(age);
    })
}, 100);


// b.js
export let age = 1;


setTimeout(() => {
    age = 2;
}, 10);
// 打開 index.html
// 執行結果:
// 1
// 2
// 2

動態加載和靜態編譯區別?

舉個例子如下:

動態加載,只有當模塊運行後,才能知道導出的模塊是什麼。

var test = 'hello'
module.exports = {
  [test + '1']: 'world'
}

靜態編譯, 在編譯階段就能知道導出什麼模塊。

export function hello() {return 'world'}

關於 ES6 模塊編譯時執行會導致有以下兩個特點:

1.import 命令會被 JavaScript 引擎靜態分析,優先於模塊內的其他內容執行。2.export 命令會有變量聲明提前的效果。

import 優先執行:

// a.js
console.log('a.js')
import { age } from './b.js';


// b.js
export let age = 1;
console.log('b.js 先執行');


// 運行 index.html 執行結果:
// b.js 先執行
// a.js

雖然 import 順序比較靠後,但是 由於 import 提升效果會優先執行。

export 變量聲明提升:

// a.js
import { foo } from './b.js';
console.log('a.js');
export const bar = 1;
export const bar2 = () => {
  console.log('bar2');
}
export function bar3() {
  console.log('bar3');
}


// b.js
export let foo = 1;
import * as a from './a.js';
console.log(a);


// 運行 node --experimental-modules a.js 執行結果:
// [Module] {
//  bar: <uninitialized>,
//  bar2: <uninitialized>,
//  bar3: [Function: bar3]
}

代碼地址:https://github.com/hua1995116/packaging-example/tree/master/modules-introduction/CommonJS-vs-ESModules

從上述例子中可以看出,a 模塊引用了 b 模塊,b 模塊也引用了 a 模塊,export 聲明優先於其他內容。由於變量和函數的提升不一樣,此處不做過多介紹。

此處有一個小插曲,我一開始用瀏覽器進行執行的結果爲:

{
  bar: 1
  bar2: () => { console.log('bar2'); }
  bar3: ƒ bar3()
}
a.js

讓我一度覺得是不是 export 有什麼特殊的聲明提升?因爲我發現深入理解 ES6 模塊機制一文中是使用的 babel-node, 是否是因爲環境不一樣導致的。因此我使用了 node v12.16.0,進行測試 node --experimental-modules a.js, 發現結果與 深入理解 ES6 模塊機制 中結果一致,後來想到 console.log 的顯示問題,console.log 常常會有一些異步的顯示。後來我經過測試發現確實是 console.log 搞的鬼

console.log(a); -> console.log(JSON.stringify(a))

會出現一個 Uncaught ReferenceError: bar is not defined 的錯誤,是因爲 bar 未初始化導致。後續也會將這個 console 的表現形式報告給 chromium

Tree shaking

介紹完了,各個模塊的標準後,爲什麼又將這個 Tree shaking 呢?因爲模塊化的一次又一次地變更,讓我們的模塊系統變得越來越好,而 Tree shaking 就是得益 ES modules 的發展的產物。

這個概念是Rollup提出來的。Rollup推薦使用ES2015 Modules來編寫模塊代碼,這樣就可以使用Tree-shaking來對代碼做靜態分析消除無用的代碼,可以查看Rollup網站上的REPL示例,代碼打包前後之前的差異,就會清晰的明白Tree-shaking的作用。

1.沒有使用額外的模塊系統,直接定位import來替換export的模塊2.去掉了未被使用的代碼

tree shaking 的實際例子

// main.js
import * as utils from './utils';


const array = [1,2,3,1,2,3]


console.log(utils.arrayUnique(array));

代碼地址:https://github.com/hua1995116/packaging-example/tree/master/modules-introduction/Tree-Shaking

Tree shaking 和 沒有Tree shaking 打包對比。

沒有 Tree-shaking 的情況下,會將 utils 中的所有文件都進行打包,使得體積暴增。

ES Modules 之所以能 Tree-shaking 主要爲以下四個原因(摘自尤雨溪在知乎的回答):

1.import 只能作爲模塊頂層的語句出現,不能出現在 function 裏面或是 if 裏面。2.import 的模塊名只能是字符串常量。3.不管 import 的語句出現的位置在哪裏,在模塊初始化的時候所有的 import 都必須已經導入完成。4.import binding 是 immutable 的,類似 const。比如說你不能 import { a } from ‘./a’ 然後給 a 賦值個其他什麼東西。

tree shaking 應該注意什麼

副作用

沒錯,就是副作用,那麼什麼是副作用呢,請看下面的例子。

// effect.js
console.log(unused());
export function unused() {
    console.log(1);
}
// index.js
import {unused} from './effect';
console.log(42);

此例子中 console.log(unused()); 就是副作用。在 index.js 中並不需要這一句 console.log。而 rollup 並不知道這個全局的函數去除是否安全。因此在打包地時候你可以顯示地指定treeshake.moduleSideEffects 爲 false,可以顯示地告訴 rollup 外部依賴項沒有其他副作用。

不指定的情況下的打包輸出。 npx rollup index.js --file bundle.js

console.log(unused());


function unused() {
    console.log(1);
}


console.log(42);


指定沒有副作用下的打包輸出。npx rollup index.js --file bundle-no-effect.js --no-treeshake.moduleSideEffects

console.log(42);

代碼地址:https://github.com/hua1995116/packaging-example/tree/master/modules-introduction/Tree-Shaking-Effect

當然以上只是副作用的一種,詳情其他幾種看查看 https://rollupjs.org/guide/en/

結語

CommonJS 同步加載, AMD 異步加載, UMD = CommonJS + AMD , ES Module 是標準規範, 取代 UMD,是大勢所趨。Tree-shaking 牢記副作用。

參考

https://github.com/rollup/rollup/issues/3011#issuecomment-516084596

https://github.com/rollup/plugins/tree/master/packages/commonjs

https://www.zhihu.com/question/63240671

https://www.yuque.com/baichuan/notes/emputh

https://github.com/indutny/webpack-common-shake#limitations

http://xbhong.top/2018/03/12/commonjs/

https://www.douban.com/note/283566440/

https://blog.fundebug.com/2018/08/15/reduce-js-payload-with-tree-shaking/

http://huangxuan.me/js-module-7day/#/13

https://www.jianshu.com/p/6c26fb7541f1

往期精彩

《秋風日常第二期》一個快速找出待SEO圖片的技巧

《秋風日常第一期》白板協作工具 LeanBoard

關注

References

[1] CommonJS規範: http://wiki.commonjs.org/wiki/CommonJS
[2] AMD規範: https://github.com/amdjs/amdjs-api/wiki/AMD
[3] CMD規範: https://github.com/cmdjs/specification/blob/master/draft/module.md
[4] UMD文檔: https://github.com/umdjs/umd
[5] ES Modules 文檔: http://es6.ruanyifeng.com/#docs/module-loader
[6] 深入理解 ES6 模塊機制: https://juejin.im/entry/5a879e28f265da4e82635152

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章