原文連接:http://yanjiie.mejavascript
偶然的一個週末複習了一下 JS 的模塊標準,刷新了一下對 JS 模塊化的理解。html
從開始 Coding 以來,總會週期性地突發奇想進行 Code Review。既是對一段時期的代碼進行總結,也是對那一段時光的懷念。前端
距離上一次 Review 已通過去近兩個月,此次居然把兩年前在源續寫的代碼翻了出來,代碼雜亂無章的程度就像那時更加浮躁的本身,讓人感慨時光流逝之快。java
話很少說,直接上碼。webpack
當時在作的是一個境外電商項目(越南天寶商城),做爲非 CS 的新手程序員,接觸 Coding 時間不長和工程化觀念不強,在當時的項目中出現了這樣的代碼:git
import.js: 程序員
這段代碼看起來就是不斷地從 DOM 中插進 CSS 和 JS,雖然寫得很爛,可是很能反映之前的 Web 開發方式。es6
在 Web 開發中,有一個原則叫「關注點分離(separation of concerns)「,意思是各類技術只負責本身的領域,不互相耦合混合在一塊兒,因此催生出了 HTML、CSS 和 JavaScript。github
其中,在 Web 中負責邏輯和交互 的 JavaScript,是一門只用 10 天設計出來的語言,雖然借鑑了許多優秀靜態和動態語言的優勢,但卻一直沒有模塊 ( module ) 體系。這致使了它將一個大程序拆分紅互相依賴的小文件,再用簡單的方法拼裝起來。其餘語言都有這項功能,好比 Ruby
的 require
、Python
的 import
,甚至就連 CSS
都有 @import
,可是 JavaScript 任何這方面的支持都沒有。並且 JS 是一種加載即運行的技術,在頁面中插入腳本時還須要考慮庫的依賴,JS 在這方面的缺陷,對開發大型的、複雜的項目造成了巨大障礙。web
雖然 JS 自己並不支持模塊化,可是這並不能阻擋 JS 走向模塊化的道路。既然自己不支持,那麼就從代碼層面解決問題。活躍的社區開始制定了一些模塊方案,其中最主要的是 CommonJS 和 AMD,ES6 規範出臺以後,以一種更簡單的形式制定了 JS 的模塊標準 (ES Module),並融合了 CommonJS 和 AMD 的優勢。
大體的發展過程:
CommonJS(服務端) => AMD (瀏覽器端) => CMD / UMD => ES Module
2009年,Node.js 橫空出世,JS 得以脫離瀏覽器運行,咱們可使用 JS 來編寫服務端的代碼了。對於服務端的 JS,沒有模塊化簡直是不能忍。
CommonJs (前 ServerJS) 在這個階段應運而生,制定了 Module/1.0 規範,定義了初版模塊標準。
標準內容:
exports
來向外暴露 API,exports
只能是一個對象,暴露的 API 須做爲此對象的屬性。require
,經過傳入模塊標識來引入其餘模塊,執行的結果即爲別的模塊暴露出來的 API。require
函數引入的模塊中也包含依賴,那麼依次加載這些依賴。特色:
它的語法看起來是這樣的:
// a.js
module.exports = {
moduleFunc: function() {
return true;
};
}
// 或
exports.moduleFunc = function() {
return true;
};
// 在 b.js 中引用
var moduleA = require('a.js');
// 或
var moduleFunc = require('a.js').moduleFunc;
console.log(moduleA.moduleFunc());
console.log(moduleFunc())
複製代碼
CommonJS 規範出現後,在 Node 開發中產生了很是良好的效果,開發者但願借鑑這個經驗來解決瀏覽器端 JS 的模塊化。
但大部分人認爲瀏覽器和服務器環境差異太大,畢竟瀏覽器端 JS 是經過網絡動態依次加載的,而不是像服務端 JS 存在本地磁盤中。所以,瀏覽器須要實現的是異步模塊,模塊在定義的時候就必須先指明它所須要依賴的模塊,而後把本模塊的代碼寫在回調函數中去執行,最終衍生出了 AMD 規範。
AMD 的主要思想是異步模塊,主邏輯在回調函數中執行,這和瀏覽器前端所習慣的開發方式不謀而合,RequireJS 應運而生。
標準內容:
define
來定義模塊,用法爲:define(id?, dependencies?, factory)
;["require", "exports", "module"]
,factory 中也會默認傳入 require, exports, module,與 ComminJS 中的實現保持一致特色:
它的用法看起來是這樣的:
// a.js
define(function (require, exports, module) {
console.log('a.js');
exports.name = 'Jack';
});
// b.js
define(function (require, exports, module) {
console.log('b.js');
exports.desc = 'Hello World';
});
// main.js
require(['a', 'b'], function (moduleA, moduleB) {
console.log('main.js');
console.log(moduleA.name + ', ' + moduleB.desc);
});
// 執行順序:
// a.js
// b.js
// main.js
複製代碼
人無完人,AMD/RequireJS 也存在飽受詬病的缺點。按照 AMD 的規範,在定義模塊的時候須要把全部依賴模塊都羅列一遍(前置依賴),並且在使用時還須要在 factory 中做爲形參傳進去。
define(['a', 'b', 'c', 'd', 'e', 'f', 'g'], function(a, b, c, d, e, f, g){ ..... });
複製代碼
看起來略微不爽 ...
RequireJS 模塊化的順序是這樣的:模塊預加載 => 所有模塊預執行 => 主邏輯中調用模塊
,因此實質是依賴加載完成後還會預先一一將模塊執行一遍,這種方式會使得程序效率有點低。
因此 RequireJS 也提供了就近依賴,會在執行至 require 方法纔會去進行依賴加載和執行,但這種方式的用戶體驗不是很好,用戶的操做會有明顯的延遲(下載依賴過程),雖然能夠經過各類 loading 去解決。
// 就近依賴
define(function () {
setTimeout(function () {
require(['a'], function (moduleA) {
console.log(moduleA.name);
});
}, 1000);
});
複製代碼
AMD/RequireJS 的 JS 模塊實現上有不少不優雅的地方,長期以來在開發者中廣受詬病,緣由主要是不能以一種更好的管理模塊的依賴加載和執行,雖然有不足的地方,但它提出的思想在當時是很是先進的。
既然優缺點那麼必然有人出來完善它,SeaJS 在這個時候出現。
SeaJS 遵循的是 CMD 規範,CMD 是在 AMD 基礎上改進的一種規範,解決了 AMD 對依賴模塊的執行時機處理問題。
SeaJS 模塊化的順序是這樣的:模塊預加載 => 主邏輯調用模塊前才執行模塊中的代碼
,經過依賴的延遲執行,很好解決了 RequireJS 被詬病的缺點。
SeaJS 用法和 AMD 基本相同,而且融合了 CommonJS 的寫法:
// a.js
define(function (require, exports, module) {
console.log('a.js');
exports.name = 'Jack';
});
// main.js
define(function (require, exports, module) {
console.log('main.js');
var moduleA = require('a');
console.log(moduleA.name);
});
// 執行順序
// main.js
// a.js
複製代碼
除此以外,SeaJS 還提供了 async API,實現依賴的延遲加載。
// main.js
define(function (require, exports, module) {
var moduleA = require.async('a');
console.log(moduleA.name);
});
複製代碼
SeaJS 的出現,貌似以一種比較完美的形式解決了 JS 模塊化的問題,是 CommonJS 在瀏覽器端的踐行者,並吸取了 RequestJS 的優勢。
ES Module 是目前 web 開發中使用率最高的模塊化標準。
隨着 JS 模塊化開發的呼聲愈來愈高,做爲 JS 語言規範的官方組織 ECMA 也開始將 JS 模塊化歸入 TC39 提案中,並在 ECMAScript 6.0 中獲得實踐。
ES Module 吸取了其餘方案的優勢並以更優雅的形式實現模塊化,它的思想是儘可能的靜態化,即在編譯時就肯定全部模塊的依賴關係,以及輸入和輸出的變量,和 CommonJS 和 AMD/CMD 這些標準不一樣的是,它們都是在運行時才能肯定須要依賴哪一些模塊而且執行它。ES Module 使得靜態分析成爲可能。有了它,就能進一步拓寬 JavaScript 的語法,實現一些只能靠靜態分析實現的功能(好比引入宏(macro)和類型檢驗(type system)。
標準內容:
export
和 import
。export
命令用於規定模塊的對外接口,import
命令用於輸入其餘模塊提供的功能。export
命令定義了模塊的對外接口,其餘 JS 文件就能夠經過 import
命令加載這個模塊。ES Module 能夠有多種用法:
模塊的定義:
/** * export 只支持對象形式導出,不支持值的導出,export default 命令用於指定模塊的默認輸出, * 只支持值導出,可是隻能指定一個,本質上它就是輸出一個叫作 default 的變量或方法 */
// 寫法 1
export var m = 1;
// 寫法 2
var m = 1;
export { m };
// 寫法 3
var n = 1;
export { n as m };
// 寫法 4
var n = 1;
export default n;
複製代碼
模塊的引入:
// 解構引入
import { firstName, lastName, year } from 'a-module';
// 爲輸入的變量從新命名
import { lastName as surname } from 'a-module';
// 引出模塊對象(引入全部)
import * as ModuleA from 'a-module';
複製代碼
在使用 ES Module 值得注意的是:import
和 export
命令只能在模塊的頂層,在代碼塊中將會報錯,這是由於 ES Module 須要在編譯時期進行模塊靜態優化,import
和 export
命令會被 JavaScript 引擎靜態分析,先於模塊內的其餘語句執行,這種設計有利於編譯器提升效率,但也致使沒法在運行時加載模塊(動態加載)。
對於這個缺點,TC39 有了一個新的提案 -- Dynamic Import,提案的內容是建議引入 import()
方法,實現模塊動態加載。
// specifier: 指定所要加載的模塊的位置
import(specifier)
複製代碼
import()
方法返回的是一個 Promise 對象。
import('b-module')
.then(module => {
module.helloWorld();
})
.catch(err => {
console.log(err.message);
});
複製代碼
import()
函數能夠用在任何地方,不只僅是模塊,非模塊的腳本也可使用。它是運行時執行,也就是說,何時運行到這一句,就會加載指定的模塊。另外,import()
函數與所加載的模塊沒有靜態鏈接關係,這點也是與 import
語句不相同。import()
相似於 Node 的 require
方法,區別主要是前者是異步加載,後者是同步加載。
經過 import
和 export
命令以及 import()
方法,ES Module 幾乎實現了 CommonJS/AMD/CMD 方案的全部功能,更重要的是它是做爲 ECMAScript 標準出現的,帶有正統基因,這也是它在如今 Web 開發中普遍應用的緣由之一。
但 ES Module 是在 ECMAScript 6.0 標準中的,而目前絕大多數的瀏覽器並直接支持 ES6 語法,ES Module 並不能直接使用在瀏覽器上,因此須要 Babel 先進行轉碼,將 import 和 export 命令轉譯成 ES2015 語法才能被瀏覽器解析。
JS 模塊化的出現使得前端工程化程度愈來愈高,讓使用 JS 開發大型應用成爲觸手可及的現實(VScode)。縱觀 JS 模塊化的發展,其中不少思想都借鑑了其餘優秀的動態語言(Python),而後結合 JS 運行環境的特色,衍生出符合自身的標準。但其實在本質上,瀏覽器端的 JS 仍沒有真正意義上的支持模塊化,只能經過工具庫(RequireJS、SeaJS)或者語法糖(ES Module)去 Hack 實現模塊化。隨着 Node 前端工程化工具的繁榮發展(Grunt/Gulp/webpack),使咱們能夠不關注模塊化的實現過程,直接享受 JS 模塊化編程的快感。
在複習 JS 模塊化的過程當中,對 Webpack 等工具的模塊化語法糖轉碼產生了新的興趣,但願有時間能夠去分析一下模塊化的打包機制和轉譯代碼,而後整理出來加深一下本身對模塊化實現原理的認識和理解。
期待下一篇。
參考文章: