本文你將學到:
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爲主要實踐者,它有四個重要的環境變量爲模塊化的實現提供支持:module
、exports
、require
、global
。require
命令用於輸入其他模塊提供的功能,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 是最佳實踐者。
模塊功能主要的幾個命令:define
、require
、return
和define.amd
。define
是全局函數,用來定義模塊,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.模塊的導入導出,通過import
和export
來確定。可以和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
往期精彩
關注
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