本文包含兩部分,第一部分經過簡明的描述介紹什麼是 CommonJS、AMD、CMD、UMD、ES Module 以及它們的常見用法,第二部分則根據實際問題指出在正常的 webpack 構建過程當中該如何指定打包配置中的模塊化參數。javascript
模塊化這個話題在 ES6 以前是不存在的,所以這也被詬病爲早期 JavaScript 開發全局污染和依賴管理混亂問題的源頭。這類歷史淵源和發展概述在本文將不會說起,所以感興趣能夠自行搜索 JavaScript 發展史進行了解。java
直接進入正題,咱們來看看常見的模塊化方案都有哪些以及他們都有哪些內容。jquery
CommonJS 的一個模塊就是一個腳本文件,經過執行該文件來加載模塊。CommonJS 規範規定,每一個模塊內部,module
變量表明當前模塊。這個變量是一個對象,它的 exports 屬性(即 module.exports
)是對外的接口。加載某個模塊,實際上是加載該模塊的 module.exports
屬性。webpack
咱們見過這樣的模塊引用:web
var myModule = require('module'); myModule.sayHello();
這是由於咱們把模塊的方法定義在了模塊的屬性上:數組
// module.js module.exports.sayHello = function() { console.log('Hello '); }; // 若是這樣寫 module.exports = sayHello; // 調用則須要改成 var sayHello = require('module'); sayHello();
require
命令第一次加載該腳本時就會執行整個腳本,而後在內存中生成一個對象(模塊能夠屢次加載,可是在第一次加載時纔會運行,結果被緩存),這個結果長成這樣:瀏覽器
{ id: '...', exports: { ... }, loaded: true, ... }
Node.js 的模塊機制實現就是參照了 CommonJS 的標準。可是 Node.js 額外作了一件事,即爲每一個模塊提供了一個 exports 變量,以指向 module.exports,這至關於在每一個模塊最開始,寫有這麼一行代碼:緩存
var exports = module.exports;
CommonJS 模塊的特色:異步
全部代碼都運行在模塊做用域,不會污染全局做用域。ide
獨立性是模塊的重要特色就,模塊內部最好不與程序的其餘部分直接交互。
模塊能夠屢次加載,可是隻會在第一次加載時運行一次,而後運行結果就被緩存了,之後再加載,就直接讀取緩存結果。要想讓模塊再次運行,必須清除緩存。
模塊加載的順序,按照其在代碼中出現的順序。
CommonJS 規範很好,可是不適用於瀏覽器環境,因而有了 AMD 和 CMD 兩種方案。AMD 全稱 Asynchronous Module Definition,即異步模塊定義。它採用異步方式加載模塊,模塊的加載不影響它後面語句的運行。全部依賴這個模塊的語句,都定義在一個回調函數中,等到加載完成以後,這個回調函數纔會運行。除了和 CommonJS 同步加載方式不一樣以外,AMD 在模塊的定義與引用上也有所不一樣。
define(id?, dependencies?, factory);
AMD 的模塊引入由 define 方法來定義,在 define API 中:
id:模塊名稱,或者模塊加載器請求的指定腳本的名字;
dependencies:是個定義中模塊所依賴模塊的數組,默認爲 ["require", "exports", "module"],舉個例子比較好理解,當咱們建立一個名爲 "alpha" 的模塊,使用了require,exports,和名爲 "beta" 的模塊,須要以下書寫(示例1);
factory:爲模塊初始化要執行的函數或對象。若是爲函數,它應該只被執行一次。若是是對象,此對象應該爲模塊的輸出值;
// 示例1 define("alpha", ["require", "exports", "beta"], function (require, exports, beta) { exports.verb = function() { return beta.verb(); // 或者 return require("beta").verb(); } });
若是模塊定義不存在依賴,那麼能夠直接定義對象:
define({ add: function(x, y){ return x + y; } });
而使用時咱們依舊經過 require 關鍵字,它包含兩個參數,第一個數組爲要加載的模塊,第二個參數爲回調函數:
require([module], callback);
舉個例子:
require(['math'], function (math) { math.add(2, 3); });
CMD 全稱爲 Common Module Definition,是 Sea.js 所推廣的一個模塊化方案的輸出。在 CMD define 的入參中,雖然也支持包含 id, deps 以及 factory 三個參數的形式,但推薦的是接受 factory 一個入參,而後在入參執行時,填入三個參數 require、exports 和 module:
define(function(require, exports, module) { var a = require('./a'); a.doSomething(); var b = require('./b'); b.doSomething(); ... })
經過執行該構造方法,能夠獲得模塊向外提供的接口。在與 AMD 比較上存在兩個主要的不一樣點(來自玉伯回答):
對於依賴的模塊,AMD 是提早執行,CMD 是延遲執行。不過 RequireJS 從 2.0 開始,也改爲能夠延遲執行(根據寫法不一樣,處理方式不一樣)。CMD 推崇 as lazy as possible.
CMD 推崇依賴就近,AMD 推崇依賴前置。
若是說的不清楚,那麼咱們直接看上面的代碼用 AMD 該怎麼寫:
define(['./a', './b'], function(a, b) { a.doSomething(); b.doSomething(); ... })
UMD,全稱 Universal Module Definition,即通用模塊規範。既然 CommonJs 和 AMD 風格同樣流行,那麼須要一個能夠統一瀏覽器端以及非瀏覽器端的模塊化方案的規範。
直接來看看官方給出的 jQuery 模塊如何用 UMD 定義的代碼:
(function (factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define(['jquery'], factory); } else if (typeof module === 'object' && module.exports) { // Node/CommonJS module.exports = function( root, jQuery ) { if ( jQuery === undefined ) { // require('jQuery') returns a factory that requires window to // build a jQuery instance, we normalize how we use modules // that require this pattern but the window provided is a noop // if it's defined (how jquery works) if ( typeof window !== 'undefined' ) { jQuery = require('jquery'); } else { jQuery = require('jquery')(root); } } factory(jQuery); return jQuery; }; } else { // Browser globals factory(jQuery); } }(function ($) { $.fn.jqueryPlugin = function () { return true; }; }));
UMD的實現很簡單:
先判斷是否支持 AMD(define 是否存在),存在則使用 AMD 方式加載模塊;
再判斷是否支持 Node.js 模塊格式(exports 是否存在),存在則使用 Node.js 模塊格式;
前兩個都不存在,則將模塊公開到全局(window 或 global);
固然,以上說的種種都是社區提供的方案,歷史上,JavaScript 一直沒有模塊系統,直到 ES6 在語言標準的層面上,實現了它。其設計思想是儘可能的靜態化,使得編譯時就能肯定模塊的依賴關係,以及輸入和輸出的變量。CommonJS 和 AMD 模塊,都只能在運行時肯定這些東西。好比,CommonJS 模塊就是對象,輸入時必須查找對象屬性。而 ES Modules 不是對象,而是經過 export
命令顯式指定輸出的代碼。
ES Modules 的模塊化能力由 export
和 import
組成,export
命令用於規定模塊的對外接口,import
命令用於輸入其餘模塊提供的功能。咱們能夠這樣定義一個模塊:
// 第一種方式 export var firstName = 'Michael'; export var lastName = 'Jackson'; export var year = 1958; // 第二種方式 var firstName = 'Michael'; var lastName = 'Jackson'; var year = 1958; export { firstName, lastName, year };
而後再這樣引入他們:
import { firstName, lastName, year } from 'module'; import { firstName as newName } from 'module'; import * as moduleA from 'module';
除以上兩種命令外,還有一個 export default
命令用於指定模塊的默認輸出(一個模塊只能有一個默認輸出)。若是使用了 export default
語法,在 import 時則能夠任意命名。因爲 export default
命令的本質是將後面的值,賦給 default
變量,因此也能夠直接將一個值寫在 export default
以後。固然,引用方式也存在多種:
import { default as foo } from 'module'; import foo from 'module';
須要注意的是 Modules 會自動採用嚴格模式,且 import 命令具備提高效果,會提高到整個模塊的頭部,首先執行。
延伸閱讀 JavaScript 模塊的循環加載
說完理論,來看看實際項目中遇到的問題。當咱們開發完一個 JavaScript 模塊必然要經歷打包的流程,而在 webpack 配置中,經過指定 output 選項就能夠告訴 webpack 如何輸出 bundle, asset 以及其餘載入的內容。那麼如何實現不一樣環境可兼容的構建呢?
import
:經過 ES Modules 規範語法進入引入;
變量:做爲一個全局變量,好比經過 script 標籤來訪問;
this
:經過 this 對象訪問;
window
:在瀏覽器中經過 window 對象訪問;
UMD:在 AMD 或 CommonJS 經過 require
引入後訪問;
output
中有一個屬性叫作 libraryTarget
,被用來指定如何暴露你的模塊的屬性。你能夠這樣嘗試賦值給一個變量或者指定對象的屬性:
// 加載完成後將模塊賦值給一個指定變量(默認值) { libraryTarget: 'var', ... } // 賦值爲指定對象的一個屬性,好比 `this` 或者 `window` { libraryTarget: "this", // libraryTarget: "window", ... } // 一樣的,如果指定 commonjs,那麼即可以將模塊分配給 exports,這也意味着能夠用於 CommonJS 環境: { libraryTarget: "commonjs", ... }
若是須要更完整的模塊化 bundle,以確保和各模塊系統兼容,那麼能夠這樣嘗試:
// 內容分配給 module.exports 對象,用於 CommonJS 環境 { libraryTarget: 'commonjs2', ... } // 暴露爲 AMD 模塊,經過特定屬性引入 { libraryTarget: 'amd', ... } // 全部模塊系統兼容的萬金油,能夠在 CommonJS, AMD 環境下運行,或將模塊導出到 global 下的變量 { libraryTarget: 'umd', ... }
所以,若是隻看 output 內容,那麼個人一個 webpack 生產環境配置能夠寫成這樣:
module.exports = { output: { // webpack 如何輸出結果的相關選項 path: path.resolve(__dirname, "dist"), filename: 'index.js', library: 'hijiangtao', umdNamedDefine: true, libraryTarget: 'umd', }, }
(完)
回覆「加羣」與大佬們一塊兒交流學習~