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

因爲 Js 起初定位的緣由(剛開始沒想到會應用在過於複雜的場景),因此它自己並無提供模塊系統,隨着應用的複雜化,模塊化成爲了一個必須解決的問題。本着菲麥深刻原理的原則,頗有必要來揭開模塊化的面紗javascript

1、模塊化須要解決的問題

要對一個東西進行深刻的剖析,有必要帶着目的去看。模塊化所要解決的問題能夠用一句話歸納html

在沒有全局污染的狀況下,更好的組織項目代碼前端

舉一個簡單的栗子,咱們如今有以下的代碼:java

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

在現實的應用場景中,doSomething 可能須要作不少不少的事情,add 函數可能也更爲複雜,而且能夠複用,那麼咱們但願能夠將 add 函數獨立到一個單獨的文件中,因而:node

// 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,從如今的上帝視角來看,這出自 CommonJS 規範(後文會有一個章節來專門講規範)中的關鍵字,分別表明導入和導出,拋開規範而言,這實際上是咱們模塊化之路上須要解決的問題。另外,雖然 add 模塊須要獲得複用,可是咱們並不但願在引入 add 的時候形成全局污染c++

2、引入的模塊如何運行

在上述的例子中,咱們已經將代碼拆分到了兩個模塊文件當中,在不形成全局污染的狀況下,如何實現 require,才能使得例子中的代碼作到正常運行呢?ajax

先不考慮模塊文件代碼的載入過程,假設 require 已經能夠從模塊文件中讀取到代碼字符串,那麼 require 能夠這樣實現後端

function require (path) {
   // lode 方法讀取 path 對應的文件模塊的代碼字符串
   // let code = load(path);
   // 不考慮 load 的過程,直接得到模塊 add 代碼字符串
   let code = 'function add(a, b) {return a+b}; module.exports = add';
   // 封裝成閉包
   code = `(function(module) {${code}})(context)`
   // 至關於 exports,用於導出對象
   let context = {};
   // 運行代碼,使得結果影響到 context
   const run = new Function('context', code);
   run(context);
   //返回導出的結果
   return context.exports;
}複製代碼

這有幾個要點:
1) 爲了避免形成全局污染,須要將代碼字符串封裝成閉包的形式,而且導出關鍵字 module.exports ,module 是與外界聯繫的惟一載體,須要做爲閉包匿名函數的入參,與引用方傳入的上下文 context 進行關聯
2) 使用 new Function 來執行代碼字符串,估計大部分同窗對 new Function 是不熟悉的,由於通常狀況下定義一個函數無需如此,要知道,用 Function 類能夠直接建立函數,語法以下:數組

var function_name = new function(arg1, arg2, ..., argN, function_body)複製代碼

在上面的形式中,每一個 arg 都是一個參數,最後一個參數是函數主體(要執行的代碼)。這些參數必須是字符串。也就是說,可使用它來執行字符串代碼,相似於 eval,而且相比 eval, 還能夠經過參數的形式傳入字符串代碼中的某些變量的值
3)若是曾經你有疑惑過爲何規範的導出關鍵字只有 exports 而咱們實際使用過程當中卻要使用module.exports(寫過 Node 代碼的應該不會陌生),那在這段代碼中就能夠找到答案了,若是隻用 exports 來接收 context,那麼對 exports 的從新賦值對 context 不會有任何影響(參數的地址傳遞),不信將代碼改爲以下形式再跑一跑:瀏覽器

演示結果
演示結果

3、代碼載入方式

解決了代碼的運行問題,還須要解決模塊文件代碼的載入問題,根據上述實例,咱們的目標是將模塊文件代碼以字符串的形式載入

在 Node 容器,全部的模塊文件都在本地,只須要從本地磁盤讀取模塊文件載入字符串代碼,再走上述的流程就能夠了。事實證實,Node 非內建、核心、c++ 模塊的載入執行方式大致如此(雖然使用的不是 new Function,但也是一個相似的方法)

在 RN/Weex 容器,要載入一個遠程 bundle.js,能夠經過 Native 的能力請求一個遠程的 js 文件,再讀取成字符串代碼載入便可(按照這個邏輯,Node 讀取一個遠程的 js 模塊好像也無不可,雖然大多數狀況下咱們不須要這麼作)

