天天看點

淺談前端子產品化規範推薦閱讀為什麼需要子產品化?CommonJS、AMD 和 CMD 規範ECMAScript6 标準的子產品支援

文章目錄

  • 推薦閱讀
  • 為什麼需要子產品化?
    • 1.原始的子產品化寫法
    • 2.添加命名空間
    • 3.立即執行函數表達式
  • CommonJS、AMD 和 CMD 規範
    • CommonJS 規範
    • AMD 規範與 RequireJS
    • CMD 規範與 Sea.js
  • ECMAScript6 标準的子產品支援
    • export
    • import
    • export default 指令

推薦閱讀

掘金-前端子產品化

子產品化七日談

部分内容摘自《移動 Web 前端高效開發實踐》- iKcamp 著

為什麼需要子產品化?

JavaScript 發展初期,代碼簡單地堆積在一起,隻要能順利地從上往下一次執行即可。但随着網站越來越複雜,實作網站功能的 JavaScript 代碼也越來越龐大,網頁越來越像桌面程式,很多問題開始暴露出來,比如全局變量沖突、函數命名沖突、依賴關系處理等。

1.原始的子產品化寫法

既然子產品是要實作某個功能,那麼可以把實作功能的一組函數放在同一檔案中,像下面這樣

function a1() {
  // ...
}
function b2() {
  // ...
}
           

函數 a1 和 b2 組成一個子產品,其他檔案先加載該子產品,再對函數進行調用。

缺點:容易發生變量命名沖突,“污染”全局變量,子產品成員之間沒有太多必然的聯系。

2.添加命名空間

使用命名空間來管理子產品,即使用單全局變量的模式。

var module_special = {
  _index: 0,
  a1: function () {
    // ......
  },
  b2: function () {
    // ......
  }
}
 
// 調用
module_special.a1()
module_special.b2()
           

通常在屬性名前加下劃線表示該屬性為私有屬性,不過這隻是一種開發規範上的約定,這裡實際上該屬性仍然向外暴露。那麼怎樣讓私有屬性不被暴露呢?那就需要下面的子產品化方式。

3.立即執行函數表達式

立即執行函數表達式簡稱 “IIFE”(Immediately-Invoked Function Expression)

其能夠形成一個獨立的作用域,用 IIFE 作為一個 “容器”,“容器” 内部可以通路外部的變量,而外部環境不能通路 “容器” 内部的變量,是以 IIFE 内部定義的變量不會與外部的變量發生沖突。

var module_special = (function () {
  var _index = 0
  var a1 = function () {
    // ......
  }
  var b2 = function () {
    // ......
  }
  return {
    a1: a1,
    b2: b2
  }
})()
 
// 調用
module_special.a1()
module_special.b2()
           

這種方式既避免了命名沖突,又使得私有變量 _index 不能被外部通路和修改。jQuery 源碼大量采用了這種方式。

CommonJS、AMD 和 CMD 規範

CommonJS 規範

node.js 應用由子產品組成,采用 CommonJS 規範,通過全局方法 require 來加載子產品

var http = require('http')                            // 引入http子產品
var server = http.createServer(function (req, res) {  // 用http子產品提供的方法建立一個服務
  res.statusCode = 200                                // 傳回狀态碼為200
  res.setHeader('Content-Type', 'text/plain')         // 指定請求和響應的HTTP内容類型
  res.end('Hello World\n')                            // 傳回的資料
})
server.listen(3000, '127.0.0.1', function () {        // 監聽的端口和主機名
  console.log('Server running at http://127.0.0.1:3000') // 服務啟動成功後控制台列印資訊
})
           

如何編寫一個 CommonJS 規範的子產品?這就需要 Module 對象。

