咱們可能常常聽到一些模塊化的概念,譬如 AMD
、CommonJS
或 ES Modules
。這些又是什麼概念呢?它們爲何而存在,做用又是什麼呢?本文將對模塊化的概念進行逐一分析。node
在瞭解模塊化的概念前,首先先解決一個問題 - 爲何須要模塊化?webpack
先從實際問題出發,在相似 require.js
、sea.js
、browserify
、webpack
等工具出現以前,咱們可能會遇到以下一些問題:web
js
文件時,可能會出現命名衝突。a.js
中引用了 b.js
,而 b.js
中也引用了 a.js
;二者相互依賴。那咱們如何決定先引用哪一個件呢?咱們再從一個生活中的例子出發,簡要了解一下模塊化的優勢,即爲何須要模塊化的緣由。npm
假設你有一套修理工具箱,裏面包含了屬於這套修理工具箱的各類型號的螺絲批、鉗子和錘子等等。每次家裏水管破了,燈泡壞了什麼的,你均可以拿這套修理工具箱進行修理。每次修理可能都會形成一些工具的損耗或損壞,損壞以後咱們就應該去買相同型號的工具進行補充;其餘不對頭的工具不會回收回這套修理工具箱中,一樣這套修理工具箱中的工具也不會隨意扔出工具箱中。設計模式
咱們把上述例子轉化爲模塊化來看看。首先,修理工具箱就是 模塊
,裏面的工具就是 模塊
中的各類變量或函數。工具出現損壞等於 模塊
內出了什麼問題,這時候咱們只須要修復 模塊
內的 bug
就行了。其餘不對頭的工具不會回收回工具箱中,反之工具箱中的工具不會隨意被扔出表示 模塊
內的變量、函數等不會污染外部的變量、函數等等,反之亦然。這套工具箱能夠重複利用也就是 模塊
的複用性很強。數組
總結模塊化有三大優勢:瀏覽器
接下來將會經過一些常見的例子與概念來解釋什麼是模塊化。注意,閉包在模塊化中有着重要的應用,這裏假設你對閉包概念已有所瞭解。安全
IIFE
- 當即執行函數表達式顧名思義,當即執行函數表達式就是一個函數在定義時就會當即執行。服務器
var global = "I'm global"
(function () {
var foo = 'foo'
function bar () {
console.log('bar')
}
console.log(foo)
bar()
console.log(global)
})()
// foo
// bar
// I'm global
var foo = 'global foo'
console.log(foo) // global foo
bar() // Uncaught ReferenceError: bar is not defined
複製代碼
能夠看到,咱們在當即執行函數表達式的外部訪問其變量會拋出錯誤,而在當即執行函數表達式的內部能夠隨時訪問外部變量。外部與當即執行函數表達式一樣有命名爲 foo
的變量,但這二者互不影響。其實這種行爲就相似於 C++
等語言中類的私有變量、私有方法。閉包
固然咱們還可讓當即執行函數表達式放回一些東西,相似類的暴露公共方法、變量的概念。
var module = (function () {
var _privateCnt = 0
var _privateProperty = 'I am private property'
function _privateCnter () {
return _privateCnt += 1
}
function publicCnter () {
return _privateCnter()
}
return {
property: _privateProperty,
publicCnter: publicCnter
}
})()
console.log(module.property) // I am private property
console.log(module.publicCnter()) // 1
console.log(module.publicCnter()) // 2
console.log(module.publicCnter()) // 3
複製代碼
這個例子展現了經過當即執行函數表達式將一些變量、方法暴露出去,並防止外部直接修改一些咱們不但願修改的變量、方法。這樣作還有一個好處,就是咱們能夠快速地瞭解到這個當即執行函數爲咱們提供了哪些公共屬性及方法,而不須要閱讀全部邏輯代碼。這種方式在設計模式中也稱做 模塊模式(Module Pattern)
。
CommonJS
CommonJS
主要是爲服務端定義的模塊規範,它一開始的名字爲 ServerJS
。npm
生態系統基本都是基於 CommonJS
規範所創建起來的。
// 在 foo.js 中,咱們導出了變量 foo
module.exports = {
foo: 'foo'
}
// 在 bar.js,咱們經過 require 引入了變量 foo
var module = require('foo')
console.log(module.foo) // foo
複製代碼
看起來很簡單是吧。可能有人會問了,這個 module
是什麼東西呢?其實 module
是 Node 中的一個內置對象。咱們能夠在 node
環境下打印看看
咱們能夠看到 module
有好幾個屬性,其中 id
是爲了讓 node
知道這個模塊在哪裏,是啥;exports
就是咱們要導出的對象了。
在確保 foo.js
和 bar.js
在同一目錄下,咱們再將例子稍加修改:
// foo,js
module.exports = {
foo: 'foo'
}
console.log('module: ', module)
// bar.js
var module = require('./foo')
console.log(module.foo)
複製代碼
運行 node bar.js
能夠獲得如下信息:
經過 CommonJS
規範定義的模塊一樣有一開始說到的模塊的三大優勢,其實咱們只須要把這些模塊文件看出一個個當即執行函數,也就會很好理解了。
在 CommonJS
裏模塊都是同步加載的,在瀏覽器中若是同步去加載模塊的話會形成阻塞,致使頁面性能降低;而在服務端中,由於文件都存在於同一個硬盤上,因此即便是同步加載都不會有什麼影響。
再補充一個小細節,你可能時不時能看到 var exports = module.exports
這樣的代碼。或許你會問爲何要怎麼作,難道有什麼技巧嗎?其實這只是簡單的引用而已。即變量 exports
一樣指向了 module.exports
的內存地址,也就是二者指向的對象是徹底同樣的。咱們想在 module.exports
裏添加導出的東西時,只須要在 exports
里加就好了。就是這麼簡單,只不過被一些說法搞得高深莫測了而已。
var exports = module.exports
exports.foo = 'foo''
複製代碼
AMD - Asynchronous Module Definition
剛剛咱們說到 CommonJS
主要是用於服務端的規範,而客戶端是沒法使用它的,而且 CommonJS
是同步加載模塊的。因此咱們又有了叫作 AMD
規範的東西,也就是異步模塊定義規範。顧名思義,咱們能夠利用這個規範來作到模塊與模塊的依賴能夠經過異步的方式來加載;這也是瀏覽器(客戶端)所但願的。
AMD
中的核心就是 define
這個方法。
define(
module_id,
[dependencies],
definition
)
複製代碼
其中 define
中的 module_id
與 dependencies
爲可選參數。
首先 module_id
它是一個字符串,指的是定義的模塊的名字,這個名字必須是惟一的。第二個參數 dependencies
是模塊所依賴的模塊組成的數組,並做爲參數傳入給第三個參數 definition
工廠方法中。第三個參數 definition
就是爲模塊初始化要執行的函數或對象。若是爲函數,它應該只被執行一次。若是是對象,此對象應該爲模塊的輸出值。
// dep1
define('dep1', [], function () {
return {
doSomething: function () {
console.log('do something')
}
}
})
define('dep2', [], function () {
return {
doOtherThing: function () {
console.log('do other thing')
}
}
})
define('module', ['dep1', 'dep2'], function (dep1, dep2) {
dep1.doSomething()
dep2.doOtherThing()
})
複製代碼
雖然 AMD
規範提供了異步加載模塊的方案,可是給個人感受就是邏輯不如 CommonJS
直觀。所以在 ES6
中也就有了原生的模塊化: ESM - ES Modules
。
ES Modules
CommonJS
在服務端中應用普遍,但因爲它是同步加載模塊的,它在客戶端不太合適;而 AMD
支持瀏覽器異步加載模塊,但在服務端卻顯得沒有必要,所以 ES Modules
出現了。咱們先來看看 ES Modules
是如何工做的。
ES Modules
與 CommonJS
很類似,較新的瀏覽器均已支持 ES Modules
,Node.js
正在慢慢支持相關規範。
ES Modules
的核心爲 export
與 import
,分別對應導出模塊與導入模塊。
導出模塊:
// CommonJS
module.exports = foo () {
console.log('here is foo')
}
// ES Modules
export default function bar () {
console.log('here is bar')
}
複製代碼
導入模塊:
// CommonJS
var foo = require('./foo')
foo() // here is foo
// ES Modules
import bar from './bar'
bar() // here is bar
複製代碼
這二者這麼類似,它們在實際的表現上有什麼不一樣呢?
我分別在 Firefox
、Edge
和 Chrome
上測試(Chrome
因爲自身的安全策略沒法直接經過本地文件進行測試,因此利用插件 Web Server for Chrome
起了個本地服務器。
測試代碼以下圖(注意咱們在使用 ES Modules
時要給 script
標籤加上 type="module"
)
測試的結果顯示爲:
開始執行 bar.js
開始執行 foo.js
here is foo
here is bar
複製代碼
若是咱們使用 CommonJS
又會有什麼結果呢?
開始執行 foo.js
here is foo
開始執行 bar.js
here is bar
複製代碼
很顯然 CommonJS
是同步加載模塊的,因此代碼的執行也是順序的。而 ES Modules
是異步加載模塊的,且 ES Modules
是編譯時加載模塊,在運行時(執行代碼時)再根據相關的引用去被加載的模塊中取值。再詳細一點來講的話,整個過程分以下三個步驟:
exports
和 imports
的空間。這一步稱爲連接所以在編譯期間,編譯器先找到了 foo.js
的依賴 bar.js
,先編譯 bar.js
而後纔是 foo.js
。因此你纔會先看到 開始執行 bar.js
。
CommonJS
工做原理爲同步加載模塊,在 Node.js
中有着普遍的使用,對客戶端不友好。AMD
工做原理爲異步加載模塊。ES Modules
爲 ES6
推出的規範,客戶端的支持比較好,Node.js
將會慢慢全面支持。它與 AMD
同樣,也是異步加載模塊。