最詳細的前端模塊化解決方案梳理

前言

美好的一天從 npm run dev 開始,對於如今的前端而言從百家爭鳴到逐漸統一的輔助開發工具對前端效率的提高有着不可替代的做用,這一切都必須依賴前端的模塊化。在前端還處在刀耕火種的年代想實現模塊化只能經過閉包也就是 IIFE ,而現在 ES6 Modules 多是前端最經常使用的模塊解決方案,那麼本篇從 IIFE 來開始概括前端模塊化的前世此生。哦,對了,沒說爲何要琢磨模塊化了,模塊化便於拆封代碼,避免變量衝突,沒有模塊化想開發一個大工程,呃,不太行。html

IIFE(閉包)

咱們該怎麼理解模塊這個概念,在刀耕火種的年代很天然的能夠把一個js文件理解成一個模塊,看下面的demo:前端

<!DOCTYPE html>
<html>
<head>
  <title></title>
</head>
<body>
  <script src="./main.js"></script>
  <script src="./app.js"></script>
  <script src="./api.js"></script>
</body>
</html>
複製代碼

main.js、app.js、api.js 把他們仨理解成是三個模塊,可是這三個js文件裏面的代碼應該如何構建呢?由於js自己機制的緣由只能將代碼暴露到全局做用域下,若是不去避免全局變量污染那麼這樣的模塊是毫無心義的也是後患無羣的。在刀耕火種的年代天然而然想到了閉包,咱們嘗試用下面的方法去構建這三個js文件達到隔離做用域的目的。node

(function(){
  var name = 'main.js'
  ...
})()
複製代碼

咱們用匿名函數而不是聲明一個function再調用是由於匿名函數,執行完後很快就會被釋放,這種機制不存在函數名污染全局做用域的狀況。這種匿名函數的方式就是IIFE,雖然它能夠有效解決命名衝突的問題,可是對於依賴管理,仍是一籌莫展。因爲瀏覽器解析器是從上至下執行腳本,所以爲了保證腳本間的依賴關係,就必須手動維護好script標籤的引入順序,涉及到模塊間的通訊能夠用參數的方式傳入。webpack

CommonJS(node.js)

CommonJS 是一位叫 Kevin Dangoor 的國際友人提出的關於js模塊的一種規範,注意了是規範不是實現方案,而 node.js 就是基於 CommonJS 規範實現的模塊化。es6

咱們先看一下CommonJS規範都規定了那些內容:web

  1. 一個單獨的文件就是一個模塊。
  2. 每個模塊都是一個單獨的做用域。

node.js 對 CommonJS 規範作了實現,那麼 node.js 到底作了什麼?npm

  1. node.js 的實現中,給每一個文件賦予了一個 module 對象,這個對象包括了描述當前模塊的全部信息,它的 exports 屬性(即module.exports)是對外的接口。加載某個模塊,實際上是加載該模塊的 module.exports 屬性。對 module 對象擁有的屬性簡單列一下:
  • module.id 模塊的識別符,一般是帶有絕對路徑的模塊文件名。
  • module.filename 模塊的文件名,帶有絕對路徑。
  • module.loaded 返回一個布爾值,表示模塊是否已經完成加載。
  • module.parent 返回一個對象,表示調用該模塊的模塊。
  • module.children 返回一個數組,表示該模塊要用到的其餘模塊。
  • module.paths 表示模塊的搜索路徑,路徑的多少取決於目錄的深度。
  • module.exports 表示模塊對外輸出的值。
  1. 同時爲每一個文件提供了一個 require 方法用於加載模塊。
  2. 爲了方便 node.js 爲每一個模塊提供一個 exports 變量,指向 module.exports。這等同在每一個模塊頭部聲明 var exports = module.exports 。

module.exports 和 exports 在使用上有什麼區別?json

上面說過 exports 的實現方法等同於 var exports = module.exports,也就是說想要經過 exports 關節字向外輸出值只能給 exports 添加屬性好比下面這種寫法。gulp

