javascript
关于 JS 模块化的最佳实践总结
?
模塊化開發是 JS 項目開發中的必備技能,它如同面向對象、設計模式一樣,可以兼顧提升軟件項目的可維護性和開發效率。
?
模塊之間通常以全局對象維系通訊。在小游戲中,GameGlobal 是全局對象。在小程序中,App 是全局對象,任何頁面都可以使用 getApp() 獲取這個全局對象。在 NodeJS 中,global 是全局對象。在傳統瀏覽器宿主中,window 是全局對象。
?
以下是作者總結的模塊化實踐經驗。簡言之,除了在瀏覽器項目中使用 sea.js,其它類型項目均建議直接使用原生的 ES6 模塊規范。
?
目錄
?
CommonJS 規范
?
CommonJS 規范最早在 NodeJS 中實踐并被推廣開來。它使用 module.exports 輸出模塊,一個模塊寫在一個獨立的文件內,一個文件即是一個模塊。在另一個JS文件中,使用 require 導入模塊。各個模塊相互隔離,模塊之間的通訊,通過全局對象 global 完成。
?
值得特別注意的是,CommonJS 這種規范天生是為 NodeJS 服務的。NodeJS 是一種服務器端編程語言,源碼文件都在硬盤上,讀起來很方便。CommonJS 規范作為一種同步方案,后續代碼必須等待前面的require指令加載模塊完成。
?
使用 CommonJS 規范的代碼示例如下:
?
// 定義模塊math.js var basicNum = 0; function add(a, b) {return a + b; } module.exports = { //在這里寫上需要向外暴露的函數、變量add: add,basicNum: basicNum } // 在另一個文件中,引用自定義的模塊時,參數包含路徑,可省略后綴.js var math = require('./math'); math.add(2, 5);?
在小程序與小游戲的官方文檔中,提到模塊化時官方建議的規范即是 CommonJS 規范。但其實在作者看來,更適合小游戲/小程序開發的規范是 ES6 模塊規范,原因稍后便會講到。
?
AMD 規范
?
CommonJS 規范主要是為服務器端的 NodeJS 服務,服務器端加載模塊文件無延時,但是在瀏覽器上就大不相同了。AMD 即是為了在瀏覽器宿主環境中實現模塊化方案的規范之一。
?
AMD是一種使用JS語言自實現的模塊化規范方案,主要由require.config()、define()、require 三個函數實現。require.config() 用于聲明基本路徑和模塊名稱;define() 用于定義模塊對象;require() 則用于加載模塊并使用。
?
與 CommonJS 規范不同,AMD 規范身處瀏覽器環境之中,是一種異步模塊加載規范。在使用時,首先要加載模塊化規范實現文件 require.js 及 JS 主文件,示例如下:
?
/** 網頁中引入require.js及main.js **/ <script src="js/require.js" data-main="js/main"></script>?
在上面的 Html 代碼中,"js/require.js" 是實現 AMD 規范的類庫文件,是任何使用 AMD 規范的網頁都需要加載的;"js/main" 是開發者的代碼主文件,在這個文件中加載并使用自定義模塊,示例代碼如下:
?
/** main.js 入口文件/主模塊 **/ // 首先用config()指定各模塊路徑和引用名 require.config({baseUrl: "js/lib",paths: {"jquery": "jquery.min", //實際路徑為js/lib/jquery.min.js"underscore": "underscore.min",} }); // 執行基本操作 require(["jquery","underscore","math"],function($,_,math){//在這里$代表jqurey、_代表underscorevar sum = math.add(10,20);$("#sum").html(sum); });?
而用于模塊的定義,在其它 JS 文件中是這樣聲明的:
?
// 定義math.js模塊 define(function () {var basicNum = 0;var add = function (x, y) {return x + y;};return {add: add,basicNum :basicNum}; });?
如果在一個模塊定義中依賴另一個模塊對象,可以這樣聲明:
?
// 定義一個依賴underscore模塊的模塊 define(['underscore'],function(_){var classify = function(list){_.countBy(list,function(num){return num > 30 ? 'old' : 'young';})};return {classify :classify}; })?
AMD 規范看起來完美解決了瀏覽器模塊化開發的難題。但是它有一個天生的缺陷,對于依賴的模塊無論實際需要與否,都會先加載并執行。如下所示:
?
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { // 等于在最前面聲明并初始化了要用到的所有模塊if (false) {// 即便沒用到某個模塊 b,但 b 還是提前執行了b.foo()} });?
在上面的代碼中,模塊 a、b、c、d、e、f 都會加載并執行,即使它們在實際的模塊代碼中沒有被用到。為了解決這個“浪費”的問題,CMD 規范應運而生。
?
CMD 規范
?
CMD 規范單從名字來看,它也與 AMD 規范很像。CMD 與 AMD 規范一樣,同樣是一種 JS 語言自實現的模塊化方案。不同之處在于,AMD 規范是依賴前置、模塊提前加載并執行;CMD 是依賴后置、模塊懶惰加載再執行。示例代碼如下:
?
/** CMD寫法 **/ define(function(require, exports, module) {var a = require('./a'),b = require('./b'),c = require('./c'); //在需要時申明、加載和使用a.doSomething();if (false) {var b = require('./b');b.doSomething();} });?
在上面的代碼中,模塊 a 在使用時才被聲明并加載。sea.js 是一個模塊加載器,是 AMD 規范的主要實現者之一。使用 sea.js 定義和使用模塊的示例如下所示:
?
/** sea.js **/ // 定義模塊 math.js define(function(require, exports, module) {var $ = require('jquery.js');var add = function(a,b){return a+b;}exports.add = add; }); // 加載模塊 seajs.use(['math.js'], function(math){var sum = math.add(1+2); });?
與 AMD 相比,CMD 貌似確實節省了無謂的模塊加載。但是 AMD 規范本身就是一種異步模塊加載方案,是只有在運行時才被加載并運行的,用則加載,不用不加載,有何浪費可言?況且,比起在代碼中分別以 require 函數加載模塊,直接在 define 方法的第一個參數中聲明,似乎還更簡潔與瀟灑些。
?
sea.js 作為 AMD 規范的升級版,簡化了使用方法,在使用上更加方便,值得推崇。但是 sea.js 便是瀏覽器開發中最佳的模塊化解決方案嗎?未必,還要看是什么類型的項目,后面會講到。
?
ES6 模塊規范
?
在講 ES6 模塊規范之前,我們先看一下規范前驅 CommonJS 的一個缺陷。如下所示:
?
// 模塊定義代碼:lib.js var counter = 3; function incCounter() {counter++; } module.exports = {counter: counter,incCounter: incCounter, }; // 模塊使用代碼:main.js var mod = require('./lib'); console.log(mod.counter); // 3 mod.incCounter(); console.log(mod.counter); // 3 var mod2 = require('./lib'); console.log(mod2.counter); // 3?
在上面的代碼中,為什么三個 mod.counter 的輸出均是3?
?
CommonJS 規范是一種動態加載、拷貝值對象執行的模塊規范。每個模塊在被使用時,都是在運行時被動態拉取并被拷貝使用的,模塊定義是唯一的,但有幾處引用便有幾處拷貝。所以,對于不同的 require 調用,生成的是不同的運行時對象。
?
即使如此,在上面的代碼中,mod 只有一個,為什么 mod.incCounter() 對這個模塊對象——即 mod 中的 counter 變量改變無效?相反,對于以下的代碼:
?
// lib.js var counter = 3; function incCounter() {counter++; } module.exports = {get counter() {return counter},incCounter: incCounter, }; // main.js var mod = require('./lib'); console.log(mod.counter); // 3 mod.incCounter(); console.log(mod.counter); // 4?
第二個輸出是4。只是將 counter 聲明為一個 getter 存取器屬性,調用便正常了,為什么?
?
這是由于 CommonJS 的拷貝機制造成的。由于 CommonJS 規范的拷貝運行機制,在 lib.js 中使用 module.exports 輸出的對象,是從 lib 模塊內拷貝而得,當時 counter 的值是幾,便拷貝了幾。無論執行 incCounter 多少次,改變的都不是輸出對象的 counter 變量。
?
而當定義了 getter 屬性之后,該屬性指向了模塊定義對象中的 counter 變量了嗎?不,是指向了被 incCounter 方法以閉包形式囊括的 counter 變量,這個變量是輸出的模塊對象的一部分。
?
CommonJS 規范的這個缺陷,有時候讓程序很無奈,一不小心就寫出了錯誤的代碼。這個缺陷在 ES6 中得到了很好的解決。
?
在 ES6 模塊規范中,只有 export 與 import 兩個關鍵字。示例如下:
?
/** 定義模塊 math.js **/ var basicNum = 0; var add = function (a, b) {return a + b; }; export { basicNum, add }; /** 引用模塊 **/ import { basicNum, add } from './math'; function test(ele) {ele.textContent = add(99 + basicNum); }?
在上面的代碼中,使用 export 關鍵字在 math.js 文件中輸出模塊,這里使用了對象字面量的屬性名稱簡寫與方法名稱簡寫。在另一個文件中引用模塊,在 import 關鍵字后面,{basicNum, add} 這是對象變量析構的寫法。
?
如果在 export 模塊時,使用了 default 限定詞,如下所示:
?
//定義輸出 export default { basicNum, add }; //引入 import math from './math'; function test(ele) {ele.textContent = math.add(99 + math.basicNum); }?
在 import 引入時,便可以省去花括號。這樣看起來代碼更清爽簡潔。
?
ES6 模塊規范與 CommonJS 規范相比,有以下不同:
?
(1)ES6 模塊規范是解析(是解析不是編譯)時靜態加載、運行時動態引用,所有引用出去的模塊對象均指向同一個模塊對象。在上面使用 CommonJS 規范聲明的 lib 模塊,如果使用 ES6 模塊規范聲明,根本不會出現 counter 變量含糊不清的問題。
?
(2)CommonJS 規范是運行時動態加載、拷貝值對象使用。每一個引用出去的模塊對象,都是一個獨立的對象。
?
結論
?
所以綜上所述,在模塊化方案上最佳選擇是什么?
?
在小程序(包括小游戲)開發項目中,由于支持 ES6,所以小程序最好的模塊化方案便是使用ES6模塊規范。雖然官方文檔中提到的模塊化規范是 CommonJS,但最佳方案作者認為卻應該是 ES6。
?
小程序在手機端(無論 iOS 還是 Android)的底層渲染內核都是類 Chrome v8 引擎。v8 引擎在執行JS代碼時,是將代碼先以 MacroAssembler 匯編庫在內存中先編譯成機器碼再送往 CPU 執行的,并不是像其它 JS 引擎那樣解析一行執行一行。所以,靜態加載的 ES6 模塊規范,更有助于 v8 引擎發揮價值。而運行時加載的 CommonJS 規范、AMD 規范、CMD 規范等,均不利于 v8 引擎施展拳腳。遇到 CommonJS 代碼,v8 可能會怒罵:“有什么話能不能一次講完,你這樣貓拉屎式的做法只能讓我更慢!”
?
在 NodeJS 開發項目中,Node9 已經支持 ES6 語法,完全可以使用 ES6 模塊規范。NodeJS 的誕生,本身就基于 Google 的 v8 引擎,沒有理由不考慮發揮 v8 的最大潛能。
?
在瀏覽器 JS 開發項目中,因為從服務器加載文件需要時間,使用 CommonJS 規范肯定是不合適了。至于是使用原生的 ES 模塊規范,還是使用sea.js,要看具體場景。如果想頁面盡快加載,sea.js 適合;如果是單頁面網站,適合使用原生的 ES6 模塊規范。還有一點,瀏覽器并非只有 Chrome 一家,對于沒有使用 v8 引擎的瀏覽器,使用 ES6 原生規范的優勢就又減少了一點。
?
2019年1月21日于北京
?
?
參考資料
- 瀏覽器已原生支持 ES 模塊,這對前端開發來說意味著什么?
- Node 9下import/export的絲般順滑使用
- Sea.js 是什么?
- 前端模塊化:CommonJS,AMD,CMD,ES6
- Module 的加載實現
?
本文首先于微信公眾號「藝述思維」:關于 JS 模塊化的最佳實踐總結
?
轉載于:https://www.cnblogs.com/sban/p/10301143.html
與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的关于 JS 模块化的最佳实践总结的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 实战dock安装和镜像的拉取
- 下一篇: gradle idea java ssm