原文鏈接:http:yanjiie.me有時的一個周末溫習了一下JS的模塊範例,革新了一下對JS模塊化的明白。從最先Coding以來,總會周期性地突發奇想舉行CodeReview。
原文鏈接: http://yanjiie.me
有時的一個周末溫習了一下 JS 的模塊範例,革新了一下對 JS 模塊化的明白。
從最先 Coding 以來,總會周期性地突發奇想舉行 Code Review。既是對一段時代的代碼舉行總結,也是對那一段時刻的思念。
間隔上一次 Review 已過去近兩個月,此次居然把兩年前在源續寫的代碼翻了出來,代碼亂七八糟的水平就像當時越發急躁的本身,讓人慨嘆時刻流逝之快。
話不多說,直接上碼。
當時在做的是一個境外電商項目(越南天寶商城),作為非 CS 的新手遞次員,打仗 Coding 時刻不長和工程化看法不強,在當時的項目中湧現了如許的代碼:
import.js:
![《探究 JS 中的模塊化》](https://img.php1.cn/3cd4a/1eebe/cd5/e62700fe09f8933e.webp)
這段代碼看起來就是不斷地從 DOM 中插進 CSS 和 JS,雖然寫得很爛,然則很能反應之前的 Web 開闢體式格局。
在 Web 開闢中,有一個準繩叫“關注點星散(separation of concerns)“,意義是種種手藝只擔任本身的範疇,不相互耦合夾雜在一起,所以催生出了 HTML、CSS 和 Javascript。
个中,在 Web 中擔任邏輯和交互 的 Javascript,是一門只用 10 天設想出來的言語,雖然自創了許多優異靜態和動態言語的長處,但卻一向沒有模塊 ( module ) 系統。這致使了它將一個大遞次拆分紅相互依靠的小文件,再用簡樸的要領拼裝起來。其他言語都有這項功用,比方 Ruby
的 require
、Python
的 import
,以至就連 CSS
都有 @import
,然則 Javascript 任何這方面的支撐都沒有。而且 JS 是一種加載即運轉的手藝,在頁面中插進去劇本時還須要斟酌庫的依靠,JS 在這方面的瑕玷,對開闢大型的、龐雜的項目形成了龐大停滯。
生長進程
雖然 JS 本身並不支撐模塊化,然則這並不能阻撓 JS 走向模塊化的途徑。既然本身不支撐,那末就從代碼層面處理題目。活潑的社區最先制訂了一些模塊計劃,个中最主要的是 CommonJS 和 AMD,ES6 範例出台以後,以一種更簡樸的情勢制訂了 JS 的模塊範例 (ES Module),並融會了 CommonJS 和 AMD 的長處。
大抵的生長進程:
CommonJS(服務端) => AMD (瀏覽器端) => CMD / UMD => ES Module
CommonJS 範例
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())
AMD 範例(Asynchromous Module Definition)
CommonJS 範例湧現后,在 Node 開闢中產生了異常優越的結果,開闢者願望自創這個履歷來處理瀏覽器端 JS 的模塊化。
但大部分人以為瀏覽器和服務器環境差異太大,畢竟瀏覽器端 JS 是經由進程收集動態順次加載的,而不是像服務端 JS 存在當地磁盤中。因而,瀏覽器須要完成的是異步模塊,模塊在定義的時刻就必須先指明它所須要依靠的模塊,然後把本模塊的代碼寫在回調函數中去實行,終究衍生出了 AMD 範例。
AMD 的主要頭腦是異步模塊,主邏輯在回調函數中實行,這和瀏覽器前端所習氣的開闢體式格局不約而同,RequireJS 應運而生。
範例內容:
- 用全局函數
define
來定義模塊,用法為:define(id?, dependencies?, factory)
; - id 為模塊標識,順從 CommonJS Module Identifiers 範例
- dependencies 為依靠的模塊數組,在 factory 中需傳入形介入之一一對應,假如 dependencies 省略不寫,則默以為
["require", "exports", "module"]
,factory 中也會默許傳入 require, exports, module,與 ComminJS 中的完成保持一致 - 假如 factory 為函數,模塊對外暴露 API 的要領有三種:return 恣意範例的數據、exports.xxx = xxx 或 module.exports = xxx
- 假如 factory 為對象,則該對象即為模塊的返回值
特性:
- 前置依靠,異步加載
- 便於治理模塊之間的依靠性,有利於代碼的編寫和保護。
它的用法看起來是如許的:
// 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);
});
CMD 範例(Common Module Definition)
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
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 等東西的模塊化語法糖轉碼產生了新的興緻,願望有時刻能夠去剖析一下模塊化的打包機制和轉譯代碼,然後整理出來加深一下本身對模塊化完成道理的熟悉和明白。
期待下一篇。
參考文章:
- ECMAScript 6 入門 — 阮一峰
- JS 模塊化進程