前言
前端的模組化之路經歷了漫長的過程,這裡根據大佬們寫的文章,將模組化規範部分做了彙總和整理,想詳細瞭解的小夥伴可以看浪裡行舟大神寫的前端模組化詳解(完整版),希望讀完的小夥伴能有些收穫,也希望覺得有用的小夥伴可以點個贊,筆芯。
什麼是模組
- 將一個複雜的程式依據一定的規則(規範)封裝成幾個塊(檔案), 並進行組合在一起
- 塊的內部資料與實現是私有的,只是向外部暴露一些介面(方法)與外部其它模組通訊
CommonJS
Node 應用由模組組成,採用 CommonJS 模組規範。每個檔案就是一個模組,有自己的作用域。在一個檔案裡面定義的變數、函式、類,都是私有的,對其他檔案不可見。在伺服器端,模組的載入是執行時同步載入的;在瀏覽器端,模組需要提前編譯打包處理。
CommonJS規範載入模組是同步的,也就是說,只有載入完成,才能執行後面的操作。
基本語法:
- 暴露模組:
module.exports = value
或exports.xxx = value
- 引入模組:
require(xxx)
,如果是第三方模組,xxx為模組名;如果是自定義模組,xxx為模組檔案路徑
但是,CommonJs有一個重大的侷限使得它不適用於瀏覽器環境,那就是require
操作是同步的。這對伺服器端不是一個問題,因為所有的模組都存放在本地硬碟,可以同步載入完成,等待時間就是硬碟的讀取時間。但是,對於瀏覽器,這卻是一個大問題,因為模組都放在伺服器端,等待時間取決於網速的快慢,可能要等很長時間,瀏覽器處於”假死”狀態。
因此,瀏覽器端的模組,不能採用”同步載入”(synchronous),只能採用”非同步載入”(asynchronous),這就是AMD規範誕生的背景。
AMD
特點:非同步載入模組,允許指定回撥函式,瀏覽器端一般採用AMD規範
代表作:require.js
用法:
//定義沒有依賴的模組
define(function(){
return 模組
})
//定義有依賴的模組
define(['module1', 'module2'], function(m1, m2){
return 模組
})
//引入使用模組
require(['module1', 'module2'], function(m1, m2){
//使用m1/m2
})
複製程式碼
CMD
特點:專門用於瀏覽器端,模組的載入是非同步的,模組使用時才會載入執行
代表作:Sea.js
用法:
//定義沒有依賴的模組
define(function(require, exports, module){
exports.xxx = value
module.exports = value
})
//定義有依賴的模組
define(function(require, exports, module){
//引入依賴模組(同步)
var module2 = require('./module2')
//引入依賴模組(非同步)
require.async('./module3', function (m3) {
})
//暴露模組
exports.xxx = value
})
//引入使用模組
define(function (require) {
var m1 = require('./module1')
var m4 = require('./module4')
m1.show()
m4.show()
})
複製程式碼
CMD與AMD區別
AMD和CMD最大的區別是對依賴模組的執行時機處理不同,而不是載入的時機或者方式不同,二者皆為非同步載入模組。
AMD依賴前置,js可以方便知道依賴模組是誰,立即載入;
而CMD就近依賴,需要使用把模組變為字串解析一遍才知道依賴了那些模組,這也是很多人詬病CMD的一點,犧牲效能來帶來開發的便利性,實際上解析模組用的時間短到可以忽略。
一句話總結: 兩者都是非同步載入,只是執行時機不一樣。AMD是依賴前置,提前執行,CMD是依賴就近,延遲執行。
UMD
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 ...
});
複製程式碼
ES6模組化
ES6 模組的設計思想是儘量的靜態化,使得編譯時就能確定模組的依賴關係,以及輸入和輸出的變數。CommonJS 和 AMD 模組,都只能在執行時確定這些東西。比如,CommonJS 模組就是物件,輸入時必須查詢物件屬性。
ES6 Module預設目前還沒有被瀏覽器支援,需要使用babel,在日常寫demo的時候經常會顯示這個錯誤:
ES6模組使用import
關鍵字匯入模組,export
關鍵字匯出模組:
/** 匯出模組的方式 **/
var a = 0;
export { a }; //第一種
export const b = 1; //第二種
let c = 2;
export default { c }//第三種
let d = 2;
export default { d as e }//第四種,別名
/** 匯入模組的方式 **/
import { a } from './a.js' //針對export匯出方式,.js字尾可省略
import main from './c' //針對export default匯出方式,使用時用 main.c
import 'lodash' //僅僅執行lodash模組,但是不輸入任何值
複製程式碼
命名式匯出與預設匯出
export {<變數>}
這種方式一般稱為 命名式匯出 或者 具名匯出,匯出的是一個變數的引用。
export default
這種方式稱為 預設匯出 或者 匿名匯出,匯出的是一個值。
舉例:
// a.js
let x = 10
let y = 20
setTimeout(()=>{
x = 100
y = 200
},100)
export { x }
export default y
// b.js
import { x } from './a.js'
import y from './a.js'
setTimeout(()=>{
console.log(x,y) // 100,20
},100)
複製程式碼
ES6 模組與 CommonJS 模組的差異
① CommonJS 模組輸出的是一個值的拷貝,ES6 模組輸出的是值的引用。
CommonJS 模組輸出的是值的拷貝,也就是說,一旦輸出一個值,模組內部的變化就影響不到這個值。而且,CommonJS 模組無論載入多少次,都只會在第一次載入時執行一次,以後再載入,返回的都是第一次執行結果的快取,除非手動清除系統快取。
ES6 模組的執行機制與 CommonJS 不一樣,JS 引擎對指令碼靜態分析的時候,遇到模組載入命令import
,就會生成一個只讀引用,等到指令碼真正執行時,再根據這個只讀引用,到被載入的那個模組裡面去取值。換句話說,ES6 的import
有點像 Unix 系統的“符號連線”,原始值變了,import
載入的值也會跟著變。因此,ES6 模組是動態引用,並且不會快取值,模組裡面的變數繫結其所在的模組。
② CommonJS 模組是執行時載入,ES6 模組是編譯時輸出介面。
CommonJS 載入的是一個物件(即module.exports
屬性),該物件只有在指令碼執行完才會生成。即在輸入時是先載入整個模組,生成一個物件,然後再從這個物件上面讀取方法,這種載入稱為“執行時載入”。
例如:
// CommonJS模組
let { stat, exists, readFile } = require('fs');
// 等同於
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
複製程式碼
上面程式碼的實質是整體載入fs模組(即載入fs的所有方法),生成一個物件(_fs),然後再從這個物件上面讀取 3 個方法。因為只有執行時才能得到這個物件,導致完全沒辦法在編譯時做“靜態優化”。
ES6 模組不是物件,它的對外介面只是一種靜態定義,在程式碼靜態解析階段就會生成。通過export
命令顯式指定輸出的程式碼,import
時採用靜態命令的形式。即在import
時可以指定載入某個輸出值,而不是載入整個模組,這種載入稱為“編譯時載入”或者“靜態載入”。
// ES6模組
import { stat, exists, readFile } from 'fs';
複製程式碼
上面程式碼的實質是從fs模組載入 3 個方法,其他方法不載入。即 ES6 可以在編譯時就完成模組載入,效率要比 CommonJS 模組的載入方式高。當然,這也導致了沒法引用 ES6 模組本身,因為它不是物件。
由於 ES6 模組是編譯時載入,使得靜態分析成為可能。有了它,就能進一步拓寬 JavaScript 的語法,比如引入巨集(macro)和型別檢驗(type system)這些只能靠靜態分析實現的功能。
除了靜態載入帶來的各種好處,ES6 模組還有以下好處:
- 不再需要UMD模組格式了,將來伺服器和瀏覽器都會支援 ES6 模組格式。目前,通過各種工具庫,其實已經做到了這一點。
- 將來瀏覽器的新API 就能用模組格式提供,不再必須做成全域性變數或者
navigator
物件的屬性。 - 不再需要物件作為名稱空間(比如
Math
物件),未來這些功能可以通過模組提供。
總結
- CommonJS規範主要用於服務端程式設計,載入模組是同步的,這並不適合在瀏覽器環境,因為同步意味著阻塞載入,瀏覽器資源是非同步載入的,因此有了AMD、CMD解決方案。
- AMD規範在瀏覽器環境中非同步載入模組,而且可以並行載入多個模組。不過,AMD規範開發成本高,程式碼的閱讀和書寫比較困難,模組定義方式的語義不順暢。
- CMD規範與AMD規範很相似,都用於瀏覽器程式設計,依賴就近,延遲執行,可以很容易在
Node.js
中執行。不過,依賴SPM打包,模組的載入邏輯偏重。 - ES6 在語言標準的層面上,實現了模組功能,而且實現得相當簡單,完全可以取代 CommonJS 和 AMD 規範,成為瀏覽器和伺服器通用的模組解決方案。
以上是本篇文章的內容,歡迎大家提出自己的想法,我們一起學習進步,與君共勉。