[譯]JS模塊化簡史

原文: https://ponyfoo.com/articles/brief-history-of-modularityweb


對於 JS 來講,模塊化是個現代的概念。本文將快速回溯和總結模塊化是如何推進了 JS 世界進化的。咱們不會羅列各類全面的清單,而只是展現改變 JS 歷史的主要範例

Script 標籤和閉包

早年間,JS 還只是委身於 HTML <script> 標籤中的內聯代碼;頂不濟也就是被封裝到專門的腳本文件中調用,也仍是得與其餘腳本共享一個全局做用域。express

在這些文件或內聯標籤裏面定義的任何變量都被全局對象 window 收入囊中,由此可能帶來的全部不相關腳本中的互相污染,將致使衝突甚至破壞體驗;某個腳本中的變量可能會在無心之間被全局中或者其餘腳本中的變量覆蓋。npm

後來,隨着 web 應用開始變得愈來愈龐雜,做用域和全局做用域的危害等概念變得顯而易見而深刻人心。當即調用函數表達式 (IIFE: Immediately-invoking function expressions)被髮明出來併成爲中流砥柱。一個 IIFE 就是把整個或部分 JS 文件包裹進一個函數,並在對其求值後當即執行。由於 JS 中的每一個函數都會建立一個新一級的做用域,因此用 var 聲明的變量就被綁定在所處的 IIFE 中了。歸功於 IIFE,儘管做用域中也有變量提高等效果,但再也不會變成隱式聲明的全局變量了,這避免了定義變量時的脆弱性。編程

下面的代碼片斷展現了各類形式的 IIFE。除非用window.foo = 'bar' 這種形式定義一個全局上下文的變量,不然每一個 IIFE 中的代碼都是獨立的。數組

(function() {
  console.log('IIFE using parenthesis')
})()

~function() {
  console.log('IIFE using a bitwise operator')
}()

void function() {
  console.log('IIFE using the void operator')
}()複製代碼

經過使用 IIFE 模式,庫就能夠經過暴露一個綁定到 window 的變量並在以後對其重用的方式,來建立一個典型的模塊了,這避免了全局命名的空間污染。如下代碼片斷展現瞭如何用這些 IIFE 中的一種形式來建立一個包含 sum 方法的 mathlib 庫。若是想對 mathlib 庫增長更多模塊,就能夠把每一個模塊置於一個 IIFE 中,並將其暴露的方法添加到 mathlib 這個公開接口中;而其餘任何東西都留在了組件所定義在的私有函數做用域中了。promise

void function() {
  window.mathlib = window.mathlib || {}
  window.mathlib.sum = sum

  function sum(...values) {
    return values.reduce((a, b) => a + b, 0)
  }
}()

mathlib.sum(1, 2, 3)
// <- 6複製代碼

這種模式也在無心之中誘發了 JS 工具的一個萌芽 -- 開發者首次能安全的將全部 IIFE 模塊集合到一個文件中,這減輕了網絡負擔。瀏覽器

IIFE 這種實現方法的問題在於,並無一個明確的依賴樹。這意味着開發者不得不去特地維護組件的明確順序,以作到模塊的依賴必須先於其被加載,還得考慮遞歸的狀況。安全

RequireJS、AngularJS 以及依賴注入

隨着模塊系統 Requirejs 以及 AngularJS 中依賴注入機制的出現,這二者無疑都容許模塊明確命名其依賴了。
bash

接下來的例子展現了使用 Requirejs 的 define 函數定義 mathlib/sum.js ;define 是添加到全局做用域中的,而隨後其回調的返回值會成爲模塊的公開接口。
服務器

define(function() {
  return sum

  function sum(...values) {
    return values.reduce((a, b) => a + b, 0)
  }
})
複製代碼

能夠用一個 mathlib.js 文件來聚集庫中的全部函數。在咱們的用例中,暫時只有 mathlib/sum,但也能夠用一樣的方法列出更多依賴。將這些依賴的文件路徑列在 define 的第一個數組參數中,而且將其各自的公開接口做爲參數傳入 define 的第二個回調函數中,注意保持順序一致。

define(['mathlib/sum'], function(sum) {
  return { sum }
})
複製代碼

這樣就定義好了一個庫,而且能借由 require 函數調用了。注意下面片斷中是如何處理依賴鏈的。

require(['mathlib'], function(mathlib) {
  mathlib.sum(1, 2, 3)
  // <- 6
})
複製代碼

這就是 RequireJS 和其固有的依賴樹的優點 -- 無論應用包含成百仍是上千的模塊,都不須要當心翼翼的維護一個依賴清單。考慮到咱們明確列出了依賴在哪裏被須要,也就排除了爲每一個組件寫一份如何關聯其餘組件的長長的清單的必要性,同時也避免了所以出錯。但排除如此之大的複雜度還僅僅是個反作用,而非其主要好處。

在模塊層面描述依賴的明確性,使得組件如何關聯到應用中其餘部分變得顯而易見。這種明確性反過來又培育出更大程度的模塊化,這在之前是沒法作到的,由於難以跟蹤依賴鏈。

RequireJS並不是沒有問題。

首先,整個模式圍繞其異步加載模塊的能力運行,若是用在產品部署上,將影響性能。使用異步加載機制,可能會在代碼被執行前發起數百個網絡請求。亟需對產品構建使用不一樣的工具實現優化。(譯註:能夠手工打包或使用官方的 r.js 實現自動打包等避免這個問題)

其次,須要一個 RequireJS 函數、一個可能很冗長的依賴列表、一個可能有一樣冗長參數的回調;全部這些只爲實現「聲明一個有依賴的模塊」一件事,這使得其應用複雜化了,其 API 也顯得不是很直觀。

