Js模組化方案總結
本文包含兩部分,第一部分通過簡明的描述介紹什麼是 Commonjs、AMD、CMD、UMD、ES Module 以及它們的常見用法,第二部分則根據實際問題指出在正常的webpack構建過程中該如何指定打包配置中的模組化引數。
JavaScript模組化方案
模組化這個話題在 ES6 之前是不存在的,因此這也被詬病為早期JavaScript開發全域性汙染和依賴管理混亂問題的源頭。這類歷史淵源和發展概述在本文將不會提及,因此感興趣可以自行搜尋 JavaScript 發展史進行了解。
直接進入正題,我們來看看常見的模組化方案都有哪些以及他們都有哪些內容。
1. Commonjs
CommonJS 的一個模組就是一個指令碼檔案,通過執行該檔案來載入模組。CommonJS 規範規定,每個模組內部, module變數代表當前模組。這個變數是一個物件,它的 exports 屬性(即module.exports)是對外的介面。載入某個模組,其實是載入該模組的 module.exports屬性。
我們見過這樣的模組引用:
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 模組的特點:
- 所有程式碼都執行在模組作用域,不會汙染全域性作用域。
- 獨立性是模組的重要特點就,模組內部最好不與程式的其他部分直接互動。
- 模組可以多次載入,但是隻會在第一次載入時執行一次,然後執行結果就被快取了,以後再載入,就直接讀取快取結果。要想讓模組再次執行,必須清除快取。
- 模組載入的順序,按照其在程式碼中出現的順序。
2. AMD
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);
});
3. CMD
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();
...
})
4. UMD
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);
5. ES Modules
當然,以上說的種種都是社群提供的方案,歷史上,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 模組的迴圈載入
資源搜尋網站大全 https://www.renrenfan.com.cn 廣州VI設計公司https://www.houdianzi.com
webpack打包輸出配置
說完理論,來看看實際專案中遇到的問題。當我們開發完一個 JavaScript 模組必然要經歷打包的流程,而在 webpack 配置中,通過指定 output 選項就可以告訴 webpack 如何輸出 bundle, asset 以及其他載入的內容。那麼如何實現不同環境可相容的構建呢?
import
this
window
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',
},
}