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