js 模塊化之 commonjs

在最初 js 被設(shè)計(jì)用來做一些表單校驗(yàn)的簡單功能赤赊,當(dāng)初的 js 只是用來作為頁面展示的一個(gè)補(bǔ)充笼吟。后來隨著 web 的發(fā)展丙号,相當(dāng)一部分業(yè)務(wù)邏輯前置到了前端進(jìn)行處理乏梁,js 的地位越來越重要芝囤,文件也越來越龐大似炎,為了將大的功能模塊進(jìn)行拆分成一個(gè)一個(gè)小的組成部分辛萍,但是拆分成小的 js 文件又帶來了新的挑戰(zhàn),由于 js 的加載和執(zhí)行順序在引入的時(shí)候就已經(jīng)決定了羡藐,這樣就需要充分考慮到各變量的作用范圍以及各變量之間的依賴關(guān)系贩毕。

就像上面這樣,a.js?會(huì)最先被執(zhí)行传睹,這樣如果在?b.js?中存在著與?a.js?同名的變量耳幢,就會(huì)發(fā)生覆蓋。同時(shí)如果在?c.js?中有使用到?a.js?聲明的變量就決定了?a.js必須在?c.js?上面被引入欧啤。這樣就存在這一種耦合睛藻,為了解決這一類問題 js 的模塊化應(yīng)運(yùn)而生。

commonjs

commonjs?隨著?nodejs?的誕生而面世邢隧,主要是用來解決服務(wù)端模塊化的問題店印,commonjs 對模塊做了如下規(guī)定

一個(gè) js 文件就是一個(gè)模塊,里面定義的變量都是私有的倒慧,對其他文件不可見

模塊內(nèi)的需要被導(dǎo)出的變量可以通過?exports?對象上的屬性進(jìn)行導(dǎo)出

使用?require?方法導(dǎo)入其他模塊的變量

所有的模塊加載都是同步的

同一個(gè)模塊可以被多次加載按摘,但是只有第一次被加載時(shí)會(huì)執(zhí)行模塊內(nèi)容,然后會(huì)緩存模塊

node 中的 commonjs 模塊

node?中一個(gè)文件就是一個(gè)模塊纫谅,各模塊之間的變量是無法互相訪問到的炫贤。

// a.jsconsta =1;

// b.jsconstb =2;console.log(a);// ReferenceError: a is not defined

在?b.js?中無法訪問到變量 a,如果需要使用 a 需要先導(dǎo)入模塊

// b.jsconsta =require('./a.js');console.log(a)// {}

這里還是無法訪問到 a 變量是因?yàn)槟K a 中沒有導(dǎo)出對應(yīng)的變量

// a.jsconsta =1exports.a= a// b.jsconsta =require('./a.js');console.log(a);// 1

node 模塊中的 module 對象

module?是?node?中的一個(gè)內(nèi)置對象付秕,是?Module?類的一個(gè)實(shí)例兰珍,?module?對象上有幾個(gè)重要屬性

module.id?模塊的標(biāo)識符。 通常是完全解析后的文件名

module.loaded?模塊是否已經(jīng)加載完成询吴,或正在加載中

exports.x=1;console.log(module.loaded)// false 還沒有加載完成setTimeout(() =>{console.log(module.loaded)// true},0)

module.exports?當(dāng)前模塊對外輸出的接口掠河,其他文件導(dǎo)入當(dāng)前模塊實(shí)際上就是在讀取當(dāng)前模塊的?module.exports?變量

除了?module.exports?之外,node?中還提供了一個(gè)內(nèi)置變量?exports猛计,它是?module.exports?的一個(gè)引用(可以理解成是一個(gè)快捷方式)唠摹,看一下?exports?和?module.exports?的關(guān)系

/**

* 實(shí)際的引用關(guān)系

* module.exports = {}

* exports = module.exports

*//**

* 一

* 這樣做的實(shí)際結(jié)果就是讓

* module.exports = {x: 1}

*/exports.x=1;// {x: 1}/**

* 二

* 同上

*/module.exports.x=1;// {x: 1}/**

* 三

* 雖然最終導(dǎo)出的內(nèi)容與上面兩種做法是

* 一樣的,但是這種做法改變了

* module.exports 的原始引用奉瘤,導(dǎo)

* 致了 exports 與 module.exports 的

* 聯(lián)系斷掉了勾拉,如果再使用 exports.y = 2

* 是沒有效果的

*/module.exports= {x:1};// {x: 1}exports.y=2;// 無效/**

* 四

* 與上面類似,改變了 exports 的引用

*/exports= {x:1};// 無效module.exports.y=2;// 2

node 模塊中的 require 方法

