摘要:所有依賴這個模塊的語句,都定義在一個回調函數中,等到所有依賴加載完成之后前置依賴,這個回調函數才會運行。如果將前面的代碼改寫成形式,就是下面這樣定義了一個文件,該文件依賴模塊,當模塊加載完畢之后執行回調函數,這里并沒有暴露任何變量。
模塊化是我們日常開發都要用到的基本技能,使用簡單且方便,但是很少人能說出來但是的原因及發展過程。現在通過對比不同時期的js的發展,將JavaScript模塊化串聯起來整理學習記憶。
如何理解模塊化 面臨的問題技術的誕生是為了解決某個問題,模塊化也是。在js模塊化誕生之前,開發者面臨很多問題:隨著前端的發展,web技術日趨成熟,js功能越來越多,代碼量也越來越大。之前一個項目通常各個頁面公用一個js,但是js逐漸拆分,項目中引入的js越來越多:
當年我剛剛實習的時候,項目中的js就是類似這樣,這樣的js引入造成了問題:
全局變量污染:各個文件的變量都是掛載到window對象上,污染全局變量。
變量重名:不同文件中的變量如果重名,后面的會覆蓋前面的,造成程序運行錯誤。
文件依賴順序:多個文件之間存在依賴關系,需要保證一定加載順序問題嚴重。
這些問題嚴重干擾開發,也是日常開發中經常遇到的問題。
什么是模塊化我覺得用樂高積木來比喻模塊化再好不過了。每個積木都是固定的顏色形狀,想要組合積木必須使用積木凸起和凹陷的部分進行連接,最后多個積木累積成你想要的形狀。
模塊化其實是一種規范,一種約束,這種約束會大大提升開發效率。將每個js文件看作是一個模塊,每個模塊通過固定的方式引入,并且通過固定的方式向外暴露指定的內容。
按照js模塊化的設想,一個個模塊按照其依賴關系組合,最終插入到主程序中。
模塊化解決方案模塊化這種規范提出之后,得到社區和廣大開發者的響應,不同時間點有多種實現方式。我們舉個例子:a.js
// a.js var aStr = "aa"; var aNum = cNum + 1;
b.js
// b.js var bStr = aStr + " bb";
c.js
// c.js var cNum = 0;
index.js
// index.js console.log(aNum, bStr);
四份文件,不同的依賴關系(a依賴c,b依賴a,index依賴a b)在沒有模塊化的時候我們需要頁面中這樣:
嚴格保證加載順序,否則報錯。
1. 閉包與命名空間這是最容易想到的也是最簡便的解決方式,早在模塊化概念提出之前很多人就已經使用閉包的方式來解決變量重名和污染問題。
這樣每個js文件都是使用IIFE包裹的,各個js文件分別在不同的詞法作用域中,相互隔離,最后通過閉包的方式暴露變量。每個閉包都是多帶帶一個文件,每個文件仍然通過script標簽的方式下載,標簽的順序就是模塊的依賴關系。
上面的例子我們用該方法修改下寫法:
a.js
// a.js var a = (function(cNum){ var aStr = "aa"; var aNum = cNum + 1; return { aStr: aStr, aNum: aNum }; })(cNum);
b.js
// b.js var bStr = (function(a){ var bStr = a.aStr + " bb"; return bStr; })(a);
c.js
// c.js var cNum = (function(){ var cNum = 0; return cNum; })();
index.js
;(function(a, bStr){ console.log(a.aNum, bStr); })(a, bStr)
這種方法下仍然需要在入口處嚴格保證加載順序:
這種方式最簡單有效,也是后續其他解決方案的基礎。這樣做的意義:
各個js文件之間避免了變量重名干擾,并且最少的暴露變量,避免全局污染。
模塊外部不能輕易的修改閉包內部的變量,程序的穩定性增加。
模塊與外部的連接通過IIFE傳參,語義化更好,清晰地知道有哪些依賴。
不過各個模塊的依賴關系仍然要通過加裝script的順序來保證。
2. 面向對象開發一開始一些人在閉包的解決方案上做出了規范約束:每個js文件始終返回一個object,將內容作為object的屬性。
比如上面的例子中b.js
// b.js var b = (function(a){ var bStr = a.aStr + " bb"; return { bStr: bStr }; })(a);
及時返回的是個值,也要用object包裹。后來很多人開始使用面向對象的方式開發插件:
;(function($){ var LightBox = function(){ // ... }; LightBox.prototype = { // .... }; window["LightBox"] = LightBox; })($);
使用的時候:
var lightbox = new LightBox();
當年很多人都喜歡這樣開發插件,并且認為能寫出這種插件的水平至少不低。這種方法只是閉包方式的小改進,約束js文件返回必須是對象,對象其實就是一些個方法和屬性的集合。這樣的優點:
規范化輸出,更加統一的便于相互依賴和引用。
使用‘類’的方式開發,便于后面的依賴進行擴展。
本質上這種方法只是對閉包方法的規范約束,并沒有做什么根本改動。
3. YUI早期雅虎出品的一個工具,模塊化管理只是一部分,其還具有JS壓縮、混淆、請求合并(合并資源需要server端配合)等性能優化的工具,說其是現有JS模塊化的鼻祖一點都不過分。
// YUI - 編寫模塊 YUI.add("dom", function(Y) { Y.DOM = { ... } }) // YUI - 使用模塊 YUI().use("dom", function(Y) { Y.DOM.doSomeThing(); // use some methods DOM attach to Y }) // hello.js YUI.add("hello", function(Y){ Y.sayHello = function(msg){ Y.DOM.set(el, "innerHTML", "Hello!"); } },"3.0.0",{ requires:["dom"] }) // main.js YUI().use("hello", function(Y){ Y.sayHello("hey yui loader"); })
YUI的出現令人眼前一新,他提供了一種模塊管理方式:通過YUI全局對象去管理不同模塊,所有模塊都只是對象上的不同屬性,相當于是不同程序運行在操作系統上。YUI的核心實現就是閉包,不過好景不長,具有里程碑式意義的模塊化工具誕生了。
4. CommonJs2009年Nodejs發布,其中Commonjs是作為Node中模塊化規范以及原生模塊面世的。Node中提出的Commonjs規范具有以下特點:
原生Module對象,每個文件都是一個Module實例
文件內通過require對象引入指定模塊
所有文件加載均是同步完成
通過module關鍵字暴露內容
每個模塊加載一次之后就會被緩存
模塊編譯本質上是沙箱編譯
由于使用了Node的api,只能在服務端環境上運行
基本上Commonjs發布之后,就成了Node里面標準的模塊化管理工具。同時Node還推出了npm包管理工具,npm平臺上的包均滿足Commonjs規范,隨著Node與npm的發展,Commonjs影響力也越來越大,并且促進了后面模塊化工具的發展,具有里程碑意義的模塊化工具。之前的例子我們這樣改寫:
a.js
// a.js var c = require("./c"); module.exports = { aStr: "aa", aNum: c.cNum + 1 };
b.js
// b.js var a = require("./a"); exports.bStr = a.aStr + " bb";
c.js
// c.js exports.cNum = 0;
入口文件就是 index.js
var a = require("./a"); var b = require("./b"); console.log(a.aNum, b.bStr);
可以直觀的看到,使用Commonjs管理模塊,十分方便。Commonjs優點在于:
強大的查找模塊功能,開發十分方便
標準化的輸入輸出,非常統一
每個文件引入自己的依賴,最終形成文件依賴樹
模塊緩存機制,提高編譯效率
利用node實現文件同步讀取
依靠注入變量的沙箱編譯實現模塊化
這里補充一點沙箱編譯:require進來的js模塊會被Module模塊注入一些變量,使用立即執行函數編譯,看起來就好像:
(function (exports, require, module, __filename, __dirname) { //原始文件內容 })();
看起來require和module好像是全局對象,其實只是閉包中的入參,并不是真正的全局對象。之前專門整理探究過 Node中的Module源碼分析,也可以看看阮一峰老師的require()源碼解讀,或者廖雪峰老師的CommonJS規范。
5. AMD和RequireJSCommonjs的誕生給js模塊化發展有了重要的啟發,Commonjs非常受歡迎,但是局限性很明顯:Commonjs基于Node原生api在服務端可以實現模塊同步加載,但是僅僅局限于服務端,客戶端如果同步加載依賴的話時間消耗非常大,所以需要一個在客戶端上基于Commonjs但是對于加載模塊做改進的方案,于是AMD規范誕生了。
AMD是"Asynchronous Module Definition"的縮寫,意思就是"異步模塊定義"。它采用異步方式加載模塊,模塊的加載不影響它后面語句的運行。所有依賴這個模塊的語句,都定義在一個回調函數中,等到所有依賴加載完成之后(前置依賴),這個回調函數才會運行。
AMD規范AMD與Commonjs一樣都是js模塊化規范,是一套抽象的約束,與2009年誕生。文檔這里。該約束規定采用require語句加載模塊,但是不同于CommonJS,它要求兩個參數:
require([module], callback);
第一個參數[module],是一個數組,里面的成員就是要加載的模塊;第二個參數callback,則是加載成功之后的回調函數。如果將前面的代碼改寫成AMD形式,就是下面這樣:
require(["math"], function (math) { math.add(2, 3); });
定義了一個文件,該文件依賴math模塊,當math模塊加載完畢之后執行回調函數,這里并沒有暴露任何變量。不同于Commonjs,在定義模塊的時候需要使用define函數定義:
define(id?, dependencies?, factory);
define方法與require類似,id是定義模塊的名字,仍然會在所有依賴加載完畢之后執行factory。
RequireJsRequireJs是js模塊化的工具框架,是AMD規范的具體實現。但是有意思的是,RequireJs誕生之后,推廣過程中產生的AMD規范。文檔這里。
RequireJs有兩個最鮮明的特點:
依賴前置:動態創建引入依賴,在標簽的onload事件監聽文件加載完畢;一個模塊的回調函數必須得等到所有依賴都加載完畢之后,才可執行,類似Promise.all。
配置文件:有一個main文件,配置不同模塊的路徑,以及shim不滿足AMD規范的js文件。
還是上面那個例子:
配置文件main.js
requirejs.config({ shim: { // ... }, paths: { a: "/a.js", b: "/b.js", c: "/c.js", index: "/index.js" } }); require(["index"], function(index){ index(); });
a.js
define("a", ["c"], function(c){ return { aStr: "aa", aNum: c.cNum + 1 } });
b.js
define("b", ["a"], function(a){ return { bStr = a.aStr + " bb"; } });
c.js
define("c", function(){ return { cNum: 0 } });
index.js
define("index", ["a", "b"], function(a, b){ return function(){ console.log(a.aNum, b.bStr); } });
頁面中嵌入
RequireJs當年在國內非常受歡迎,主要是以下優點:
動態并行加載js,依賴前置,無需再考慮js加載順序問題。
核心還是注入變量的沙箱編譯,解決模塊化問題。
規范化輸入輸出,使用起來方便。
對于不滿足AMD規范的文件可以很好地兼容。
不過個人覺得RequireJs配置還是挺麻煩的,但是當年已經非常方便了。
6. CMD和SeaJs CMD規范同樣是受到Commonjs的啟發,國內(阿里)誕生了一個CMD(Common Module Definition)規范。該規范借鑒了Commonjs的規范與AMD規范,在兩者基礎上做了改進。
define(id?, dependencies?, factory);
與AMD相比非常類似,CMD規范(2011)具有以下特點:
define定義模塊,require加載模塊,exports暴露變量。
不同于AMD的依賴前置,CMD推崇依賴就近(需要的時候再加載)
推崇api功能單一,一個模塊干一件事。
SeaJsSeaJs是CMD規范的實現,跟RequireJs類似,CMD也是SeaJs推廣過程中誕生的規范。CMD借鑒了很多AMD和Commonjs優點,同樣SeaJs也對AMD和Commonjs做出了很多兼容。
SeaJs核心特點:
需要配置模塊對應的url。
入口文件執行之后,根據文件內的依賴關系整理出依賴樹,然后通過插入標簽加載依賴。
依賴加載完畢之后,執行根factory。
在factory中遇到require,則去執行對應模塊的factory,實現就近依賴。
類似Commonjs,對所有模塊進行緩存(模塊的url就是id)。
類似Commonjs,可以使用相對路徑加載模塊。
可以向RequireJs一樣前置依賴,但是推崇就近依賴。
exports和return都可以暴露變量。
修改下上面那個例子:
a.js
console.log("a1"); define(function(require,exports,module){ console.log("inner a1"); require("./c.js") }); console.log("a2")
b.js
console.log("b1"); define(function(require,exports,module){ console.log("inner b1"); }); console.log("b2")
c.js
console.log("c1"); define(function(require,exports,module){ console.log("inner c1"); }); console.log("c2")
頁面引入
對于seaJs中的就近依賴,有必要多帶帶說一下。來看一下上面例子中的log順序:
seaJs執行入口文件,入口文件依賴a和b,a內部則依賴c。
依賴關系梳理完畢,開始動態script標簽下載依賴,控制臺輸出:
a1 a2 b1 b2 c1 c2
依賴加載之后,按照依賴順序開始解析模塊內部的define:inner a1
在a模塊中遇到了require("./c"),就近依賴這時候才去執行c模塊的factory:inner c1
然后解析b模塊:inner b1
全部依賴加載完畢,執行最后的factory:index
完整的順序就是:
a1 a2 b1 b2 c1 c2 inner a1 inner c1 inner b1 index
這是一個可以很好理解SeaJs的例子。
7. ES6中的模塊化之前的各種方法和框架,都出自于各個大公司或者社區,都是民間出臺的結局方法。到了2015年,ES6規范中,終于將模塊化納入JavaScript標準,從此js模塊化被官方扶正,也是未來js的標準。
之前那個例子再用ES6的方式實現一次:
a.js
import {cNum} from "./c"; export default { aStr: "aa", aNum: cNum + 1 };
b.js
import {aStr} from "./a"; export const bStr = aStr + " bb";
c.js
export const bNum = 0;
index.js
import {aNum} from "./a"; import {bStr} from "./b"; console.log(aNum, bStr);
可以看到,ES6中的模塊化在Commonjs的基礎上有所不同,增加了關鍵字import,export,default,as,from,而不是全局對象。另外深入理解的話,有兩點主要的區別:
CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。
CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口。
一個經典的例子:
// counter.js exports.count = 0 setTimeout(function () { console.log("increase count to", ++exports.count, "in counter.js after 500ms") }, 500) // commonjs.js const {count} = require("./counter") setTimeout(function () { console.log("read count after 1000ms in commonjs is", count) }, 1000) //es6.js import {count} from "./counter" setTimeout(function () { console.log("read count after 1000ms in es6 is", count) }, 1000)
分別運行 commonjs.js 和 es6.js:
? test node commonjs.js increase count to 1 in counter.js after 500ms read count after 1000ms in commonjs is 0 ? test babel-node es6.js increase count to 1 in counter.js after 500ms read count after 1000ms in es6 is 1
這個例子解釋了CommonJS 模塊輸出的是值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。ES6 模塊的運行機制與 CommonJS 不一樣。JS 引擎對腳本靜態分析的時候,遇到模塊加載命令import,就會生成一個只讀引用。等到腳本真正執行時,再根據這個只讀引用,到被加載的那個模塊里面去取值。換句話說,ES6 的import有點像 Unix 系統的“符號連接”,原始值變了,import加載的值也會跟著變。因此,ES6 模塊是動態引用,并且不會緩存值,模塊里面的變量綁定其所在的模塊。
更多ES6模塊化特點,參照阮一峰老師的ECMAScript 6 入門。
總結思考寫了這么多,其實都是蜻蜓點水地從使用方式和運行原理分析了不同方法的實現。現在重新看一下當時模塊化的痛點:
全局變量污染:各個文件的變量都是掛載到window對象上,污染全局變量。
變量重名:不同文件中的變量如果重名,后面的會覆蓋前面的,造成程序運行錯誤。
文件依賴順序:多個文件之間存在依賴關系,需要保證一定加載順序問題嚴重。
不同的模塊化手段都在致力于解決這些問題。前兩個問題其實很好解決,使用閉包配合立即執行函數,高級一點使用沙箱編譯,緩存輸出等等。
我覺得真正的難點在于依賴關系梳理以及加載。Commonjs在服務端使用fs可以接近同步的讀取文件,但是在瀏覽器中,不管是RequireJs還是SeaJs,都是使用動態創建script標簽方式加載,依賴全部加載完畢之后執行,省去了開發手動書寫加載順序這一煩惱。
到了ES6,官方出臺設定標準,不在需要出框架或者hack的方式解決該問題,該項已經作為標準要求各瀏覽器實現,雖然現在瀏覽器全部實現該標準尚無時日,但是肯定是未來趨勢。
參考JavaScript模塊化開發的演進歷程
精讀 js 模塊化發展
淺談模塊化開發
深入理解 ES6 模塊機制
Module 的加載實現
SeaJS 從入門到原理
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/95486.html
摘要:前兩天有朋友拿了這樣一段代碼來問我,我想把一段代碼寫成模塊化的樣子,你幫我看看是不是這樣的。的一個好處在與依賴前置,所有被使用到的模塊都會被提前加載好,從而加快運行速度。 前兩天有朋友拿了這樣一段代碼來問我,我想把一段代碼寫成模塊化的樣子,你幫我看看是不是這樣的。,代碼大概是這樣的: (function(global) { var myModules = { n...
摘要:我是這一期的主持人黃子毅本期精讀的文章是。模塊化需要保證全局變量盡量干凈,目前為止的模塊化方案都沒有很好的做到這一點。精讀本次提出獨到觀點的同學有流形,黃子毅,蘇里約,,楊森,淡蒼,留影,精讀由此歸納。 這次是前端精讀期刊與大家第一次正式碰面,我們每周會精讀并分析若干篇精品好文,試圖討論出結論性觀點。沒錯,我們試圖通過觀點的碰撞,爭做無主觀精品好文的意見領袖。 我是這一期的主持人 ——...
摘要:但目前來看,單頁應用在技術實現和體驗上還有更大的發展空間,而這就是所要推進的。模塊化頁面模塊化單頁應用的特點之一是將頁面劃分為多個模塊,跳轉時更新模塊的內容。與其他單頁庫相比,它們的職責更清晰,也易于理解。 showImg(https://segmentfault.com/img/bV2wO3?w=792&h=303);單頁Web應用作為新一代Web模式,為用戶提供了更流暢的體驗滿足感...
摘要:但目前來看,單頁應用在技術實現和體驗上還有更大的發展空間,而這就是所要推進的。模塊化頁面模塊化單頁應用的特點之一是將頁面劃分為多個模塊,跳轉時更新模塊的內容。與其他單頁庫相比,它們的職責更清晰,也易于理解。 showImg(https://segmentfault.com/img/bV2wO3?w=792&h=303);單頁Web應用作為新一代Web模式,為用戶提供了更流暢的體驗滿足感...
閱讀 1025·2021-11-23 10:11
閱讀 3864·2021-11-16 11:50
閱讀 931·2021-10-14 09:43
閱讀 2717·2021-10-14 09:42
閱讀 2716·2021-09-22 16:02
閱讀 1061·2019-08-29 10:57
閱讀 3383·2019-08-29 10:57
閱讀 2274·2019-08-26 13:52