原文連接: http://yanjiie.mejavascript
偶然的一個週末複習了一下 JS 的模塊標準,刷新了一下對 JS 模塊化的理解。
從開始 Coding 以來,總會週期性地突發奇想進行 Code Review。既是對一段時期的代碼進行總結,也是對那一段時光的懷念。html
距離上一次 Review 已通過去近兩個月,此次居然把兩年前在源續寫的代碼翻了出來,代碼雜亂無章的程度就像那時更加浮躁的本身,讓人感慨時光流逝之快。前端
話很少說,直接上碼。java
當時在作的是一個境外電商項目(越南天寶商城),做爲非 CS 的新手程序員,接觸 Coding 時間不長和工程化觀念不強,在當時的項目中出現了這樣的代碼:webpack
import.js:
git
這段代碼看起來就是不斷地從 DOM 中插進 CSS 和 JS,雖然寫得很爛,可是很能反映之前的 Web 開發方式。程序員
在 Web 開發中,有一個原則叫「關注點分離(separation of concerns)「,意思是各類技術只負責本身的領域,不互相耦合混合在一塊兒,因此催生出了 HTML、CSS 和 JavaScript。es6
其中,在 Web 中負責邏輯和交互 的 JavaScript,是一門只用 10 天設計出來的語言,雖然借鑑了許多優秀靜態和動態語言的優勢,但卻一直沒有模塊 ( module ) 體系。這致使了它將一個大程序拆分紅互相依賴的小文件,再用簡單的方法拼裝起來。其餘語言都有這項功能,好比 Ruby
的 require
、Python
的 import
,甚至就連 CSS
都有 @import
,可是 JavaScript 任何這方面的支持都沒有。並且 JS 是一種加載即運行的技術,在頁面中插入腳本時還須要考慮庫的依賴,JS 在這方面的缺陷,對開發大型的、複雜的項目造成了巨大障礙。github
雖然 JS 自己並不支持模塊化,可是這並不能阻擋 JS 走向模塊化的道路。既然自己不支持,那麼就從代碼層面解決問題。活躍的社區開始制定了一些模塊方案,其中最主要的是 CommonJS 和 AMD,ES6 規範出臺以後,以一種更簡單的形式制定了 JS 的模塊標準 (ES Module),並融合了 CommonJS 和 AMD 的優勢。web
大體的發展過程:
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 等工具的模塊化語法糖轉碼產生了新的興趣,但願有時間能夠去分析一下模塊化的打包機制和轉譯代碼,而後整理出來加深一下本身對模塊化實現原理的認識和理解。
期待下一篇。
參考文章: