打一個通用 UMD 包

有這樣一個場景,客戶端運行好久,可是法務部和數據部須要收集用戶的一些信息,這些信息收集好以後須要進行相應的數據處理,以後上報到服務端。客戶端提供一個純粹的 JS 執行引擎,不須要 WebView 容器。iOS 端有成熟的 JavaScriptCore、Android 可使用 V8 引擎。這樣一個引擎配套有一個 SDK,訪問 Native 的基礎能力和數據運算能力,能夠當作是一個閹割版的 Hybrid SDK 額外增長了一些數據處理能力。前端

問題結束了嗎?處理邏輯的時候還須要用到2個庫:cheeriosql。由於都是 Node 工程,因此純粹的 JS 環境是沒辦法直接執行。因此需求就進行了轉變 ———— 將 Node 項目打包成 UMD 規範。這樣就能夠在純粹的 JS 環境下運行。接下來的文章就分析下各類規範。其實也就是前端模塊化的幾種規範。git

前端模塊化開發的價值

隨着互聯網的飛速發展,前端開發愈來愈複雜。本文將從實際項目中遇到的問題出發,講述模塊化能解決哪些問題,以及以 Sea.js 爲例講解如何進行前端的模塊化開發。github

  1. 惱人的命名衝突 咱們從一個簡單的習慣出發。我作項目時,經常會將一些通用的、底層的功能抽象出來,獨立成一個個函數,好比
function each(arr) {
  // 實現代碼
}

function log(str) {
  // 實現代碼
}
複製代碼

並像模像樣的將這些代碼抽取出來並統一到 util.js 中,在須要使用的地方引入該文件,看起來很棒,團隊內的同事很感激我提供了這麼便利的工具包。sql

直到團隊愈來愈大,開始有人抱怨後端

小楊:我定義了一個 each 方法遍歷對象,可是 util.js 中已經存在一個 each 方法,每次都須要改方法名,我只能叫 eachObject 方法。
張三:我定義了一個 log 方法,但是王武的代碼出問題了,誰來看看?數組

抱怨愈來愈多,最後參照 Java 的方式,引入命名空間解決問題。因而 util.js 代碼變成了瀏覽器

var org = {};
org.Utils = {};

org.Utils.each = function (arr) {
  // 實現代碼
};

org.Utils.log = function (str) {
  // 實現代碼
};
複製代碼

可能看上去的代碼很 low,其實命名空間在前端領域的佈道者是 Yahoo!的 YUI2 項目,看看下面的代碼,是 Yahoo!的一個開源項目性能優化

if (org.cometd.Utils.isString(response)) {
  return org.cometd.JSON.fromJSON(response);
}
if (org.cometd.Utils.isArray(response)) {
  return response;
}
複製代碼

經過命名空間雖然能夠極大的解決衝突問題,可是每次在調用一個方法時都須要寫一大堆命名空間相關的代碼,剝奪了編碼樂趣。服務器

另外一種方式是一個自執行函數來實現。併發

(function (args) {
  //...
})(this);
複製代碼
  1. 繁瑣的文件依賴

繼續上述場景,不少狀況下都須要開發 UI 層通用組件,這樣項目組就不須要重複造輪子。其中有一個高頻使用的組件就是 dialog.js

<script src="util.js"></script>
<script src="dialog.js"></script>
<script>
  org.Dialog.init({ /* 傳入配置 */ });
</script>
複製代碼

雖然公共組作項目都會編寫使用文檔、發送郵件告知全員(項目地址、使用方式等),可是仍是有人問「爲何 dialog.js 有問題」,最後排查的結果基本都是沒有引入 util.js

<script src="dialog.js"></script>
<script>
  org.Dialog.init({ /* 傳入配置 */ });
</script>
複製代碼

命名衝突文件依賴是前端開發中2個經典問題,通過開發者不斷的思考和研究,誕生了模塊化的解決方案,以 CMD 爲例

define(function(require, exports) {
    exports.each = function (array) {
        // ...
    };
    exports.log = function(message) {
        // ... 
    };
});
複製代碼

經過 exports 就能夠向外提供接口, dialog.js 代碼變成

define(function(require, exports) {
    var util = require('./util.js')

    exports.init = function () {
        // ...
    };
});
複製代碼

使用的時候能夠經過 require('./util.js') 獲取到 util.js 中經過 exports 暴露的接口。 require 的方式在其餘不少語言中都有解決方案:include、