在瀏覽器環境,全部的 Js 模塊都須要遠程讀取,尷尬的是,受限於瀏覽器提供的能力,並不能經過 ajax 以文件流的形式將遠程的 js 文件直接讀取爲字符串代碼。前提條件沒法達成,上述運行策略便行不通,只能另闢蹊徑

這就是爲何有了 CommonJs 規範了,爲何還會出現 AMD/CMD 規範的緣由

那麼瀏覽器上是怎麼作的呢?在瀏覽器中經過 Js 控制動態的載入一個遠程的 Js 模塊文件,須要動態的插入一個 <script> 節點:

// 摘抄自 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);複製代碼

要知道,設置了 <script> 標籤的 src 以後,代碼一旦下載完成,就會當即執行,根本由不得你再封裝成閉包,因此文件模塊須要在定義之初就要作文章,這就是咱們說熟知的 AMD/CMD 規範中的 define,開篇的 add.js 須要從新改寫一下

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

而對於 define 的實現,最重要的就是將 callback 的執行結果註冊到 context 的一個模塊數組中:

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

因而 require 就能夠從 context.modules 中根據模塊名載入模塊了,是否是有了一種本身去寫一個 「requirejs」 的衝動感

具體的 AMD 實現固然還會複雜不少,還須要控制模塊載入時序、模塊依賴等等,可是瞭解了這其中的靈魂,想必去精讀 require.js 的源碼也不是一件困難的事情

4、Webpack 中的模塊化

Webpack 也能夠配置異步模塊,當配置爲異步模塊的時候,在瀏覽器環境一樣的是基於動態插入 <script> 的方式載入遠程模塊。在大多數狀況下,模塊的載入方式都是相似於 Node 的本地磁盤同步載入的方式

嫑忘記,Webpack 除了有模塊化的能力,仍是一個在輔助完善開發工做流的工具,也就是說,Webpack 的模塊化是在開發階段的完成的,使用 Webpack 構築的工做環境,在開發階段雖然是獨立的模塊文件,可是在運行時,倒是一個合併好的文件

因此 Webpack 是一種在非運行時的模塊化方案(基於 CommonJs),只有在配置了異步模塊的時候對異步模塊的加載纔是運行時的(基於 AMD)

5、模塊化規範

通用的問題在解決的過程當中總會造成規範,上文已經屢次提到 CommonJs、AMD、CMD,有必要花點篇幅來說一講規範

Js 的模塊化規範的萌發於將 Js 擴展到後端的想法,要使得 Js 具有相似於 Python、Ruby 和 Java 那樣具有開發大型應用的基礎能力,模塊化規範是必不可少的。CommonJS 規範的提出,爲Js 制定了一個美好願景,但願 Js 能在任何地方運行,包括但不限於:

  • 服務器端 Js 應用
  • 命令行工具
  • 桌面應用
  • 混合應用

CommonJS 對模塊的定義並不複雜,主要分爲模塊引用、模塊定義和模塊標識

  1. 模塊引用:使用 require 方法來引入一個模塊
  2. 模塊定義:使用 exports 導出模塊對象
  3. 模塊標識:給 require 方法傳入的參數,小駝峯命名的字符串、相對路徑或者絕對路徑

模塊示意
模塊示意

CommonJs 規範在 Node 中大放異彩而且相互促進,可是在瀏覽器端,鑑於網絡的緣由,同步的方式加載模塊顯然不太實用,在通過一段爭執以後,AMD 規範最終在前端場景中勝出(全稱 Asynchronous Module Definition,即「異步模塊定義」)

什麼是 AMD,爲何須要 AMD ?在前述模塊化實現的推演過程當中,你應該可以找到答案

除此以外還有國內玉伯提出的 CMD 規範,AMD 和 CMD 的差別主要是,前者須要在定義之初聲明全部的依賴,後者能夠在任意時機動態引入模塊。CMD 更接近於 CommonJS

兩種規範都須要從遠程網絡中載入模塊,不一樣之處在於,前者是預加載,後者是延遲加載

5、總結

若是有心,能夠參照本文的推演,來實現一個 「yourRequireJs」,沒有什麼比重複造輪子更能讓知識沉澱~~

菲麥前端 是一個讓知識深刻原理的知識社羣,咱們有 知識星球、公衆號以及羣,歡迎加微勾搭:facemagic2014

相關文章
相關標籤/搜索