詳解Js中的模塊化是如何實(shí)現(xiàn)的
由于 Js 起初定位的原因(剛開(kāi)始沒(méi)想到會(huì)應(yīng)用在過(guò)于復(fù)雜的場(chǎng)景),所以它本身并沒(méi)有提供模塊系統(tǒng),隨著應(yīng)用的復(fù)雜化,模塊化成為了一個(gè)必須解決的問(wèn)題。本著菲麥深入原理的原則,很有必要來(lái)揭開(kāi)模塊化的面紗
一、模塊化需要解決的問(wèn)題
要對(duì)一個(gè)東西進(jìn)行深入的剖析,有必要帶著目的去看。模塊化所要解決的問(wèn)題可以用一句話概括
在沒(méi)有全局污染的情況下,更好的組織項(xiàng)目代碼
舉一個(gè)簡(jiǎn)單的栗子,我們現(xiàn)在有如下的代碼:
function doSomething () { const a = 10; const b = 11; const add = function (a + b) { return a + b } add (a + b) }
在現(xiàn)實(shí)的應(yīng)用場(chǎng)景中,doSomething 可能需要做很多很多的事情,add 函數(shù)可能也更為復(fù)雜,并且可以復(fù)用,那么我們希望可以將 add 函數(shù)獨(dú)立到一個(gè)單獨(dú)的文件中,于是:
// doSomething.js 文件 const add = require('add.js'); const a = 10; const b = 11; add(a+ b);
// add.js 文件 function add (a, b) { return a + b; } module.exports = add;
這樣做的目的顯而易見(jiàn),更好的組織項(xiàng)目代碼,注意到兩個(gè)文件中的 require 和 module.exports,從現(xiàn)在的上帝視角來(lái)看,這出自 CommonJS 規(guī)范(后文會(huì)有一個(gè)章節(jié)來(lái)專門講規(guī)范)中的關(guān)鍵字,分別代表導(dǎo)入和導(dǎo)出,拋開(kāi)規(guī)范而言,這其實(shí)是我們模塊化之路上需要解決的問(wèn)題。另外,雖然 add 模塊需要得到復(fù)用,但是我們并不希望在引入 add 的時(shí)候造成全局污染
二、引入的模塊如何運(yùn)行
在上述的例子中,我們已經(jīng)將代碼拆分到了兩個(gè)模塊文件當(dāng)中,在不造成全局污染的情況下,如何實(shí)現(xiàn) require,才能使得例子中的代碼做到正常運(yùn)行呢?
先不考慮模塊文件代碼的載入過(guò)程,假設(shè) require 已經(jīng)可以從模塊文件中讀取到代碼字符串,那么 require 可以這樣實(shí)現(xiàn)
function require (path) { // lode 方法讀取 path 對(duì)應(yīng)的文件模塊的代碼字符串 // let code = load(path); // 不考慮 load 的過(guò)程,直接獲得模塊 add 代碼字符串 let code = 'function add(a, b) {return a+b}; module.exports = add'; // 封裝成閉包 code = `(function(module) {$[code]})(context)` // 相當(dāng)于 exports,用于導(dǎo)出對(duì)象 let context = {}; // 運(yùn)行代碼,使得結(jié)果影響到 context const run = new Function('context', code); run(context, code); //返回導(dǎo)出的結(jié)果 return context.exports; }
這有幾個(gè)要點(diǎn):
1) 為了不造成全局污染,需要將代碼字符串封裝成閉包的形式,并且導(dǎo)出關(guān)鍵字 module.exports ,module 是與外界聯(lián)系的唯一載體,需要作為閉包匿名函數(shù)的入?yún)?,與引用方傳入的上下文 context 進(jìn)行關(guān)聯(lián)
2) 使用 new Function 來(lái)執(zhí)行代碼字符串,估計(jì)大部分同學(xué)對(duì) new Function 是不熟悉的,因?yàn)橐话闱闆r下定義一個(gè)函數(shù)無(wú)需如此,要知道,用 Function 類可以直接創(chuàng)建函數(shù),語(yǔ)法如下:
var function_name = new function(arg1, arg2, ..., argN, function_body)
在上面的形式中,每個(gè) arg 都是一個(gè)參數(shù),最后一個(gè)參數(shù)是函數(shù)主體(要執(zhí)行的代碼)。這些參數(shù)必須是字符串。也就是說(shuō),可以使用它來(lái)執(zhí)行字符串代碼,類似于 eval,并且相比 eval, 還可以通過(guò)參數(shù)的形式傳入字符串代碼中的某些變量的值
3)如果曾經(jīng)你有疑惑過(guò)為什么規(guī)范的導(dǎo)出關(guān)鍵字只有 exports 而我們實(shí)際使用過(guò)程中卻要使用module.exports(寫過(guò) Node 代碼的應(yīng)該不會(huì)陌生),那在這段代碼中就可以找到答案了,如果只用 exports 來(lái)接收 context,那么對(duì) exports 的重新賦值對(duì) context 不會(huì)有任何影響(參數(shù)的地址傳遞),不信將代碼改成如下形式再跑一跑:
演示結(jié)果
三、代碼載入方式
解決了代碼的運(yùn)行問(wèn)題,還需要解決模塊文件代碼的載入問(wèn)題,根據(jù)上述實(shí)例,我們的目標(biāo)是將模塊文件代碼以字符串的形式載入
在 Node 容器,所有的模塊文件都在本地,只需要從本地磁盤讀取模塊文件載入字符串代碼,再走上述的流程就可以了。事實(shí)證明,Node 非內(nèi)建、核心、c++ 模塊的載入執(zhí)行方式大體如此(雖然使用的不是 new Function,但也是一個(gè)類似的方法)
在 RN/Weex 容器,要載入一個(gè)遠(yuǎn)程 bundle.js,可以通過(guò) Native 的能力請(qǐng)求一個(gè)遠(yuǎn)程的 js 文件,再讀取成字符串代碼載入即可(按照這個(gè)邏輯,Node 讀取一個(gè)遠(yuǎn)程的 js 模塊好像也無(wú)不可,雖然大多數(shù)情況下我們不需要這么做)
在瀏覽器環(huán)境,所有的 Js 模塊都需要遠(yuǎn)程讀取,尷尬的是,受限于瀏覽器提供的能力,并不能通過(guò) ajax 以文件流的形式將遠(yuǎn)程的 js 文件直接讀取為字符串代碼。前提條件無(wú)法達(dá)成,上述運(yùn)行策略便行不通,只能另辟蹊徑
這就是為什么有了 CommonJs 規(guī)范了,為什么還會(huì)出現(xiàn) AMD/CMD 規(guī)范的原因
那么瀏覽器上是怎么做的呢?在瀏覽器中通過(guò) Js 控制動(dòng)態(tài)的載入一個(gè)遠(yuǎn)程的 Js 模塊文件,需要?jiǎng)討B(tài)的插入一個(gè) <script> 節(jié)點(diǎn):
// 摘抄自 require.js 的一段代碼 var node = config.xhtml ? document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') : document.createElement('script'); node.type = config.scriptType || 'text/javascript'; node.charset = 'utf-8'; node.async = true; node.setAttribute('data-requirecontext', context.contextName); node.setAttribute('data-requiremodule', moduleName); node.addEventListener('load', context.onScriptLoad, false); node.addEventListener('error', context.onScriptError, false);
要知道,設(shè)置了 <script> 標(biāo)簽的 src 之后,代碼一旦下載完成,就會(huì)立即執(zhí)行,根本由不得你再封裝成閉包,所以文件模塊需要在定義之初就要做文章,這就是我們說(shuō)熟知的 AMD/CMD 規(guī)范中的 define,開(kāi)篇的 add.js 需要重新改寫一下
// add.js 文件 define ('add',function () { function add (a, b) { return a + b; } return add; })
而對(duì)于 define 的實(shí)現(xiàn),最重要的就是將 callback 的執(zhí)行結(jié)果注冊(cè)到 context 的一個(gè)模塊數(shù)組中:
context.modules = {} function define(name, callback) { context.modules[name] = callback && callback() }
于是 require 就可以從 context.modules 中根據(jù)模塊名載入模塊了,是不是有了一種自己去寫一個(gè) “requirejs” 的沖動(dòng)感
具體的 AMD 實(shí)現(xiàn)當(dāng)然還會(huì)復(fù)雜很多,還需要控制模塊載入時(shí)序、模塊依賴等等,但是了解了這其中的靈魂,想必去精讀 require.js 的源碼也不是一件困難的事情
四、Webpack 中的模塊化
Webpack 也可以配置異步模塊,當(dāng)配置為異步模塊的時(shí)候,在瀏覽器環(huán)境同樣的是基于動(dòng)態(tài)插入 <script> 的方式載入遠(yuǎn)程模塊。在大多數(shù)情況下,模塊的載入方式都是類似于 Node 的本地磁盤同步載入的方式
嫑忘記,Webpack 除了有模塊化的能力,還是一個(gè)在輔助完善開(kāi)發(fā)工作流的工具,也就是說(shuō),Webpack 的模塊化是在開(kāi)發(fā)階段的完成的,使用 Webpack 構(gòu)筑的工作環(huán)境,在開(kāi)發(fā)階段雖然是獨(dú)立的模塊文件,但是在運(yùn)行時(shí),卻是一個(gè)合并好的文件
所以 Webpack 是一種在非運(yùn)行時(shí)的模塊化方案(基于 CommonJs),只有在配置了異步模塊的時(shí)候?qū)Ξ惒侥K的加載才是運(yùn)行時(shí)的(基于 AMD)
五、模塊化規(guī)范
通用的問(wèn)題在解決的過(guò)程中總會(huì)形成規(guī)范,上文已經(jīng)多次提到 CommonJs、AMD、CMD,有必要花點(diǎn)篇幅來(lái)講一講規(guī)范
Js 的模塊化規(guī)范的萌發(fā)于將 Js 擴(kuò)展到后端的想法,要使得 Js 具備類似于 Python、Ruby 和 Java 那樣具備開(kāi)發(fā)大型應(yīng)用的基礎(chǔ)能力,模塊化規(guī)范是必不可少的。CommonJS 規(guī)范的提出,為Js 制定了一個(gè)美好愿景,希望 Js 能在任何地方運(yùn)行,包括但不限于:
- 服務(wù)器端 Js 應(yīng)用
- 命令行工具
- 桌面應(yīng)用
- 混合應(yīng)用
CommonJS 對(duì)模塊的定義并不復(fù)雜,主要分為模塊引用、模塊定義和模塊標(biāo)識(shí)
- 模塊引用:使用 require 方法來(lái)引入一個(gè)模塊
- 模塊定義:使用 exports 導(dǎo)出模塊對(duì)象
- 模塊標(biāo)識(shí):給 require 方法傳入的參數(shù),小駝峰命名的字符串、相對(duì)路徑或者絕對(duì)路徑
模塊示意
CommonJs 規(guī)范在 Node 中大放異彩并且相互促進(jìn),但是在瀏覽器端,鑒于網(wǎng)絡(luò)的原因,同步的方式加載模塊顯然不太實(shí)用,在經(jīng)過(guò)一段爭(zhēng)執(zhí)之后,AMD 規(guī)范最終在前端場(chǎng)景中勝出(全稱 Asynchronous Module Definition,即“異步模塊定義”)
什么是 AMD,為什么需要 AMD ?在前述模塊化實(shí)現(xiàn)的推演過(guò)程中,你應(yīng)該能夠找到答案
除此之外還有國(guó)內(nèi)玉伯提出的 CMD 規(guī)范,AMD 和 CMD 的差異主要是,前者需要在定義之初聲明所有的依賴,后者可以在任意時(shí)機(jī)動(dòng)態(tài)引入模塊。CMD 更接近于 CommonJS
兩種規(guī)范都需要從遠(yuǎn)程網(wǎng)絡(luò)中載入模塊,不同之處在于,前者是預(yù)加載,后者是延遲加載
五、總結(jié)
如果有心,可以參照本文的推演,來(lái)實(shí)現(xiàn)一個(gè) “yourRequireJs”,沒(méi)有什么比重復(fù)造輪子更能讓知識(shí)沉淀~~
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- 淺談JS前端模塊化的幾種規(guī)范
- 詳解Js模塊化的作用原理和方案
- javascript中導(dǎo)出與導(dǎo)入實(shí)現(xiàn)模塊化管理教程
- 如何通過(guò)Proxy實(shí)現(xiàn)JSBridge模塊化封裝
- JavaScript 幾種循環(huán)方式以及模塊化的總結(jié)
- JavaScript 模塊化開(kāi)發(fā)實(shí)例詳解【seajs、requirejs庫(kù)使用】
- Javascript模塊化機(jī)制實(shí)現(xiàn)原理詳解
- javascript高級(jí)模塊化require.js的具體使用方法
- 如何理解JavaScript模塊化
相關(guān)文章
JS+CSS實(shí)現(xiàn)高亮關(guān)鍵詞(不侵入DOM)的方式
這篇文章主要為大家詳細(xì)介紹了JS+CSS實(shí)現(xiàn)高亮關(guān)鍵詞(不侵入DOM)的方式,文中的示例代碼講解詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-12-12解決layui動(dòng)態(tài)添加的元素click等事件觸發(fā)不了的問(wèn)題
今天小編就為大家分享一篇解決layui動(dòng)態(tài)添加的元素click等事件觸發(fā)不了的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-09-09javascript實(shí)現(xiàn)的固定位置懸浮窗口實(shí)例
這篇文章主要介紹了javascript實(shí)現(xiàn)的固定位置懸浮窗口,以一個(gè)完整實(shí)例形式詳細(xì)分析了javascript實(shí)現(xiàn)固定位置懸浮窗口的相關(guān)技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-04-04js在Firefox與IE中對(duì)DOM對(duì)像的引用的比較
直接用ID屬性進(jìn)行引用 直接用NAME屬性進(jìn)行引用 使用getElementById(),getElementsByName(),getElementsByTagName()進(jìn)行引用2009-06-06JS實(shí)現(xiàn)黑客帝國(guó)文字下落效果
看過(guò)黑客帝國(guó)的朋友或許都對(duì)開(kāi)頭的字幕效果很熟悉,自從影片播放以來(lái),網(wǎng)頁(yè)設(shè)計(jì)者有不少都在模仿這種文字下落的效果,而且還有文字漸變效果,對(duì)我們學(xué)習(xí)研究JS還是挺有幫助的哦,下面跟著小編一起學(xué)習(xí)JS 黑客帝國(guó)文字下落效果吧2015-09-09JS中如何實(shí)現(xiàn)Laravel的route函數(shù)詳解
這篇文章主要給大家介紹了JS中是如何實(shí)現(xiàn)Laravel的route函數(shù),文中通過(guò)示例代碼介紹的很詳細(xì),相信對(duì)大家具有一定的參考價(jià)值,有需要的朋友們下面來(lái)一起看看吧。2017-02-02JavaScript之a(chǎn)ppendChild、insertBefore和insertAfter使用說(shuō)明
這幾天需要用到對(duì)HTML節(jié)點(diǎn)元素的刪/插操作,由于用到insertBefore方法的時(shí)候遇到了一些麻煩,現(xiàn)在作為知識(shí)的整理,分別對(duì)appendChild、insertBefore和insertAfter做個(gè)總結(jié)2010-12-12.NET微信公眾號(hào)開(kāi)發(fā)之創(chuàng)建自定義菜單
這篇文章主要介紹了.NET微信公眾號(hào)開(kāi)發(fā)之創(chuàng)建自定義菜單的相關(guān)資料,需要的朋友可以參考下2015-07-07