本文包含兩部分,第一部分通過(guò)簡(jiǎn)明的描述介紹什么是 CommonJS、AMD、CMD、UMD、ES Module 以及它們的常見(jiàn)用法,第二部分則根據(jù)實(shí)際問(wèn)題指出在正常的 webpack 構(gòu)建過(guò)程中該如何指定打包配置中的模塊化參數(shù)。
JavaScript 模塊化方案
模塊化這個(gè)話題在 ES6 之前是不存在的,因此這也被詬病為早期 JavaScript 開(kāi)發(fā)全局污染和依賴管理混亂問(wèn)題的源頭。這類歷史淵源和發(fā)展概述在本文將不會(huì)提及,因此感興趣可以自行搜索 JavaScript 發(fā)展史進(jìn)行了解。
直接進(jìn)入正題,我們來(lái)看看常見(jiàn)的模塊化方案都有哪些以及他們都有哪些內(nèi)容。
CommonJS
CommonJS 的一個(gè)模塊就是一個(gè)腳本文件,通過(guò)執(zhí)行該文件來(lái)加載模塊。CommonJS 規(guī)范規(guī)定,每個(gè)模塊內(nèi)部,module 變量代表當(dāng)前模塊。這個(gè)變量是一個(gè)對(duì)象,它的 exports 屬性(即 module.exports)是對(duì)外的接口。加載某個(gè)模塊,其實(shí)是加載該模塊的 module.exports 屬性。
我們見(jiàn)過(guò)這樣的模塊引用
- var myModule = require('module');
- myModule.sayHello();
這是因?yàn)槲覀儼涯K的方法定義在了模塊的屬性上:
- // module.js
- module.exports.sayHello = function() {
- console.log('Hello ');
- };
- // 如果這樣寫(xiě)
- module.exports = sayHello;
- // 調(diào)用則需要改為
- var sayHello = require('module');
- sayHello();
require 命令第一次加載該腳本時(shí)就會(huì)執(zhí)行整個(gè)腳本,然后在內(nèi)存中生成一個(gè)對(duì)象(模塊可以多次加載,但是在第一次加載時(shí)才會(huì)運(yùn)行,結(jié)果被緩存),這個(gè)結(jié)果長(zhǎng)成這樣:
- {
- id: '...',
- exports: { ... },
- loaded: true,
- ...
- }
Node.js 的模塊機(jī)制實(shí)現(xiàn)就是參照了 CommonJS 的標(biāo)準(zhǔn)。但是 Node.js 額外做了一件事,即為每個(gè)模塊提供了一個(gè) exports 變量,以指向 module.exports,這相當(dāng)于在每個(gè)模塊最開(kāi)始,寫(xiě)有這么一行代碼:
- var exports = module.exports;
CommonJS 模塊的特點(diǎn):
- 所有代碼都運(yùn)行在模塊作用域,不會(huì)污染全局作用域。
- 獨(dú)立性是模塊的重要特點(diǎn)就,模塊內(nèi)部最好不與程序的其他部分直接交互。
- 模塊可以多次加載,但是只會(huì)在第一次加載時(shí)運(yùn)行一次,然后運(yùn)行結(jié)果就被緩存了,以后再加載,就直接讀取緩存結(jié)果。要想讓模塊再次運(yùn)行,必須清除緩存。
- 模塊加載的順序,按照其在代碼中出現(xiàn)的順序。
AMD
CommonJS 規(guī)范很好,但是不適用于瀏覽器環(huán)境,于是有了 AMD 和 CMD 兩種方案。AMD 全稱 Asynchronous Module Definition,即異步模塊定義。它采用異步方式加載模塊,模塊的加載不影響它后面語(yǔ)句的運(yùn)行。所有依賴這個(gè)模塊的語(yǔ)句,都定義在一個(gè)回調(diào)函數(shù)中,等到加載完成之后,這個(gè)回調(diào)函數(shù)才會(huì)運(yùn)行。除了和 CommonJS 同步加載方式不同之外,AMD 在模塊的定義與引用上也有所不同。
- define(id?, dependencies?, factory);
AMD 的模塊引入由 define 方法來(lái)定義,在 define API 中:
- id:模塊名稱,或者模塊加載器請(qǐng)求的指定腳本的名字;
- dependencies:是個(gè)定義中模塊所依賴模塊的數(shù)組,默認(rèn)為 [“require”, “exports”, “module”],舉個(gè)例子比較好理解,當(dāng)我們創(chuàng)建一個(gè)名為 “alpha” 的模塊,使用了require,exports,和名為 “beta” 的模塊,需要如下書(shū)寫(xiě)(示例1);
- factory:為模塊初始化要執(zhí)行的函數(shù)或?qū)ο?。如果為函?shù),它應(yīng)該只被執(zhí)行一次。如果是對(duì)象,此對(duì)象應(yīng)該為模塊的輸出值;
- // 示例1
- define("alpha", ["require", "exports", "beta"], function (require, exports, beta) {
- exports.verb = function() {
- return beta.verb();
- // 或者
- return require("beta").verb();
- }
- });
如果模塊定義不存在依賴,那么可以直接定義對(duì)象:
- define({
- add: function(x, y){
- return x + y;
- }
- });
而使用時(shí)我們依舊通過(guò) require 關(guān)鍵字,它包含兩個(gè)參數(shù),第一個(gè)數(shù)組為要加載的模塊,第二個(gè)參數(shù)為回調(diào)函數(shù):
- require([module], callback);
舉個(gè)例子:
- require(['math'], function (math) {
- math.add(2, 3);
- });
CMD
CMD 全稱為 Common Module Definition,是 Sea.js 所推廣的一個(gè)模塊化方案的輸出。在 CMD define 的入?yún)⒅?,雖然也支持包含 id, deps 以及 factory 三個(gè)參數(shù)的形式,但推薦的是接受 factory 一個(gè)入?yún)?,然后在入?yún)?zhí)行時(shí),填入三個(gè)參數(shù) require、exports 和 module:
- define(function(require, exports, module) {
- var a = require('./a');
- a.doSomething();
- var b = require('./b');
- b.doSomething();
- ...
- })
通過(guò)執(zhí)行該構(gòu)造方法,可以得到模塊向外提供的接口。在與 AMD 比較上存在兩個(gè)主要的不同點(diǎn)(來(lái)自玉伯回答):
- 對(duì)于依賴的模塊,AMD 是提前執(zhí)行,CMD 是延遲執(zhí)行。不過(guò) RequireJS 從 2.0 開(kāi)始,也改成可以延遲執(zhí)行(根據(jù)寫(xiě)法不同,處理方式不同)。CMD 推崇 as lazy as possible.
- CMD 推崇依賴就近,AMD 推崇依賴前置。
如果說(shuō)的不清楚,那么我們直接看上面的代碼用 AMD 該怎么寫(xiě):
- define(['./a', './b'], function(a, b) {
- a.doSomething();
- b.doSomething();
- ...
- })
UMD
UMD,全稱 Universal Module Definition,即通用模塊規(guī)范。既然 CommonJs 和 AMD 風(fēng)格一樣流行,那么需要一個(gè)可以統(tǒng)一瀏覽器端以及非瀏覽器端的模塊化方案的規(guī)范。
直接來(lái)看看官方給出的 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的實(shí)現(xiàn)很簡(jiǎn)單:
- 先判斷是否支持 AMD(define 是否存在),存在則使用 AMD 方式加載模塊;
- 再判斷是否支持 Node.js 模塊格式(exports 是否存在),存在則使用 Node.js 模塊格式;
- 前兩個(gè)都不存在,則將模塊公開(kāi)到全局(window 或 global);
ES Modules
當(dāng)然,以上說(shuō)的種種都是社區(qū)提供的方案,歷史上,JavaScript 一直沒(méi)有模塊系統(tǒng),直到 ES6 在語(yǔ)言標(biāo)準(zhǔn)的層面上,實(shí)現(xiàn)了它。其設(shè)計(jì)思想是盡量的靜態(tài)化,使得編譯時(shí)就能確定模塊的依賴關(guān)系,以及輸入和輸出的變量。CommonJS 和 AMD 模塊,都只能在運(yùn)行時(shí)確定這些東西。比如,CommonJS 模塊就是對(duì)象,輸入時(shí)必須查找對(duì)象屬性。而 ES Modules 不是對(duì)象,而是通過(guò) export 命令顯式指定輸出的代碼。
ES Modules 的模塊化能力由 export 和 import 組成,export 命令用于規(guī)定模塊的對(duì)外接口,import 命令用于輸入其他模塊提供的功能。我們可以這樣定義一個(gè)模塊:
- // 第一種方式
- 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';
除以上兩種命令外,還有一個(gè) export default 命令用于指定模塊的默認(rèn)輸出(一個(gè)模塊只能有一個(gè)默認(rèn)輸出)。如果使用了 export default 語(yǔ)法,在 import 時(shí)則可以任意命名。由于 export default 命令的本質(zhì)是將后面的值,賦給 default 變量,所以也可以直接將一個(gè)值寫(xiě)在 export default 之后。當(dāng)然,引用方式也存在多種:
- import { default as foo } from 'module';
- import foo from 'module';
需要注意的是 Modules 會(huì)自動(dòng)采用嚴(yán)格模式,且 import 命令具有提升效果,會(huì)提升到整個(gè)模塊的頭部,首先執(zhí)行。
延伸閱讀 JavaScript 模塊的循環(huán)加載
webpack 打包輸出配置
說(shuō)完理論,來(lái)看看實(shí)際項(xiàng)目中遇到的問(wèn)題。當(dāng)我們開(kāi)發(fā)完一個(gè) JavaScript 模塊必然要經(jīng)歷打包的流程,而在 webpack 配置中,通過(guò)指定 output 選項(xiàng)就可以告訴 webpack 如何輸出 bundle, asset 以及其他載入的內(nèi)容。那么如何實(shí)現(xiàn)不同環(huán)境可兼容的構(gòu)建呢?
- import:通過(guò) ES Modules 規(guī)范語(yǔ)法進(jìn)入引入;
- 變量:作為一個(gè)全局變量,比如通過(guò) script 標(biāo)簽來(lái)訪問(wèn);
- this:通過(guò) this 對(duì)象訪問(wèn);
- window:在瀏覽器中通過(guò) window 對(duì)象訪問(wèn);
- UMD:在 AMD 或 CommonJS 通過(guò) require 引入后訪問(wèn);
output 中有一個(gè)屬性叫做 libraryTarget,被用來(lái)指定如何暴露你的模塊的屬性。你可以這樣嘗試賦值給一個(gè)變量或者指定對(duì)象的屬性:
- // 加載完成后將模塊賦值給一個(gè)指定變量(默認(rèn)值)
- {
- libraryTarget: 'var',
- ...
- }
- // 賦值為指定對(duì)象的一個(gè)屬性,比如 `this` 或者 `window`
- {
- libraryTarget: "this",
- // libraryTarget: "window",
- ...
- }
- // 同樣的,若是指定 commonjs,那么便可以將模塊分配給 exports,這也意味著可以用于 CommonJS 環(huán)境:
- {
- libraryTarget: "commonjs",
- ...
- }
如果需要更完整的模塊化 bundle,以確保和各模塊系統(tǒng)兼容,那么可以這樣嘗試:
- // 內(nèi)容分配給 module.exports 對(duì)象,用于 CommonJS 環(huán)境
- {
- libraryTarget: 'commonjs2',
- ...
- }
- // 暴露為 AMD 模塊,通過(guò)特定屬性引入
- {
- libraryTarget: 'amd',
- ...
- }
- // 所有模塊系統(tǒng)兼容的萬(wàn)金油,可以在 CommonJS, AMD 環(huán)境下運(yùn)行,或?qū)⒛K導(dǎo)出到 global 下的變量
- {
- libraryTarget: 'umd',
- ...
- }
因此,如果只看 output 內(nèi)容,那么我的一個(gè) webpack 生產(chǎn)環(huán)境配置可以寫(xiě)成這樣:
- module.exports = {
- output: {
- // webpack 如何輸出結(jié)果的相關(guān)選項(xiàng)
- path: path.resolve(__dirname, "dist"),
- filename: 'index.js',
- library: 'hijiangtao',
- umdNamedDefine: true,
- libraryTarget: 'umd',
- },
- }