模塊化的好處

  1. 模塊的版本管理:經過別名等配置,配合構建工具,能夠輕鬆實現模塊的版本管理
  2. 提升可維護性: 模塊化能夠實現每一個文件的職責單一,很是有利於代碼的維護。
  3. 前端性能優化: 對於前端開發來講,異步加載模塊對於頁面性能很是有益。
  4. 跨環境共享模塊: CMD 模塊定義規範與 NodeJS 的模塊規範很是相近,因此經過 Sea.JS 的 NodeJS 版本,能夠方便的實現模塊的跨服務器和瀏覽器共享。

CommonJS 規範

CommonJS 是服務器端模塊的規範。NodeJS 採用了這個規範。CommonJS 加載模塊是同步的,因此只有加載完成後才能執行後面的操做。 由於服務器的特色,加載的模塊文件通常都存在在本地硬盤,因此加載起來比較快,不用考慮異步的方式。

CommonJS 模塊化的餓規範中,每一個文件都是一個模塊,擁有獨立的做用域、變量、以及方法等,對其餘模塊不可見。 CommonJS 規範規定,每一個模塊內部, module 變量表示當前模塊,它是一個對象,它的 exports 屬性是對外的接口,加載某個模塊,實際上是加載該模塊的 module.exports 屬性,require 方法用於加載模塊。

// Person.js
function Person () {
    this.eat = function () {
        console.log('eat something')
    }

    this.sleep = function () {
        console.log('sleep')
    }
}

var person = new Person();
exports.person = person;
exports.name = name;

// index.js
let person = require('./Person').person;
person.eat()
複製代碼

CommonJS 與 ES6 模塊的差別

  1. CommonJS 模塊輸出的是值的拷貝,ES6 模塊輸出的是值的引用
  2. CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口

CommonJS 模塊導出的是一個對象(module.exports 屬性),該對象只在腳本運行完纔會生成。 ES6 的模塊機制是 JS 引擎對腳本進行靜態分析的時候,遇到模塊加載命令 import,就會生成一個只讀引用,等到腳本真正執行時,再根據這個只讀引用到被加載的模塊中取值,

AMD 規範

AMD(Asynchronous Module Definition) 是在 Require.JS 推廣的過程當中對模塊定義的規範化產出。AMD 推崇依賴前置。它是 CommonJS 模塊化規範的超集,做用在瀏覽器上。它的特色是異步,利用了瀏覽器的併發能力,讓模塊的依賴阻塞變少。

AMD 的 API

define(id?, dependencyies?, factory);
複製代碼

id 是模塊的名字,是可選參數。 dependencies 指定了該模塊所依賴的模塊列表,是一個數組,也是可選參數。每一個依賴的模塊的輸出都將做爲參數依次傳入 factory 中。

require([module], callback)
複製代碼

AMD 規範容許輸出模塊兼容 CommonJS 規範,這時 define 方法以下

define(['module1', 'module2'], function(module1, module2) {
  function foo () {
    // ...
  }
  return { foo: foo };
});
複製代碼
define(function(require, exports, module) {
  var requestedModule1 = require('./module1')
  var requestedModule2 = require('./module2')

  function foo () {
    // ...
  }
  return { foo: foo };
});
複製代碼

優勢: 適合在瀏覽器環境中加載模塊,能夠實現並行加載多個模塊 缺點: 提升了開發成本,並不能按需加載,而是提早加載全部的依賴

CMD 規範

CMD 是 Sea.JS 推廣的過程當中對模塊定義的規範化產出。CMD 推崇依賴就近。 CMD 規範儘可能保持簡單,並與 CommonJS 規範中的 Module 保持兼容,經過 CMD 規範編寫的模塊,能夠在 NodeJS 中運行。

CMD 模塊定義規範

CMD 中 require 依賴的描述用數組,則是異步加載,若是是單個依賴使用字符串,則是同步加載。

AMD 是 RequireJS 在推廣過程當中對模塊定義的規範化產出,CMD是SeaJS 在推廣過程當中被普遍認知。SeaJS 出自國內螞蟻金服玉伯。兩者的區別,玉伯在12年如是說:

