Js 中的模塊化是如何達成的

由于 Js 起初定位的原因(剛開始沒想到會應(yīng)用在過于復(fù)雜的場景),所以它本身并沒有提供模塊系統(tǒng)土童,隨著應(yīng)用的復(fù)雜化诗茎,模塊化成為了一個必須解決的問題。本著菲麥深入原理的原則献汗,很有必要來揭開模塊化的面紗

一敢订、模塊化需要解決的問題

要對一個東西進行深入的剖析,有必要帶著目的去看罢吃。模塊化所要解決的問題可以用一句話概括

在沒有全局污染的情況下楚午,更好的組織項目代碼

舉一個簡單的栗子,我們現(xiàn)在有如下的代碼:

function doSomething () {
  const a = 10;
  const b = 11;
  const add = function (a, b) {
    return a + b
  }
  add (a + b)
}

在現(xiàn)實的應(yīng)用場景中刃麸,doSomething 可能需要做很多很多的事情醒叁,add 函數(shù)可能也更為復(fù)雜,并且可以復(fù)用泊业,那么我們希望可以將 add 函數(shù)獨立到一個單獨的文件中把沼,于是:

// 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;

這樣做的目的顯而易見,更好的組織項目代碼吁伺,注意到兩個文件中的 requiremodule.exports饮睬,從現(xiàn)在的上帝視角來看,這出自 CommonJS 規(guī)范(后文會有一個章節(jié)來專門講規(guī)范)中的關(guān)鍵字篮奄,分別代表導(dǎo)入和導(dǎo)出捆愁,拋開規(guī)范而言割去,這其實是我們模塊化之路上需要解決的問題。另外昼丑,雖然 add 模塊需要得到復(fù)用呻逆,但是我們并不希望在引入 add 的時候造成全局污染

二、引入的模塊如何運行

在上述的例子中菩帝,我們已經(jīng)將代碼拆分到了兩個模塊文件當(dāng)中咖城,在不造成全局污染的情況下,如何實現(xiàn) require呼奢,才能使得例子中的代碼做到正常運行呢宜雀?

先不考慮模塊文件代碼的載入過程,假設(shè) require 已經(jīng)可以從模塊文件中讀取到代碼字符串握础,那么 require 可以這樣實現(xiàn)

function require (path) {
   // lode 方法讀取 path 對應(yīng)的文件模塊的代碼字符串
   // let code = load(path);
   // 不考慮 load 的過程辐董,直接獲得模塊 add 代碼字符串
   let code = 'function add(a, b) {return a+b}; module.exports = add';
   // 封裝成閉包
   code = `(function(module) {${code}})(context)`
   // 相當(dāng)于 exports,用于導(dǎo)出對象
   let context = {};
   // 運行代碼禀综,使得結(jié)果影響到 context
   const run = new Function('context', code);
   run(context);
   //返回導(dǎo)出的結(jié)果
   return context.exports;
}

這有幾個要點:
1) 為了不造成全局污染简烘,需要將代碼字符串封裝成閉包的形式,并且導(dǎo)出關(guān)鍵字 module.exports 菇存,module 是與外界聯(lián)系的唯一載體夸研,需要作為閉包匿名函數(shù)的入?yún)蠲郏c引用方傳入的上下文 context 進行關(guān)聯(lián)
2) 使用 new Function 來執(zhí)行代碼字符串依鸥,估計大部分同學(xué)對 new Function 是不熟悉的,因為一般情況下定義一個函數(shù)無需如此悼沈,要知道贱迟,用 Function 類可以直接創(chuàng)建函數(shù),語法如下:

var function_name = new function(arg1, arg2, ..., argN, function_body)

在上面的形式中絮供,每個 arg 都是一個參數(shù)衣吠,最后一個參數(shù)是函數(shù)主體(要執(zhí)行的代碼)。這些參數(shù)必須是字符串壤靶。也就是說缚俏,可以使用它來執(zhí)行字符串代碼,類似于 eval贮乳,并且相比 eval, 還可以通過參數(shù)的形式傳入字符串代碼中的某些變量的值
3)如果曾經(jīng)你有疑惑過為什么規(guī)范的導(dǎo)出關(guān)鍵字只有 exports 而我們實際使用過程中卻要使用module.exports(寫過 Node 代碼的應(yīng)該不會陌生)忧换,那在這段代碼中就可以找到答案了,如果只用 exports 來接收 context向拆,那么對 exports 的重新賦值對 context 不會有任何影響(參數(shù)的地址傳遞)亚茬,不信將代碼改成如下形式再跑一跑:

演示結(jié)果

三、代碼載入方式

解決了代碼的運行問題浓恳,還需要解決模塊文件代碼的載入問題刹缝,根據(jù)上述實例碗暗,我們的目標(biāo)是將模塊文件代碼以字符串的形式載入

在 Node 容器,所有的模塊文件都在本地梢夯,只需要從本地磁盤讀取模塊文件載入字符串代碼言疗,再走上述的流程就可以了。事實證明颂砸,Node 非內(nèi)建洲守、核心、c++ 模塊的載入執(zhí)行方式大體如此(雖然使用的不是 new Function沾凄,但也是一個類似的方法)

在 RN/Weex 容器梗醇,要載入一個遠(yuǎn)程 bundle.js,可以通過 Native 的能力請求一個遠(yuǎn)程的 js 文件撒蟀,再讀取成字符串代碼載入即可(按照這個邏輯叙谨,Node 讀取一個遠(yuǎn)程的 js 模塊好像也無不可,雖然大多數(shù)情況下我們不需要這么做)

在瀏覽器環(huán)境保屯,所有的 Js 模塊都需要遠(yuǎn)程讀取手负,尷尬的是,受限于瀏覽器提供的能力姑尺,并不能通過 ajax 以文件流的形式將遠(yuǎn)程的 js 文件直接讀取為字符串代碼竟终。前提條件無法達成,上述運行策略便行不通切蟋,只能另辟蹊徑

這就是為什么有了 CommonJs 規(guī)范了统捶,為什么還會出現(xiàn) AMD/CMD 規(guī)范的原因

那么瀏覽器上是怎么做的呢?在瀏覽器中通過 Js 控制動態(tài)的載入一個遠(yuǎn)程的 Js 模塊文件柄粹,需要動態(tài)的插入一個 <script> 節(jié)點:

// 摘抄自 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 之后,代碼一旦下載完成驻右,就會立即執(zhí)行什黑,根本由不得你再封裝成閉包,所以文件模塊需要在定義之初就要做文章堪夭,這就是我們說熟知的 AMD/CMD 規(guī)范中的 define愕把,開篇的 add.js 需要重新改寫一下

// add.js 文件
define ('add',function () {
    function add (a, b) {
      return a + b;
    }
    return add;
})

而對于 define 的實現(xiàn)森爽,最重要的就是將 callback 的執(zhí)行結(jié)果注冊到 context 的一個模塊數(shù)組中:

    context.modules = {}
    function define(name, callback) {
        context.modules[name] = callback && callback()
    }

于是 require 就可以從 context.modules 中根據(jù)模塊名載入模塊了恨豁,是不是有了一種自己去寫一個 “requirejs” 的沖動感

具體的 AMD 實現(xiàn)當(dāng)然還會復(fù)雜很多,還需要控制模塊載入時序拗秘、模塊依賴等等圣絮,但是了解了這其中的靈魂,想必去精讀 require.js 的源碼也不是一件困難的事情

四雕旨、Webpack 中的模塊化

Webpack 也可以配置異步模塊扮匠,當(dāng)配置為異步模塊的時候捧请,在瀏覽器環(huán)境同樣的是基于動態(tài)插入 <script> 的方式載入遠(yuǎn)程模塊。在大多數(shù)情況下棒搜,模塊的載入方式都是類似于 Node 的本地磁盤同步載入的方式

嫑忘記疹蛉,Webpack 除了有模塊化的能力,還是一個在輔助完善開發(fā)工作流的工具力麸,也就是說,Webpack 的模塊化是在開發(fā)階段的完成的克蚂,使用 Webpack 構(gòu)筑的工作環(huán)境闺鲸,在開發(fā)階段雖然是獨立的模塊文件,但是在運行時埃叭,卻是一個合并好的文件

所以 Webpack 是一種在非運行時的模塊化方案(基于 CommonJs)摸恍,只有在配置了異步模塊的時候?qū)Ξ惒侥K的加載才是運行時的(基于 AMD)

五、模塊化規(guī)范