require?是?node?模塊中的內(nèi)置方法毛好,該方法用于導(dǎo)入模塊望艺,其函數(shù)簽名如下:

interfacerequire {/**

? * id? 模塊的名稱或路徑

? */(id:string):any}

require?方法上有幾個(gè)比較重要的屬性和方法

require.main?是?Module?的一個(gè)實(shí)例,表示當(dāng)前?node?進(jìn)程啟動(dòng)的入口模塊

require.resolve?是一個(gè)方法肌访,用來查詢指定的模塊的路徑找默,如果存在會(huì)返回模塊的路徑(如果是原生模塊,則只會(huì)返回原生模塊的名稱吼驶,例如?http)惩激,不存在則會(huì)報(bào)出錯(cuò)誤店煞,與?require?不同的是這個(gè)方法只會(huì)查找對應(yīng)的模塊路徑,不會(huì)執(zhí)行模塊中的代碼风钻,其函數(shù)簽名如下

interfaceRequireResolve{/**

? * request 指定要查找的模塊路徑

? * options.paths 從 paths 指定的路徑中進(jìn)行查找

? */(request: string,options: {paths: string[]}): string}

// /home/user/a.jsconsole.log(require.resolve('.b'));// /home/user/b.jsconsole.log(require.resolve('http'));// httpconsole.log(require.resolve('./index', {paths: ['/home/local/']}));// /home/local/index.js

require.resolve?方法與?require?解析文件路徑的方式是一樣的(后面會(huì)做介紹具體的解析過程)顷蟀,會(huì)優(yōu)先查看是否是原生模塊、然后會(huì)查看是否具有緩存骡技、然后才是更具不同的文件擴(kuò)展名進(jìn)行查找

require.cache?是一個(gè)對象鸣个,被引入的模塊將被緩存在這個(gè)對象中,可以手動(dòng)進(jìn)行刪除

require 本身的用法

require?可以通過傳入?string?類型的 id 作為入?yún)⒉茧琲d 可以是一個(gè)文件路徑或者是一個(gè)模塊名稱囤萤,路徑可以是一個(gè)相對路徑(以 ./ 或者 ../ 開頭)或者是一個(gè)絕對路徑(以 / 開頭)。相對路徑的方式比較簡單是趴,會(huì)以當(dāng)前文件的?__dirname?作為基礎(chǔ)路徑計(jì)算出絕對路徑涛舍,無論是相對路徑還是絕對路徑都可以是文件或者文件夾。

i. 文件加載規(guī)則

LOAD_AS_FILE(X)LOAD_AS_FILE1.是否存在 X 文件唆途,是則優(yōu)先加載 X2.否則會(huì)加載 X.js3.否則會(huì)加載 X.json4.否則會(huì)加載 X.node

ii. 文件夾加載規(guī)則

LOAD_AS_DIRECTORY(X)LOAD_AS_DIRECTORY1.是否存在`X/package.json`富雅,是則繼續(xù)? ? a. `package.json` 是否有 `main` 字段,無則執(zhí)行 2肛搬,是則執(zhí)行 b

? ? b. 加載 `(X + main)` 文件没佑,規(guī)則: `LOAD_AS_FILE(X + main)` ,無則繼續(xù)執(zhí)行 c

? ? c. 加載 `(X + main)/index`温赔,規(guī)則: `LOAD_AS_FILE((X + main)/index)`图筹,無則拋出錯(cuò)誤

2. 否則會(huì)執(zhí)行去查找 `X/index`,規(guī)則: `LOAD_AS_FILE(X/index)`

iii. 模塊名稱加載規(guī)則

id 作為模塊名稱會(huì)遵守如下優(yōu)先級規(guī)則進(jìn)行模塊查找:

加載內(nèi)置模塊

加載當(dāng)前目錄下?node_modules?文件夾中的模塊

加載父級目錄下?node_modules?文件夾中的模塊让腹,一直到最頂層

模塊緩存

模塊在第一次被加載之后會(huì)緩存,多次調(diào)用同一個(gè)模塊只會(huì)讓模塊執(zhí)行一次扣溺。

// a.jsmodule.exports= {name:'張三'}// b.jsrequire('./a.js')// {name: '張三'}require('./a.js').age=18require('./a.js')// {name: '張三', age: 18}

最后一個(gè)?require('./a.js')?會(huì)輸出?{name: '張三', age: 18}?則說明?a.js?模塊只執(zhí)行了一次骇窍,返回的還是最早被緩存的對象。如果要強(qiáng)制重新執(zhí)行被引用的模塊代碼锥余,可以通過刪除緩存的方式

