《模塊化系列》完全理清 AMD,CommonJS,CMD,UMD,ES6

本文你將學到:javascript

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

本文全部例子都存放於 github.com/hua1995116/…css

引言

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

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 的模塊,必需要加載插件 github.com/rollup/plug… 固然並非對全部的 CommonJS 都自動支持,只針對相似於靜態的寫法才能導出,例如針動態模塊導出,以及隱式地導出將沒法自動導出,這樣的場景下須要手動指定導出模塊。以上的例子就是一個動態的方式,只有當 factory 函數執行了才能知道導出的模塊,須要手動指定。java

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']
  }
});
複製代碼

固然上述只是我寫這篇文章的一個原由,就是由於我對這一塊的迷惑,因此使得我想從新複習一下這一塊知識,上面將的可能你徹底聽不懂我在說什麼,沒有關係,下面開始切入正題。node

Rollup 是什麼?

由於在最一開始,是我引入了這個概念,因此由我出來填坑,固然對這個工具很是熟悉的朋友能夠跳過。不熟悉的朋友你只須要知道,這個是一個打包 ES Module 的工具。webpack

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

CommonJS

CommonJS規範es6

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

// 模塊 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
複製代碼

代碼地址: github.com/hua1995116/…web

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

AMD

AMD規範

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
複製代碼

代碼地址: github.com/hua1995116/…

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

CMD

CMD規範

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
複製代碼

github.com/hua1995116/…

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

UMD

UMD文檔

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. 若是以上兩種都沒有,設定爲原始的代碼規範。

代碼地址:github.com/hua1995116/…

ES Modules

ES Modules 文檔

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>
複製代碼

代碼地址: github.com/hua1995116/…

詳細能夠查看 深刻理解 ES6 模塊機制

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]
}
複製代碼

代碼地址:github.com/hua1995116/…

從上述例子中能夠看出,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));
複製代碼

代碼地址:github.com/hua1995116/…

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

1581857899385.jpg

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

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

  1. import 只能做爲模塊頂層的語句出現,不能出如今 function 裏面或是 if 裏面。
  2. import 的模塊名只能是字符串常量。
  3. 無論 import 的語句出現的位置在哪裏,在模塊初始化的時候全部的 import 都必須已經導入完成。
  4. import bindingimmutable 的,相似 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);
複製代碼

代碼地址: github.com/hua1995116/…

固然以上只是反作用的一種,詳情其餘幾種看查看 rollupjs.org/guide/en/

結語

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

參考

github.com/rollup/roll…

github.com/rollup/plug…

www.zhihu.com/question/63…

www.yuque.com/baichuan/no…

github.com/indutny/web…

xbhong.top/2018/03/12/…

www.douban.com/note/283566…

blog.fundebug.com/2018/08/15/…

huangxuan.me/js-module-7…

www.jianshu.com/p/6c26fb754…

關注

相關文章
相關標籤/搜索