node.js 内部提供一個 Module 建構函數,所有子產品都是 Module 的執行個體。每個子產品内部,都有一個 Module 對象,代表目前子產品,包含如下屬性:

  • id:子產品的識别符,通常是帶有絕對路徑的子產品檔案名
  • filename:子產品的檔案名,帶有絕對路徑
  • loaded:傳回一個布爾值,表示子產品是否已經完成加載
  • parent:傳回一個對象,表示調用該子產品
  • children:傳回一個數組,表示該子產品要用到的其他子產品
  • exports:表示子產品對外輸出的值

其中 exports 是編寫子產品的關鍵,其表示目前子產品對外輸出的接口。其他檔案加載該子產品,實際讀取的是 module.exports。

// moduleA.js
module.exports = function (params) {
  console.log(params)
}
 
// 假設兩個檔案在同一目錄下
var moduleA = require('./moduleA')
moduleA()
 
// 為了友善,node.js為每個子產品提供一個exports變量指向module.exports
// 那麼moduleA也可以這樣編寫
exports.moduleA = function (params) {
  console.log(params)
}
           

注意:不能把值直接賦給 exports,因為這樣等于切斷了 exports 與 module.exports 的聯系

總結 CommonJS 子產品的特點如下:

  1. 所有子產品都有單獨作用域,不會污染全局作用域
  2. 重複加載子產品隻會加載一次,後面都從緩存讀取
  3. 子產品加載的順序按照代碼中出現的順序
  4. 子產品加載是同步的

AMD 規範與 RequireJS

AMD 和 CMD 規範因為現在用的比較少了(反正我是沒看見過),就簡單介紹下

CommonJS 子產品采用同步加載,适合服務端卻不适合浏覽器。AMD 規範支援異步加載子產品,規範中定義了一個全局變量 define 函數,描述如下:

define(id?, dependencies?, factory)

第一個參數 id,為字元串類型,表示子產品辨別,為可選參數。若不存在則子產品辨別預設定義為在加載器中被請求腳本的辨別。如果存在,那麼子產品辨別必須為頂層的或者一個絕對的辨別。

第二個參數 dependencies,定義目前所依賴子產品的數組。依賴子產品必須根據子產品的工廠方法優先級執行,并且執行的結果按照依賴數組中的位置順序以參數的形式傳入(定義中子產品的)工廠方法中。

第三個參數 factory,為子產品初始化時要執行的函數或對象。如果為函數,隻被執行一次。如果是對象,此對象應該為子產品的輸出值。如果工廠方法傳回一個值(對象、函數或任意強制類型轉換為 true 的值),應該設定為該子產品的輸出值。

建立一個标準 AMD 子產品

define('alpha', ['require', 'exports', 'beta'], function (require, exports, beta) {
  exports.berb = function () {
    return beta.verb()
    // 或者 return require('beta').verb()
  }
})
           

建立子產品辨別為 “alpha” 的子產品,依賴于内置的 “require” 和 “exports” 子產品和外部辨別為 “beta” 的子產品。require 函數取得子產品的引用,進而即使子產品沒有作為參數定義,也能夠被使用。exports 是定義的 alpha 子產品的實體,在其上定義的任何屬性和方法也就是 alpha 子產品的屬性和方法。

RequireJS 庫能夠把 AMD 規範應用到實際浏覽器 Web 端的開發中,其主要解決了兩個問題:實作 JavaScript 檔案的異步加載,避免網頁失去響應;管理子產品之間的依賴性,便于代碼的編寫和維護。

// AMD Wrapper
define(
  ['types/Employee'],     // 依賴
  function(Employee) {    // 這個回調會在所有依賴都被加載後才執行
    function Programmer() {
      // do something
    }
    Programmer.prototype = new Employee()
    return Programmer // return Constructor
  }
)
           

我們來比較下 CommonJS 和 AMD 的書寫風格:

// CommonJS
var a = require('./a') // 依賴就近
a.doSomething()

var b = require('./b')
b.doSomething()

// AMD
define(['a', 'b'], function (a, b) { // 依賴前置
  a.doSomething()
  b.doSomething()
})
           

CMD 規範與 Sea.js

CMD 規範全稱為 Common Module Definition

