摘要:的主要思想是異步模塊,主邏輯在回調函數中執行,這和瀏覽器前端所習慣的開發方式不謀而合,應運而生。是目前開發中使用率最高的模塊化標準。
原文鏈接: http://yanjiie.me
偶然的一個周末復習了一下 JS 的模塊標準,刷新了一下對 JS 模塊化的理解。
從開始 Coding 以來,總會周期性地突發奇想進行 Code Review。既是對一段時期的代碼進行總結,也是對那一段時光的懷念。
距離上一次 Review 已經過去近兩個月,這次竟然把兩年前在源續寫的代碼翻了出來,代碼雜亂無章的程度就像那時更加浮躁的自己,讓人感慨時光流逝之快。
話不多說,直接上碼。
當時在做的是一個境外電商項目(越南天寶商城),作為非 CS 的新手程序員,接觸 Coding 時間不長和工程化觀念不強,在當時的項目中出現了這樣的代碼:
import.js:
這段代碼看起來就是不斷地從 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 ModuleES 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 模塊化歷程
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/95773.html
摘要:在項目配置中的探索持續更新中后臺配置一項目需求請認真看目錄結構項目構建很大都基于目錄來的線上用戶訪問的目錄線上資源文件目錄前端開發目錄一個業務需求模塊每個業務需求模塊下會有很多頁面 WebPack在項目配置中的探索(持續更新中) webpack + gulp + vue (thinkPHP后臺配置) 一、項目需求(請認真看目錄結構,項目構建很大都基于目錄來的) --- Applicat...
摘要:模塊化編程,已經成為一個迫切的需求。隨著網站功能逐漸豐富,網頁中的也變得越來越復雜和臃腫,原有通過標簽來導入一個個的文件這種方式已經不能滿足現在互聯網開發模式,我們需要團隊協作模塊復用單元測試等等一系列復雜的需求。 隨著網站逐漸變成互聯網應用程序,嵌入網頁的Javascript代碼越來越龐大,越來越復雜。網頁越來越像桌面程序,需要一個團隊分工協作、進度管理、單元測試等等......開發...
閱讀 2940·2023-04-26 01:52
閱讀 3468·2021-09-04 16:40
閱讀 3629·2021-08-31 09:41
閱讀 1764·2021-08-09 13:41
閱讀 555·2019-08-30 15:54
閱讀 2959·2019-08-30 11:22
閱讀 1612·2019-08-30 10:52
閱讀 947·2019-08-29 13:24