RequireJS 和 SeaJS 都是很不錯的模塊加載器,二者區別以下:

  1. 二者定位有差別。RequireJS 想成爲瀏覽器端的模塊加載器,同時也想成爲 Rhino / Node 等環境的模塊加載器。SeaJS 則專一於 Web 瀏覽器端,同時經過 Node 擴展的方式能夠很方便跑在 Node 服務器端

  2. 二者遵循的標準有差別。RequireJS 遵循的是 AMD(異步模塊定義)規範,SeaJS 遵循的是 CMD (通用模塊定義)規範。規範的不一樣,致使了二者API 的不一樣。SeaJS 更簡潔優雅,更貼近 CommonJS Modules/1.1 和 Node Modules 規範。

  3. 二者社區理念有差別。RequireJS 在嘗試讓第三方類庫修改自身來支持 RequireJS,目前只有少數社區採納。SeaJS 不強推,而採用自主封裝的方式來「海納百川」,目前已有較成熟的封裝策略。

  4. 二者代碼質量有差別。RequireJS 是沒有明顯的 bug,SeaJS 是明顯沒有 bug。

  5. 二者對調試等的支持有差別。SeaJS 經過插件,能夠實現 Fiddler 中自動映射的功能,還能夠實現自動 combo 等功能,很是方便便捷。RequireJS無這方面的支持。

  6. 二者的插件機制有差別。RequireJS 採起的是在源碼中預留接口的形式,源碼中留有爲插件而寫的代碼。SeaJS 採起的插件機制則與 Node 的方式一致開放自身,讓插件開發者可直接訪問或修改,從而很是靈活,能夠實現各類類型的插件。

UMD 規範

UMD(Universal Module Definition)是隨着大前端的趨勢產生,但願提供一個先後端跨平臺的解決方案(支持 AMD、CMD、CommonJS 模塊方式)。

實現原理:

  1. 先判斷是否支持 Node.js 模塊格式(exports 是否存在),存在則使用 Node.js 模塊格式
  2. 再判斷是否支持 AMD 模塊格式(define 是否存在),存在則使用 AMD 模塊格式
  3. 前2個都不存在則將模塊公開到全局(window 或 global)
// if the module has no dependencies, the above pattern can be simplified to
(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define([], factory);
    } else if (typeof exports === 'object') {
        // Node. Does not work with strict CommonJS, but
        // only CommonJS-like environments that support module.exports,
        // like Node.
        module.exports = factory();
    } else {
        // Browser globals (root is window)
        root.returnExports = factory();
  }
}(this, function () {

    // Just return a value to define the module export.
    // This example returns an object, but the module
    // can return a function as the exported value.
    return {};
}));
複製代碼

可能有些人就要問了,爲何在上面的判斷中寫了 AMD,怎麼沒有 CMD? 😂 由於前端構建工具 Webpack 不可識別 CMD 規範,使用 CMD 就須要引用工具,好比 Sea.JS

講道理,若是想判斷 CMD,那 UMD 代碼如何寫?

(function(root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define([], factory);
    } else if (typeof define === 'function' && define.cmd) {
        // CMD
        define(function(require, exports, module) {
            module.exports = factory()
        })
    } else if (typeof exports === 'object') {
        // Node. Does not work with strict CommonJS, but
        // only CommonJS-like environments that support module.exports,
        // like Node.
        module.exports = factory();
    } else {
        // Browser globals (root is window)
        root.returnExports = factory();
    }
}(this, function() {
    // Just return a value to define the module export.
    // This example returns an object, but the module
    // can return a function as the exported value.
    return {};
}))
複製代碼

模塊化

回到正題

Cheerio 如何打包到普通的 JS 執行環境中。

Webpack 支持的模塊化參數以下圖所示:

Webpack 模塊化參數

藉助 Webpack 能夠方便的打出一個 umd 規範的包。

module.exports = {
    entry: './src/cheerio.js',
    output: {
        filename: 'cheerio.js',
        // export to AMD, CommonJS, or window
        libraryTarget: 'umd',
        // the name exported to window
        library: 'cheerio',
        globalObject: 'this'
  }
}
複製代碼

總結

手機端(不管 iOS 仍是 Android)的底層渲染內核都是類 Chrome v8 引擎。v8 引擎在執行 JS 代碼時,是將代碼先以 MacroAssembler 彙編庫在內存中先編譯成機器碼再送往 CPU 執行的,並非像其它 JS 引擎那樣解析一行執行一行。因此,靜態加載的 ES6 模塊規範,更有助於 v8 引擎發揮價值。而運行時加載的 CommonJS、AMD、CMD 規範等,均不利於 v8 引擎施展拳腳。

在 NodeJS 開發項目中,Node9 已經支持 ES6 語法,徹底可使用 ES6 模塊規範。NodeJS 的誕生,自己就基於 Google 的 v8 引擎,沒有理由不考慮發揮 v8 的最大潛能。

在瀏覽器 JS 開發項目中,由於從服務器加載文件須要時間,使用 CommonJS 規範確定是不合適了。至因而使用原生的 ES 模塊規範,仍是使用 Sea.js,要看具體場景。若是想頁面儘快加載,Sea.js 適合;若是是單頁面網站,適合使用原生的 ES6 模塊規範。還有一點,瀏覽器並不是只有 Chrome 一家,對於沒有使用 v8 引擎的瀏覽器,使用 ES6 原生規範的優點就又減小了一點。

相關文章
相關標籤/搜索