exports.myName = 'tom'
exports.getMyName = function(){
  return 'tom'
}
複製代碼

要注意的是,不能直接將 exports 變量指向一個值,由於這樣等於切斷了exports 與 module.exports 的聯繫。好比下面這種寫法是無效的,由於 exports 再也不指向 module.exports 了。api

exports = function(){
  return 'tom'
}
// or
exports = 'tom'
複製代碼

require() 方法的加載機制:

require 命令用於加載模塊文件。require 命令的基本功能是,讀入並執行一個 js 文件,而後返回該模塊的 exports 對象,若是沒有發現指定模塊,會報錯。require 方法默認讀取 js 文件,因此能夠省略 js 後綴名,js文件名前面須要加上路徑,能夠是相對路徑也能夠是絕對路徑。若是省略路徑,node.js 會認爲你要加載一個內部模塊,或者已經安裝在本地的 node_modules 目錄中的模塊。若是加載的是一個目錄,node.js會首先尋找該目錄中的 package.json 文件,加載該文件 main 字段配置的模塊,不然就尋找該目錄下的 index.js 文件。在加載 node_modules 目錄中的模塊時按照下面的路徑順序去查找。

/usr/local/lib/node/x.js
/home/user/projects/node_modules/x.js
/home/user/node_modules/x.js
/home/node_modules/x.js
/node_modules/x.js

require() 方法的緩存機制:

在 node.js 中經過 require 屢次引入同一模塊,模塊內的代碼並不會重複執行屢次,這依賴於 node.js 的緩存機制,第一次加載某個模塊時,node.js 會緩存該模塊,一個模塊被加載一次以後,就會在緩存中維持一個副本,若是遇到重複加載的模塊會直接提取緩存中的副本,也就是說在任什麼時候候每一個模塊都只在緩存中有一個實例,模塊加載的順序,按照其在代碼中出現的順序。

關於運行時:

在服務器端,模塊的加載是運行時同步加載的;在瀏覽器端,模塊須要提早編譯打包處理(gulp, webpack)。

require() 加載模塊是同步仍是異步?

首先 CommonJS 規範加載模塊是同步的,因爲 node.js 主要用於服務器,模塊文件通常都已經存在於本地硬盤,何況有上文提到的緩存機制模塊常常被複用,因此加載起來比較快,IO開銷能夠忽略,不用考慮非同步加載的方式,因此 CommonJS 規範比較適用。也就是說,只有加載完成,才能執行後面的操做。可是,若是是瀏覽器環境,要從服務器端加載模塊,這時就必須採用非同步模式,所以瀏覽器端通常採用AMD規範。

如何實現做用域隔離?

在瀏覽器端經過觀察打包以後的代碼能夠發現實現做用域隔離的本質仍是經過函數做用域也就是閉包,這部分不展開說了,後面計劃出一篇分析 webpack 打包生成代碼的博客...

AMD(RequireJS)

AMD 也是關於模塊化的規範,RequireJS 是基於 AMD 規範實現的模塊化解決方案。上文說過 CommonJS 規範中模塊的加載是同步的,那麼試想在瀏覽器環境中模塊是存在於服務器的,在加載模塊的過程當中會致使阻塞,使得咱們後面的步驟沒法進行下去致使很長的時間內瀏覽器是被阻塞的,還可能會執行一個未定義的方法而致使出錯。相對於服務端的模塊化,瀏覽器環境下模塊化的標準必須知足一個新的需求就是「異步的模塊管理」,在這樣的背景下,RequireJS 出現了。

RequireJS 提供了兩個方法:

  • require() 用於引入其餘模塊
  • define() 定義新的模塊

RequireJS 的內部邏輯是經過函數 define() 將須要的依賴模塊加載進來,在回調裏拿到被依賴也就是被加載的模塊而後返回一個新的值(模塊),咱們全部的關於新模塊的業務代碼都在這個函數內部操做。

咱們看下面的例子:

定義模塊

// 定義不依賴其餘模塊的模塊
define(function() {
  return {
    ...
  }
})