AngularJS 中的依賴注入(DI - dependency injection)系統有着許多一樣的問題。做爲當時一個優雅的解決方案,依靠巧妙的字符串解析以免依賴數組,使用函數參數名來處理依賴。可是這個機制和代碼壓縮工具不兼容,將致使參數被從新命名成單字符,從而破壞了依賴的注入。

在以後的 AngularJS v1 版本中,引入了一個 build task 來轉換以下的代碼:

module.factory('calculator', function(mathlib) {
  // …
})
複製代碼

會轉換爲下面這種格式的代碼,由於包含了明確的依賴列表,就能夠安全的使用壓縮工具了。

module.factory('calculator', ['mathlib', function(mathlib) {
  // …
}])
複製代碼

不用說,以後引入的這個不爲人知的構建工具,做爲一個額外的構建步驟有過分設計之嫌,和帶來的小小好處相比,不管如何都妨礙了該模式的使用。開發者幾乎都會選擇繼續使用熟悉的類 RequireJS 風格來硬編碼依賴數組。

Node.js 和 CommonJS 的降臨

在由 Node.js 催生的若干創新中,CommonJS 模塊系統算得上一個,也被簡稱爲 CJS。利用 Node.js 程序能夠訪問文件系統的優點,CommonJS 標準更加貼近傳統的模塊加載機制。在 CommonJS 中,每一個文件都是擁有本身的做用域和上下文的單獨模塊。使用一個異步的 require 函數來加載依賴項,而且能夠在該模塊生命週期中的任什麼時候候動態調用,就像下面這樣:

const mathlib = require('./mathlib')
複製代碼

和 RequireJS 以及 AngularJS 很像的是,CommonJS 中的依賴也是靠路徑名稱實現的。主要的區別在於,再也不須要樣板函數和依賴數組什麼的了,而是將模塊的接口指派到一個綁定的變量中,或是在任何地方由 JS 表達式使用。

與前面提到的二者不一樣的是,CommonJS 更加嚴格。在 RequireJS 和 AngularJS 中,每一個文件中能夠包含若干個動態定義的模塊,而 CommonJS 則限制了每一個文件只能一個模塊。同時,RequireJS 有多種聲明模塊的途徑,而 AngularJS 則有不一樣種類的 factories、services、providers 等等 -- 以及幕後和其依賴注入機制緊密耦合的框架自己。相比較而言,CommonJS 描述模塊的方式則是惟一的。JS 文件皆模塊,調用 require 就加載依賴,而且其接口就是指定給 module.exports 的東西。這帶來了良好的工具化,以及更好的代碼自省 -- 讓工具也能更容易的找出 CommonJS 模塊系統中的層次。

最後,Browserify 被髮明出來,用於在本爲 Node.js 服務器而生的 CommonJS 模塊和瀏覽器之間架起了橋樑。使用 browserify 命令行接口程序,並向其傳遞入口模塊的路徑,就能將不管多少個模塊打包成一個瀏覽器適用的單獨文件。而 CommonJS 的殺手級特性:npm 包註冊器,爲其接管模塊加載生態系統起到了決定性做用。

的確,npm 沒有限制爲只能有 CommonJS 的模塊,甚至也沒規定只能是 JS 包,但 CommonJS 的 JS 包一直並仍將是其主流。指尖輕點之間,數以千計(如今已經有50多萬並仍穩定增加)的包就在你的應用中可用了,加上能夠在系統的一大部分重用 Node.js 服務器端和每種客戶端 web 瀏覽器中代碼的能力,使其極大的保持了對其餘模塊系統的競爭優點。

ES六、import、Babel 和 Webpack

當 ES6 在 2015 年中標準化,加之在此好久以前就已經能夠用 Babel 將 ES6 轉換爲 ES5 了,一場新的革命旋即展開。ES6 規範包括了一個 JS 原生的模塊系統,通常被稱爲 ECMAScript Modules (ESM)。

ESM 深受 CJS 及其前輩的影響,提供了一個靜態聲明式 API,以及一個基於 promise 的動態可編程 API。以下所示:

import mathlib from './mathlib'
import('./mathlib').then(mathlib => {
  // …
})
複製代碼

在 ESM 中,和 CJS 同樣,每一個文件都是擁有本身的做用域和上下文的單獨模塊。EMS 相比 CJS 的一個主要的優點是:其具有並被鼓勵使用的靜態導入依賴的方式。靜態導入極大改善了模塊系統的自省能力,使其能夠被靜態化的分析;並有了從系統中每一個模塊的抽象語法樹(AST - abstract syntax tree)中的詞法層面抽取的能力。

在 Node.js v8.5.0 中,引入了 ESM 模塊支持。大部分現代瀏覽器也已經支持。

做爲 Browserify 的接班人,Webpack 主要接管了通用模塊打包器的角色,這歸功於其具有的大量新特性。正如 Babel 之於 ES6,Webpack 也一直支持着 ESM -- 及包括其 import 和 export聲明語句,也包括動態 import() 函數。Webpack 採用 ESM 並取得了特別豐富的成果,不只是其引入的「代碼分割(code-splitting)」機制,更是憑藉能將應用分爲不一樣部分打包的能力提高了首次加載時的使用體驗。

相比於 CJS,考慮到 ESM 做爲 JS 這門語言的自然性 -- 在幾年後,有理由期待其全面接管模塊生態系統。





-------------------------------------

長按二維碼或搜索 fewelife 關注咱們哦

相關文章
相關標籤/搜索