通用的問題在解決的過程中總會形成規(guī)范赤屋,上文已經(jīng)多次提到 CommonJs立镶、AMD、CMD类早,有必要花點篇幅來講一講規(guī)范

Js 的模塊化規(guī)范的萌發(fā)于將 Js 擴展到后端的想法媚媒,要使得 Js 具備類似于 Python、Ruby 和 Java 那樣具備開發(fā)大型應(yīng)用的基礎(chǔ)能力涩僻,模塊化規(guī)范是必不可少的缭召。CommonJS 規(guī)范的提出,為Js 制定了一個美好愿景令哟,希望 Js 能在任何地方運行恼琼,包括但不限于:

  • 服務(wù)器端 Js 應(yīng)用
  • 命令行工具
  • 桌面應(yīng)用
  • 混合應(yīng)用

CommonJS 對模塊的定義并不復(fù)雜妨蛹,主要分為模塊引用屏富、模塊定義和模塊標(biāo)識

  1. 模塊引用:使用 require 方法來引入一個模塊
  2. 模塊定義:使用 exports 導(dǎo)出模塊對象
  3. 模塊標(biāo)識:給 require 方法傳入的參數(shù),小駝峰命名的字符串蛙卤、相對路徑或者絕對路徑
模塊示意

CommonJs 規(guī)范在 Node 中大放異彩并且相互促進狠半,但是在瀏覽器端,鑒于網(wǎng)絡(luò)的原因颤难,同步的方式加載模塊顯然不太實用神年,在經(jīng)過一段爭執(zhí)之后,AMD 規(guī)范最終在前端場景中勝出(全稱 Asynchronous Module Definition行嗤,即“異步模塊定義”)

什么是 AMD已日,為什么需要 AMD ?在前述模塊化實現(xiàn)的推演過程中栅屏,你應(yīng)該能夠找到答案

除此之外還有國內(nèi)玉伯提出的 CMD 規(guī)范飘千,AMD 和 CMD 的差異主要是堂鲜,前者需要在定義之初聲明所有的依賴,后者可以在任意時機動態(tài)引入模塊护奈。CMD 更接近于 CommonJS

兩種規(guī)范都需要從遠(yuǎn)程網(wǎng)絡(luò)中載入模塊缔莲,不同之處在于,前者是預(yù)加載霉旗,后者是延遲加載

五痴奏、總結(jié)

如果有心,可以參照本文的推演厌秒,來實現(xiàn)一個 “yourRequireJs”读拆,沒有什么比重復(fù)造輪子更能讓知識沉淀~~

菲麥前端 是一個讓知識深入原理的知識社群,我們有 知識星球鸵闪、公眾號以及群建椰,歡迎加微勾搭:facemagic2014

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市岛马,隨后出現(xiàn)的幾起案子棉姐,更是在濱河造成了極大的恐慌,老刑警劉巖啦逆,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件伞矩,死亡現(xiàn)場離奇詭異,居然都是意外死亡夏志,警方通過查閱死者的電腦和手機乃坤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來沟蔑,“玉大人湿诊,你說我怎么就攤上這事∈莶模” “怎么了厅须?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長食棕。 經(jīng)常有香客問我朗和,道長,這世上最難降的妖魔是什么簿晓? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任眶拉,我火速辦了婚禮,結(jié)果婚禮上憔儿,老公的妹妹穿的比我還像新娘忆植。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布朝刊。 她就那樣靜靜地躺著吴侦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪坞古。 梳的紋絲不亂的頭發(fā)上备韧,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機與錄音痪枫,去河邊找鬼织堂。 笑死,一個胖子當(dāng)著我的面吹牛奶陈,可吹牛的內(nèi)容都是我干的易阳。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼吃粒,長吁一口氣:“原來是場噩夢啊……” “哼潦俺!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起徐勃,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤事示,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后僻肖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肖爵,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年臀脏,在試婚紗的時候發(fā)現(xiàn)自己被綠了劝堪。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡揉稚,死狀恐怖秒啦,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情搀玖,我是刑警寧澤余境,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站巷怜,受9級特大地震影響葛超,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜延塑,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望答渔。 院中可真熱鬧关带,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至磨总,卻和暖如春嗦明,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蚪燕。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工娶牌, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人馆纳。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓诗良,卻偏偏與公主長得像,于是被迫代替她去往敵國和親鲁驶。 傳聞我的和親對象是個殘疾皇子鉴裹,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

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