// a.jsmodule.exports= {name:'張三'}// b.jsrequire('./a.js')// {name: '張三'}require('./a.js').age=18require('./a.js')// {name: '張三', age: 18}deleterequire.cache[require.resolve('./a')]require('./a.js')// {name: '張三'}

上面的例子還能說明模塊的緩存是基于文件路徑進(jìn)行的腹纳,只要在被加載時(shí)路徑不一致同一個(gè)模塊也會(huì)執(zhí)行兩次

循環(huán)依賴

要說弄清楚這個(gè)問題需要先了解 node 中模塊加載機(jī)制,在?commonjs?模塊體系中?require?加載的是一個(gè)對象的副本驱犹,實(shí)際也就是?module.exports?所指向的變量嘲恍,所以除非是存在引用類型的變量否則模塊內(nèi)部的變化是影響不到外部的。舉個(gè)例子說明這個(gè):

// b.jsletcount =1letcountObj = {count:10}module.exports= {? count,? countObj,setCount(newVal) {? ? count = newVal? },setCountObj(newVal) {? ? countObj.count= newVal? }}// a.jsconstmoduleB =require('./b.js')console.log(moduleB.count)// 1moduleB.setCount(2)console.log(moduleB.count)// 1console.log(moduleB.countObj.count)// 10moduleB.setCountObj(20)console.log(moduleB.countObj.count)// 20

上面的例子說明了?require?的結(jié)果實(shí)際是?module.exports?的一個(gè)副本雄驹,按照這樣的思路循環(huán)加載的情況下佃牛,也就會(huì)讀取已經(jīng)存在?module.exports?上的屬性,如果還存在部分屬性未掛在到?module.exports?上則會(huì)讀取不到医舆。

// a.jsconsole.log('a 開始');exports.done=false;constb =require('./b.js');console.log('在 a 中俘侠,b.done = %j', b.done);exports.done=true;console.log('a 結(jié)束');// b.jsconsole.log('b 開始');exports.done=false;consta =require('./a.js');console.log('在 b 中象缀,a.done = %j', a.done);exports.done=true;console.log('b 結(jié)束');// main.jsconsole.log('main 開始');consta =require('./a.js');constb =require('./b.js');console.log('在 main 中,a.done=%j爷速,b.done=%j', a.done, b.done);

當(dāng)?main.js?加載?a.js?時(shí)央星,?a.js?又加載?b.js。 此時(shí)惫东,?b.js?會(huì)嘗試去加載?a.js莉给。 為了防止無限的循環(huán),會(huì)返回一個(gè)?a.js?的?exports?對象的 未完成的副本 給?b.js?模塊廉沮。 然后?b.js?完成加載颓遏,并將?exports?對象提供給?a.js?模塊。

當(dāng) main.js 加載這兩個(gè)模塊時(shí)废封,它們都已經(jīng)完成加載州泊。 因此,該程序的輸出會(huì)是:

main 開始a 開始b 開始在 b 中漂洋,a.done =falseb 結(jié)束在 a 中遥皂,b.done =truea 結(jié)束在 main 中,a.done=true刽漂,b.done=true

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末演训,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子贝咙,更是在濱河造成了極大的恐慌样悟,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件庭猩,死亡現(xiàn)場離奇詭異窟她,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)蔼水,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進(jìn)店門震糖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人趴腋,你說我怎么就攤上這事吊说。” “怎么了优炬?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵颁井,是天一觀的道長谢揪。 經(jīng)常有香客問我绣张,道長,這世上最難降的妖魔是什么逢享? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任糊余,我火速辦了婚禮秀又,結(jié)果婚禮上单寂,老公的妹妹穿的比我還像新娘。我一直安慰自己吐辙,他們只是感情好宣决,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著昏苏,像睡著了一般尊沸。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上贤惯,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天洼专,我揣著相機(jī)與錄音,去河邊找鬼孵构。 笑死屁商,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的颈墅。 我是一名探鬼主播蜡镶,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼恤筛!你這毒婦竟也來了官还?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤毒坛,失蹤者是張志新(化名)和其女友劉穎望伦,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體煎殷,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡屯伞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了豪直。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片愕掏。...
    茶點(diǎn)故事閱讀 39,795評論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖顶伞,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情剑梳,我是刑警寧澤唆貌,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站垢乙,受9級特大地震影響锨咙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜追逮,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一酪刀、第九天 我趴在偏房一處隱蔽的房頂上張望粹舵。 院中可真熱鬧,春花似錦骂倘、人聲如沸眼滤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽诅需。三九已至,卻和暖如春荧库,著一層夾襖步出監(jiān)牢的瞬間堰塌,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工分衫, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留场刑,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓蚪战,卻偏偏與公主長得像牵现,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子屎勘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內(nèi)容