CommonJS是服務器模塊的規範,Node.js採用了這個規範。html
根據 CommonJS
規範,一個單獨的文件就是一個模塊,每個模塊都是一個單獨的做用域,在一個文件定義的變量(還包括函數和類),都是私有的,對其餘文件是不可見的。前端
CommonJS規範加載模塊是同步的,也就是說,只有加載完成,才能執行後面的操做。node
CommonJS
中,加載模塊使用 require
方法。該方法讀取一個文件並執行,最後返回文件內部的 exports
對象。jquery
Node.js
主要用於服務器編程,加載的模塊文件通常都已經存在本地硬盤,加載起來較快,不用考慮異步加載的方式,因此CommonJS
的同步加載模塊規範是比較適用的。webpack但若是是瀏覽器環境,要從服務器加載模塊,這是就必須採用異步模式。因此就有了
AMD
,CMD
等解決方案。git
var x = 5; var addX = function(value) { return value + x; }; module.exports.x = x; module.exports.addX = addX; // 也能夠改寫爲以下 module.exports = { x: x, addX: addX, }; 複製代碼
let math = require('./math.js'); console.log('math.x',math.x); console.log('math.addX', math.addX(4)); 複製代碼
AMD
= Asynchronous Module Definition
,即 異步模塊定義。AMD
規範加載模塊是異步的,並容許函數回調,沒必要等到全部模塊都加載完成,後續操做能夠正常執行。AMD
中,使用 require
獲取依賴模塊,使用 exports
導出 API
。//規範 API define(id?, dependencies?, factory); define.amd = {}; // 定義無依賴的模塊 define({ add: function(x,y){ return x + y; } }); // 定義有依賴的模塊 define(["alpha"], function(alpha){ return { verb: function(){ return alpha.verb() + 1; } } }); 複製代碼
require([module], callback) 中
callback
爲模塊加載完成後的回調函數程序員
//加載 math模塊,完成以後執行回調函數 require(['math'], function(math) {  math.add(2, 3); }); 複製代碼
RequireJS
是一個前端模塊化管理的工具庫,遵循 AMD
規範,RequireJS
是對 AMD
規範的闡述。es6
RequireJS
基本思想爲,經過一個函數來將全部所需的或者所依賴的模塊裝載進來,而後返回一個新的函數(模塊)。後續全部的關於新模塊的業務代碼都在這個函數內部操做。github
RequireJS
要求每一個模塊均放在獨立的文件之中,並使用 define
定義模塊,使用 require
方法調用模塊。web
按照是否有依賴其餘模塊狀況,能夠分爲 獨立模塊 和 非獨立模塊。
define({ method1: function(){}, method2: function(){} }); //等價於 define(function() { return { method1: function(){}, method2: function(){} } }); 複製代碼
define([ 'module1', 'module2' ], function(m1, m2) { ... }); //等價於 define(function(require) { var m1 = require('module1'); var m2 = require('module2'); ... }); 複製代碼
require
方法調用模塊require(['foo', 'bar'], function(foo, bar) { foo.func(); bar.func(); }); 複製代碼
CMD
= Common Module Definition
,即 通用模塊定義。CMD
是 SeaJS
在推廣過程當中對模塊定義的規範化產出。
CMD規範和AMD相似,都主要運行於瀏覽器端,寫法上看起來也很相似。主要是區別在於 模塊初始化時機
CMD
推崇依賴就近,AMD
推崇依賴前置。AMD
的 API
默認是一個當多個用,CMD
嚴格的區分推崇職責單一。例如,AMD
裏 require
分全局的和局部的。CMD裏面沒有全局的 require
,提供 seajs.use()
來實現模塊系統的加載啓動。CMD
裏每一個 API
都簡單純粹。//AMD define(['./a','./b'], function (a, b) { //依賴一開始就寫好 a.test(); b.test(); }); //CMD define(function (requie, exports, module) { //依賴能夠就近書寫 var a = require('./a'); a.test(); ... //軟依賴 if (status) { var b = requie('./b'); b.test(); } }); 複製代碼
使用Sea.js,在書寫文件時,須要遵照CMD(Common Module Definition)模塊定義規範。一個文件就是一個模塊。
exports
暴露接口。這意味着不須要命名空間了,更不須要全局變量。這是一種完全的命名衝突解決方案。require
引入依賴。這可讓依賴內置,開發者只需關心當前模塊的依賴,其餘事情 Sea.js
都會自動處理好。對模塊開發者來講,這是一種很好的 關注度分離,能讓程序員更多地享受編碼的樂趣。define
定義模塊,更多詳情參考SeasJS | 極客學院。例如,對於下述util.js
代碼
var org = {}; org.CoolSite = {}; org.CoolSite.Utils = {}; org.CoolSite.Utils.each = function (arr) { // 實現代碼 }; org.CoolSite.Utils.log = function (str) { // 實現代碼 }; 複製代碼
能夠採用SeaJS重寫爲
define(function(require, exports) { exports.each = function (arr) { // 實現代碼 }; exports.log = function (str) { // 實現代碼 }; }); 複製代碼
經過 exports
就能夠向外提供接口。經過 require('./util.js')
就能夠拿到 util.js
中經過 exports
暴露的接口。這裏的 require
能夠認爲是 Sea.js
給 JavaScript 語言增長的一個語法關鍵字,經過 require
能夠獲取其餘模塊提供的接口。
define(function(require, exports) { var util = require('./util.js'); exports.init = function() { // 實現代碼 }; }); 複製代碼
兩者區別主要表如今模塊初始化時機
AMD(RequireJS)中只要模塊做爲依賴時,就會加載並初始化。即儘早地執行(依賴)模塊。至關於全部的require都被提早了,並且模塊執行的順序也不必定100%就是require書寫順序。
CMD(SeaJS)中,模塊做爲依賴且被引用時纔會初始化,不然只會加載。即只會在模塊真正須要使用的時候才初始化。模塊加載的順序是嚴格按照require書寫的順序。
從規範上來講,AMD 更加簡單且嚴謹,適用性更廣,而在RequireJS強力的推進下,在國外幾乎成了事實上的異步模塊標準,各大類庫也相繼支持AMD規範。
但從SeaJS與CMD來講,也作了不少不錯東西:一、相對天然的依賴聲明風格 二、小而美的內部實現 三、貼心的外圍功能設計 四、更好的中文社區支持。
UMD
= Universal Module Definition
,即通用模塊定義。UMD
是AMD
和 CommonJS
的糅合。
AMD
模塊以瀏覽器第一的原則發展,異步加載模塊。CommonJS
模塊以服務器第一原則發展,選擇同步加載。它的模塊無需包裝(unwrapped modules)。 這迫令人們又想出另外一個更通用的模式UMD
(Universal Module Definition),實現跨平臺的解決方案。
UMD
先判斷是否支持 Node.js
的模塊(exports
)是否存在,存在則使用 Node.js
模塊模式。再判斷是否支持 AMD
(define
是否存在),存在則使用 AMD
方式加載模塊。(function (window, factory) { if (typeof exports === 'object') { module.exports = factory(); } else if (typeof define === 'function' && define.amd) { define(factory); } else { window.eventUtil = factory(); } })(this, function () { //module ... }); 複製代碼
CommonJS
輸出的是值的拷貝。CommonJS
模塊是運行時加載,ES6 模塊是編譯時輸出接口。CommonJS 模塊輸出的是值的拷貝(類比於基本類型和引用類型的賦值操做)。對於基本類型,一旦輸出,模塊內部的變化影響不到這個值。對於引用類型,效果同引用類型的賦值操做。
// lib.js var counter = 3; var obj = { name: 'David' }; function changeValue() { counter++; obj.name = 'Peter'; }; module.exports = { counter: counter, obj: obj, changeValue: changeValue, }; 複製代碼
// main.js var mod = require('./lib'); console.log(mod.counter); // 3 console.log(mod.obj.name); // 'David' mod.changeValue(); console.log(mod.counter); // 3 console.log(mod.obj.name); // 'Peter' // Or console.log(require('./lib').counter); // 3 console.log(require('./lib').obj.name); // 'Peter' 複製代碼
counter
是基本類型值,模塊內部值的變化不影響輸出的值變化。obj
是引用類型值,模塊內部值的變化影響輸出的值變化。也能夠藉助取值函數(getter
),將 counter
轉爲引用類型值,效果以下。
在類的內部,可使用
get
和set
關鍵字,對某個屬性設置存執函數和取值函數,攔截該屬性的存取行爲。 —— class | 阮一峯
// lib.js var counter = 3; function incCounter() { counter++; } module.exports = { get counter() { return counter }, incCounter: incCounter, }; 複製代碼
// main.js var mod = require('./lib'); console.log(mod.counter); // 3 mod.incCounter(); console.log(mod.counter); // 4 複製代碼
ES6 模塊是動態關聯模塊中的值,輸出的是值得引用。原始值變了,import
加載的值也會跟着變。
ES6
模塊的運行機制與CommonJS
不同。JS 引擎對腳本靜態分析時,遇到模塊加載命令import
,就會生成一個只讀引用。等到腳本真正執行時,再根據這個只讀引用,到被加載的那個模塊裏面去取值。ES6 模塊中,原始值變了,import
加載的值也會跟着變。所以,ES6 模塊是動態引用,而且不會緩存值。 —— ES6 Module 的加載實現 | 阮一峯
// lib.js export let counter = 3; export function incCounter() { counter++; } // main.js import { counter, incCounter } from './lib'; console.log(counter); // 3 incCounter(); console.log(counter); // 4 複製代碼
CommonJS
模塊是運行時加載,ES6 模塊是編譯時輸出接口。
這是由於,CommonJS
加載的是一個對象(即 module.exports
屬性),該對象只有在腳本運行完纔會生成。而 ES6
模塊不是對象,它的對外接口只是一種靜態定義,在代碼靜態解析階段就會生成。
ES6 模塊是編譯時輸出接口,所以有以下2個特色
import
命令會被 JS 引擎靜態分析,優先於模塊內的其餘內容執行export
命令會有變量聲明提高的效果在文件中的任何位置引入 import
模塊都會被提早到文件頂部
// a.js console.log('a.js') import { foo } from './b'; // b.js export let foo = 1; console.log('b.js 先執行'); // 執行結果: // b.js 先執行 // a.js 複製代碼
雖然 a
模塊中 import
引入晚於 console.log('a')
,可是它被 JS 引擎經過靜態分析,提到模塊執行的最前面,優於模塊中的其餘部分的執行。
因爲 import
和 export
是靜態執行,因此 import
和 export
具備變量提高效果。即 import
和 export
命令在模塊中的位置並不影響程序的輸出。
// a.js import { foo } from './b'; 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'; console.log(a); // 執行結果: // { bar: undefined, bar2: undefined, bar3: [Function: bar3] } // a.js 複製代碼
a
模塊引用了 b
模塊,b
模塊也引用了 a
模塊,export
聲明的變量也是優於模塊其它內容的執行的。但具體對變量賦值須要等到執行到相應代碼的時候。
重複引入某個相同的模塊時,模塊只會執行一次。
CommonJS 模塊的重要特性是加載時執行,即腳本代碼在 require
的時候,就會所有執行。一旦出現某個模塊被「循環加載」,就只輸出已經執行的部分,還未執行的部分不會輸出。
//a.js exports.done = false; var b = require('./b.js'); console.log('在 a.js 之中,b.done = %j', b.done); exports.done = true; console.log('a.js 執行完畢'); 複製代碼
上面代碼之中,a.js
腳本先輸出一個 done
變量,而後加載另外一個腳本文件 b.js
。注意,此時 a.js
代碼就停在這裏,等待 b.js
執行完畢,再往下執行。
再看 b.js
的代碼。
//b.js exports.done = false; var a = require('./a.js'); console.log('在 b.js 之中,a.done = %j', a.done); exports.done = true; console.log('b.js 執行完畢'); 複製代碼
上面代碼之中,b.js
執行到第二行,就會去加載 a.js
,這時,就發生了「循環加載」。系統會 a.js
模塊對應對象的 exports
屬性取值,但是由於 a.js
尚未執行完,從 exports
屬性只能取回已經執行的部分,而不是最後的值。
a.js
已經執行的部分,只有一行。
exports.done = false; 複製代碼
所以,對於 b.js
來講,它從 a.js
只輸入一個變量 done
,值爲 false
。
而後,b.js
接着往下執行,等到所有執行完畢,再把執行權交還給 a.js
。因而,a.js
接着往下執行,直到執行完畢。咱們寫一個腳本 main.js
,驗證這個過程。
var a = require('./a.js'); var b = require('./b.js'); console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done); 複製代碼
執行 main.js
,運行結果以下。
$ node main.js 在 b.js 之中,a.done = false b.js 執行完畢 在 a.js 之中,b.done = true a.js 執行完畢 在 main.js 之中, a.done=true, b.done=true 複製代碼
上面的代碼證實了2點
b.js
之中,a.js
沒有執行完畢,只執行了第一行main.js
執行到第二行時,不會再次執行 b.js
,而是輸出緩存的 b.js
的執行結果,即它的第四行。exports.done = true; 複製代碼
總之,CommonJS 輸入的是被輸出值的拷貝,不是引用。
另外,因爲 CommonJS 模塊遇到循環加載時,返回的是當前已經執行的部分的值,而不是代碼所有執行後的值,二者可能會有差別。因此,輸入變量的時候,必須很是當心。
var a = require('a'); // 安全的寫法 導入總體,保證module已經執行完成 var foo = require('a').foo; // 危險的寫法 exports.good = function (arg) { return a.foo('good', arg); // 使用的是 a.foo 的最新值 }; exports.bad = function (arg) { return foo('bad', arg); // 使用的是一個部分加載時的值 }; 複製代碼
上面代碼中,若是發生循環加載,require('a').foo
的值極可能後面會被改寫,改用 require('a')
會更保險一點。
// a.js console.log('a starting'); exports.done = false; const b = require('./b'); console.log('in a, b.done =', b.done); exports.done = true; console.log('a done'); // b.js console.log('b starting'); exports.done = false; const a = require('./a'); console.log('in b, a.done =', a.done); exports.done = true; console.log('b done'); // node a.js // 執行結果: // a starting // b starting // in b, a.done = false // b done // in a, b.done = true // a done 複製代碼
從上面的執行過程當中,能夠看到,在 CommonJS 規範中,當遇到 require()
語句時,會執行 require
模塊中的代碼,並緩存執行的結果,當下次再次加載時不會重複執行,而是直接取緩存的結果。正由於此,出現循環依賴時纔不會出現無限循環調用的狀況。
跟 CommonJS 模塊同樣,ES6 不會再去執行重複加載的模塊,又因爲 ES6 動態輸出綁定的特性,能保證 ES6 在任什麼時候候都能獲取其它模塊當前的最新值。
ES6 模塊在編譯時就會靜態分析,優先於模塊內的其餘內容執行,因此致使了咱們沒法寫出像下面這樣的代碼
if(some condition) { import a from './a'; }else { import b from './b'; } // or import a from (str + 'b'); 複製代碼
由於編譯時靜態分析,致使了咱們沒法在條件語句或者拼接字符串模塊,由於這些都是須要在運行時才能肯定的結果在 ES6 模塊是不被容許的,因此 動態引入import()
應運而生。
import()
容許你在運行時動態地引入 ES6 模塊,想到這,你可能也想起了 require.ensure
這個語法,可是它們的用途卻大相徑庭的。
require.ensure
的出現是 webpack
的產物,它是由於瀏覽器須要一種異步的機制能夠用來異步加載模塊,從而減小初始的加載文件的體積,因此若是在服務端的話, require.ensure
就無用武之地了,由於服務端不存在異步加載模塊的狀況,模塊同步進行加載就能夠知足使用場景了。 CommonJS 模塊能夠在運行時確認模塊加載。
而 import()
則不一樣,它主要是爲了解決 ES6 模塊沒法在運行時肯定模塊的引用關係,因此須要引入 import()
。
先來看下它的用法
import()
提供一個基於 Promise
的 API
import()
能夠在腳本的任何地方使用 import()
接受字符串文字,能夠根據須要構造說明符// a.js const str = './b'; const flag = true; if(flag) { import('./b').then(({foo}) => { console.log(foo); }) } import(str).then(({foo}) => { console.log(foo); }) // b.js export const foo = 'foo'; // babel-node a.js // 執行結果 // foo // foo 複製代碼
固然,若是在瀏覽器端的 import()
的用途就會變得更普遍,好比 按需異步加載模塊,那麼就和 require.ensure
功能相似了。
由於是基於 Promise
的,因此若是你想要同時加載多個模塊的話,能夠是 Promise.all
進行並行異步加載。
Promise.all([ import('./a.js'), import('./b.js'), import('./c.js'), ]).then(([a, {default: b}, {c}]) => { console.log('a.js is loaded dynamically'); console.log('b.js is loaded dynamically'); console.log('c.js is loaded dynamically'); }); 複製代碼
還有 Promise.race
方法,它檢查哪一個 Promise
被首先 resolved
或 reject
。咱們可使用 import()
來檢查哪一個 CDN
速度更快:
const CDNs = [ { name: 'jQuery.com', url: 'https://code.jquery.com/jquery-3.1.1.min.js' }, { name: 'googleapis.com', url: 'https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js' } ]; console.log(`------`); console.log(`jQuery is: ${window.jQuery}`); Promise.race([ import(CDNs[0].url).then(()=>console.log(CDNs[0].name, 'loaded')), import(CDNs[1].url).then(()=>console.log(CDNs[1].name, 'loaded')) ]).then(()=> { console.log(`jQuery version: ${window.jQuery.fn.jquery}`); }); 複製代碼
固然,若是你以爲這樣寫還不夠優雅,也能夠結合 async/await
語法糖來使用。
async function main() { const myModule = await import('./myModule.js'); const {export1, export2} = await import('./myModule.js'); const [module1, module2, module3] = await Promise.all([ import('./module1.js'), import('./module2.js'), import('./module3.js'), ]); } 複製代碼
動態 import()
爲咱們提供了以異步方式使用 ES 模塊的額外功能。
根據咱們的需求動態或有條件地加載它們,這使咱們可以更快,更好地建立更多優點應用程序。
Webpack容許使用不一樣的模塊類型,可是底層
必須使用同一種實現。全部的模塊能夠直接在盒外運行。
import MyModule from './MyModule.js'; 複製代碼
var MyModule = require('./MyModule.js'); 複製代碼
define(['./MyModule.js'], function (MyModule) { }); 複製代碼