// 定義依賴其餘模塊的模塊
define([ './module1', './module2' ], function(m1, m2) {
  // m一、m2 就是 './module1''./module2' 加載到的值
  return {
    ...
  }
})
複製代碼

加載模塊

require(['./module1', './module2'], function(m1, m2) {
  // m一、m2 一樣是 './module1''./module2' 加載到的值
  m1.fun()
  m2.fun()
})
複製代碼

CMD(CommonJS)

CommonJS是規範,AMD是規範,CMD固然也是規範,基於CMD的實現方案是 Sea.js。經過對 RequireJS 的分析咱們發現一個問題,就是在用 RequireJS 聲明一個模塊時要提早指定全部的依賴,這些依賴項會被當作形參傳到 define() 方法中,這加大了開發的難度,由於依賴的模塊會優先所有加載,那麼在閱讀代碼的時候要先把依賴模塊都閱讀一次(由於這些依賴模塊是初始化的時候執行的)。偷懶是開發者提升效率的初衷,那麼若是能夠在業務代碼中使用到依賴模塊的時候再去加載該模塊這樣就沒必要提早閱讀所有依賴模塊代碼了。這樣 Sea.js 就出現了,在使用上能夠理解 Sea.js 是 CommonJS 和 AMD 的結合。

define(function(require, exports, module) {
  let myHeader, myBody, myFooter
  if (status1) {
    var header = require('./header')
    myHeader = header.fun()
  }
  if (status2) {
    var body = require('./body')
    myBody = body.fun()
  }
  if (status3) {
    var footer = require('./footer')
    myFooter = footer.fun()
  }
  require.async('./module', function (m) { })

  module.exports = {
    header: myHeader,
    body: myBody,
    footer: myFooter
  }
})
複製代碼

要注意的是雖然 require() 方法夾雜在了業務代碼中可是仍是會提早加載,加載完某個依賴模塊後並不執行,只是下載而已,在全部依賴模塊加載完成後進入主邏輯,遇到 require 語句的時候才執行對應的模塊,這樣模塊的執行順序和書寫順序是徹底一致的。若是使用 require.async() 方法,能夠實現模塊的懶加載。

雖然 RequireJS 也支持 SeaJS 的寫法,可是依賴的模塊任然是預先加載的。

UMD(AMD + CommonJS)

CommonJS 適用於服務端,AMD、CMD 適用於web端,那麼須要同時運行在這兩端的模塊就能夠採用 UMD 的方法,使用該模塊化方案,能夠很好地兼容AMD、CommonJS語法。UMD 先判斷是否支持Node.js的模塊(exports)是否存在,存在則使用 node.js 模塊模式。再判斷是否支持 AMD(define是否存在),存在則使用AMD方式加載模塊。因爲這種通用模塊解決方案的適用性強,不少JS框架和類庫都會打包成這種形式的代碼。

ES6 Module

在 ES6 中沒必要使用特殊的手段,ES6在語法層面就支持了模塊化,然而受限於瀏覽器的實現程度,若是想要在瀏覽器中運行,仍是須要經過 Babel 等轉譯工具進行編譯。ES6提供了 importexport 命令,分別對應模塊的導入和導出功能。es6模塊相關的語法有好多,不展開說了。

特色:

  1. 語法是靜態的,import 會自動提高到代碼的頂層。
  2. 使用 import 導入的變量是隻讀的,沒法被賦值,並且是引用傳遞。(若是你的模塊在運行過程當中修改了導出的變量值,就會反映到使用模塊的代碼中去。因此,不推薦在模塊中修改導出值,導出的變量應該是靜態的。)
  3. ES6使用的是基於文件的模塊,也就是一個文件一個模塊。
  4. ES6模塊API是靜態的,一旦導入模塊後,沒法再在程序運行過程當中增添方法。

Webpack中的模塊化方案

webpack 興起以後,咱們可使用 CommonJS、AMD、CMD、UMD、ES6 任意的方法來開發最終 webpack 會打包成你須要的樣子。

相關文章
相關標籤/搜索