CMD 是另一種 js 子產品化方案,它與 AMD 很類似,不同點在于:AMD 推崇依賴前置、提前執行,CMD 推崇依賴就近、延遲執行。此規範其實是在 sea.js 推廣過程中産生的。

/** AMD寫法 **/
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { 
     // 等于在最前面聲明并初始化了要用到的所有子產品
    a.doSomething();
    if (false) {
        // 即便沒用到某個子產品 b,但 b 還是提前執行了
        b.doSomething()
    } 
});
 
/** CMD寫法 **/
define(function(require, exports, module) {
    var a = require('./a'); //在需要時申明
    a.doSomething();
    if (false) {
        var b = require('./b');
        b.doSomething();
    }
});
 
/** sea.js **/
// 定義子產品 math.js
define(function(require, exports, module) {
    var $ = require('jquery.js');
    var add = function(a,b){
        return a+b;
    }
    exports.add = add;
});
// 加載子產品
seajs.use(['math.js'], function(math){
    var sum = math.add(1+2);
});
           

ECMAScript6 标準的子產品支援

ECMAScript5 及之前的版本不支援原生子產品化,需要引入 AMD 規範的 RequireJS 或者 AMD 規範的 Seajs 等第三方庫來實作。

直到 ECMAScript6 才支援原生子產品化,其不但具有 CommonJS 規範和 AMD 規範的優點,而且實作得更加友好,文法較之 CommonJS 更簡潔、支援編譯時加載(靜态加載),循環依賴處理得更好。

ES6 子產品功能主要由兩個指令構成:export 和 import,export 指令用于規定子產品的對外接口,import 指令用于輸入其他子產品提供的功能。

export

在 ES6 中,一個子產品也是一個獨立的檔案,具有獨立的作用域,通過 export 指令輸出内部變量

let name = 'bus'
let color = 'green'
let weight = '20噸噸噸'
export {name, color, weight}
 
// export指令除了輸出變量,還可以輸出函數或類
export function run() {
  console.log('Bus is running')
}

           
// 可以使用 as 關鍵字對輸出的變量、函數、類重命名
let name = 'bus'
let color = 'green'
let weight = '20噸噸噸'
function run() { console.log('Bus is running') }
export {
  name as busName,
  color as busColor,
  weight as busWeight,
  run as busRun
}
           

import

import 指令用于導入子產品

import { name, color, weight, run } from './car'
 
// 導入一個子產品的時候也可以用 as 關鍵字對子產品進行重命名
import {name as busName } from './car'
 
// 通過星号 '*' 整體加載某個檔案
import * as car from './car'
console.log(car.name)   // bus
console.log(car.color)  // green
           

export default 指令

從前面的例子可以看出,使用 import 指令加載子產品時需要知道變量名或者函數名,或者整個檔案,否則無法加載。為了友善,可以使用 export default 指令為子產品指定預設輸出,加載該子產品時,可以使用 import 指令為其指定任意名字。

// 定義子產品 math.js
let basicNum = 0
let add = function(a, b) {
  return a+b
}
export default { basicNum, add }
 
// 引入
import math from './math'
function test() {
  console.log(math.add(99 + math.basicNum))
}
           

附:阮一峰《ES6标準入門》

import 指令是靜态加載而不是動态加載的,如果 import 指令要取代 require 方法,就要能實作動态加載。

有一個提案:建議引入 import() 函數,完成動态加載,import 指令能夠接收什麼參數,import() 函數指令就能接受什麼參數。

關于上面所說的提案,現在配置 webpack 使用 babel 轉譯應該能實作了(Vue 的路由懶加載,Webpack 的 splitChunk 都有用到)。

現在前端架構基本上使用 ES6 的子產品化文法,node.js 仍然保持 require 導入,兩者最主要的差別是:

  • require 是運作時加載,import 是編譯時加載

即下面的條件加載時不可能實作的

if (x === 2) {
    import MyModual from './myModual'
}
           

繼續閱讀