淺析前端的模塊化

咱們可能常常聽到一些模塊化的概念,譬如 AMDCommonJSES Modules。這些又是什麼概念呢?它們爲何而存在,做用又是什麼呢?本文將對模塊化的概念進行逐一分析。node

爲何須要模塊化

在瞭解模塊化的概念前,首先先解決一個問題 - 爲何須要模塊化?webpack

先從實際問題出發,在相似 require.jssea.jsbrowserifywebpack 等工具出現以前,咱們可能會遇到以下一些問題:web

  • 咱們在引用不一樣的庫或者 js 文件時,可能會出現命名衝突。
  • 引用的庫或者文件都會有個先來後到,那如何去決定與維護這些順序呢?
  • 庫與庫之間、文件與文件之間可能存在循環引用,即在 a.js 中引用了 b.js,而 b.js 中也引用了 a.js;二者相互依賴。那咱們如何決定先引用哪一個件呢?

咱們再從一個生活中的例子出發,簡要了解一下模塊化的優勢,即爲何須要模塊化的緣由。npm

假設你有一套修理工具箱,裏面包含了屬於這套修理工具箱的各類型號的螺絲批、鉗子和錘子等等。每次家裏水管破了,燈泡壞了什麼的,你均可以拿這套修理工具箱進行修理。每次修理可能都會形成一些工具的損耗或損壞,損壞以後咱們就應該去買相同型號的工具進行補充;其餘不對頭的工具不會回收回這套修理工具箱中,一樣這套修理工具箱中的工具也不會隨意扔出工具箱中。設計模式

咱們把上述例子轉化爲模塊化來看看。首先,修理工具箱就是 模塊,裏面的工具就是 模塊 中的各類變量或函數。工具出現損壞等於 模塊 內出了什麼問題,這時候咱們只須要修復 模塊 內的 bug 就行了。其餘不對頭的工具不會回收回工具箱中,反之工具箱中的工具不會隨意被扔出表示 模塊 內的變量、函數等不會污染外部的變量、函數等等,反之亦然。這套工具箱能夠重複利用也就是 模塊 的複用性很強。數組

總結模塊化有三大優勢:瀏覽器

  • 可維護性強,更新或修復模塊內的邏輯不會影響外部邏輯
  • 獨立的命名空間,模塊內的變量不會污染外部的變量,即便它們擁有相同的變量名
  • 可複用性強,咱們須要在不一樣的地方用到某個模塊,只須要在對應的地方引入它就好了,無需重複地拷貝複製。

什麼是模塊化

接下來將會經過一些常見的例子與概念來解釋什麼是模塊化。注意,閉包在模塊化中有着重要的應用,這裏假設你對閉包概念已有所瞭解。安全

IIFE - 當即執行函數表達式

顧名思義,當即執行函數表達式就是一個函數在定義時就會當即執行。服務器

var global = "I'm global"

(function () {
  var foo = 'foo'

  function bar () {
    console.log('bar')
  }

  console.log(foo)

  bar()

  console.log(global)
})()
// foo
// bar
// I'm global

var foo = 'global foo'
console.log(foo)  // global foo
bar() // Uncaught ReferenceError: bar is not defined
複製代碼

能夠看到,咱們在當即執行函數表達式的外部訪問其變量會拋出錯誤,而在當即執行函數表達式的內部能夠隨時訪問外部變量。外部與當即執行函數表達式一樣有命名爲 foo 的變量,但這二者互不影響。其實這種行爲就相似於 C++ 等語言中類的私有變量、私有方法。閉包

固然咱們還可讓當即執行函數表達式放回一些東西,相似類的暴露公共方法、變量的概念。

var module = (function () {
  var _privateCnt = 0
  var _privateProperty = 'I am private property'

  function _privateCnter () {
    return _privateCnt += 1
  }

  function publicCnter () {
    return _privateCnter()
  }

  return {
    property: _privateProperty,
    publicCnter: publicCnter
  }

})()

console.log(module.property) // I am private property
console.log(module.publicCnter()) // 1
console.log(module.publicCnter()) // 2
console.log(module.publicCnter()) // 3
複製代碼

這個例子展現了經過當即執行函數表達式將一些變量、方法暴露出去,並防止外部直接修改一些咱們不但願修改的變量、方法。這樣作還有一個好處,就是咱們能夠快速地瞭解到這個當即執行函數爲咱們提供了哪些公共屬性及方法,而不須要閱讀全部邏輯代碼。這種方式在設計模式中也稱做 模塊模式(Module Pattern)

CommonJS

CommonJS 主要是爲服務端定義的模塊規範,它一開始的名字爲 ServerJSnpm 生態系統基本都是基於 CommonJS 規範所創建起來的。

// 在 foo.js 中,咱們導出了變量 foo
module.exports = {
  foo: 'foo'
}

// 在 bar.js,咱們經過 require 引入了變量 foo
var module = require('foo')
console.log(module.foo) // foo
複製代碼

看起來很簡單是吧。可能有人會問了,這個 module 是什麼東西呢?其實 module 是 Node 中的一個內置對象。咱們能夠在 node 環境下打印看看

咱們能夠看到 module 有好幾個屬性,其中 id 是爲了讓 node 知道這個模塊在哪裏,是啥;exports 就是咱們要導出的對象了。

在確保 foo.jsbar.js 在同一目錄下,咱們再將例子稍加修改:

// foo,js
module.exports = {
  foo: 'foo'
}
console.log('module: ', module)

// bar.js
var module = require('./foo')
console.log(module.foo)
複製代碼

運行 node bar.js 能夠獲得如下信息:

經過 CommonJS 規範定義的模塊一樣有一開始說到的模塊的三大優勢,其實咱們只須要把這些模塊文件看出一個個當即執行函數,也就會很好理解了。

CommonJS 裏模塊都是同步加載的,在瀏覽器中若是同步去加載模塊的話會形成阻塞,致使頁面性能降低;而在服務端中,由於文件都存在於同一個硬盤上,因此即便是同步加載都不會有什麼影響。

再補充一個小細節,你可能時不時能看到 var exports = module.exports 這樣的代碼。或許你會問爲何要怎麼作,難道有什麼技巧嗎?其實這只是簡單的引用而已。即變量 exports 一樣指向了 module.exports 的內存地址,也就是二者指向的對象是徹底同樣的。咱們想在 module.exports 裏添加導出的東西時,只須要在 exports 里加就好了。就是這麼簡單,只不過被一些說法搞得高深莫測了而已。

var exports = module.exports

exports.foo = 'foo''
複製代碼

AMD - Asynchronous Module Definition

剛剛咱們說到 CommonJS 主要是用於服務端的規範,而客戶端是沒法使用它的,而且 CommonJS 是同步加載模塊的。因此咱們又有了叫作 AMD 規範的東西,也就是異步模塊定義規範。顧名思義,咱們能夠利用這個規範來作到模塊與模塊的依賴能夠經過異步的方式來加載;這也是瀏覽器(客戶端)所但願的。

AMD 中的核心就是 define 這個方法。

define(
  module_id,
  [dependencies],
  definition
)
複製代碼

其中 define 中的 module_iddependencies 爲可選參數。

首先 module_id 它是一個字符串,指的是定義的模塊的名字,這個名字必須是惟一的。第二個參數 dependencies 是模塊所依賴的模塊組成的數組,並做爲參數傳入給第三個參數 definition 工廠方法中。第三個參數 definition 就是爲模塊初始化要執行的函數或對象。若是爲函數,它應該只被執行一次。若是是對象,此對象應該爲模塊的輸出值。

// dep1
define('dep1', [], function () {
  return {
    doSomething: function () {
      console.log('do something')
    }
  }
})

define('dep2', [], function () {
  return {
    doOtherThing: function () {
      console.log('do other thing')
    }
  }
})

define('module', ['dep1', 'dep2'], function (dep1, dep2) {
  dep1.doSomething()
  dep2.doOtherThing()
})
複製代碼

雖然 AMD 規範提供了異步加載模塊的方案,可是給個人感受就是邏輯不如 CommonJS 直觀。所以在 ES6 中也就有了原生的模塊化: ESM - ES Modules

ES Modules

CommonJS 在服務端中應用普遍,但因爲它是同步加載模塊的,它在客戶端不太合適;而 AMD 支持瀏覽器異步加載模塊,但在服務端卻顯得沒有必要,所以 ES Modules 出現了。咱們先來看看 ES Modules 是如何工做的。

ES ModulesCommonJS 很類似,較新的瀏覽器均已支持 ES ModulesNode.js 正在慢慢支持相關規範。

ES Modules 的核心爲 exportimport,分別對應導出模塊與導入模塊。

導出模塊:

// CommonJS
module.exports = foo () {
  console.log('here is foo')
}

// ES Modules
export default function bar () {
  console.log('here is bar')
}
複製代碼

導入模塊:

// CommonJS
var foo = require('./foo')

foo() // here is foo

// ES Modules
import bar from './bar'

bar() // here is bar
複製代碼

這二者這麼類似,它們在實際的表現上有什麼不一樣呢?

我分別在 FirefoxEdgeChrome 上測試(Chrome 因爲自身的安全策略沒法直接經過本地文件進行測試,因此利用插件 Web Server for Chrome 起了個本地服務器。

測試代碼以下圖(注意咱們在使用 ES Modules 時要給 script 標籤加上 type="module"

測試的結果顯示爲:

開始執行 bar.js
開始執行 foo.js
here is foo
here is bar
複製代碼

若是咱們使用 CommonJS 又會有什麼結果呢?

開始執行 foo.js
here is foo
開始執行 bar.js
here is bar
複製代碼

很顯然 CommonJS 是同步加載模塊的,因此代碼的執行也是順序的。而 ES Modules 是異步加載模塊的,且 ES Modules 是編譯時加載模塊,在運行時(執行代碼時)再根據相關的引用去被加載的模塊中取值。再詳細一點來講的話,整個過程分以下三個步驟:

  • 構建 - 查找、下載並將文件解析到模塊記錄中
  • 實例化 - 在內存中找到全部導出(export)的值的位置,但暫不對這些值進行賦值;而後在內存中建立 exportsimports 的空間。這一步稱爲連接
  • 運行 - 運行代碼並將實際的值賦予給實例化中導出的值

ES Modules 解析的相關參考文章

所以在編譯期間,編譯器先找到了 foo.js 的依賴 bar.js,先編譯 bar.js 而後纔是 foo.js。因此你纔會先看到 開始執行 bar.js

總結

  • 模塊化簡單來講就是將相關的邏輯代碼獨立出來,獨立的形式有不少種,能夠是單純的一個函數,亦能夠是單獨的一個文件。
  • 模塊化能夠更好地組織代碼結構,加強其可維護性,可複用性強。
  • CommonJS 工做原理爲同步加載模塊,在 Node.js 中有着普遍的使用,對客戶端不友好。
  • AMD 工做原理爲異步加載模塊。
  • ES ModulesES6 推出的規範,客戶端的支持比較好,Node.js 將會慢慢全面支持。它與 AMD 同樣,也是異步加載模塊。
相關文章
相關標籤/搜索