有這樣一個場景,客戶端運行好久,可是法務部和數據部須要收集用戶的一些信息,這些信息收集好以後須要進行相應的數據處理,以後上報到服務端。客戶端提供一個純粹的 JS 執行引擎,不須要 WebView 容器。iOS 端有成熟的 JavaScriptCore、Android 可使用 V8 引擎。這樣一個引擎配套有一個 SDK,訪問 Native 的基礎能力和數據運算能力,能夠當作是一個閹割版的 Hybrid SDK 額外增長了一些數據處理能力。前端
問題結束了嗎?處理邏輯的時候還須要用到2個庫:cheerio 和 sql。由於都是 Node 工程,因此純粹的 JS 環境是沒辦法直接執行。因此需求就進行了轉變 ———— 將 Node 項目打包成 UMD 規範。這樣就能夠在純粹的 JS 環境下運行。接下來的文章就分析下各類規範。其實也就是前端模塊化的幾種規範。git
隨着互聯網的飛速發展,前端開發愈來愈複雜。本文將從實際項目中遇到的問題出發,講述模塊化能解決哪些問題,以及以 Sea.js 爲例講解如何進行前端的模塊化開發。github
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);
複製代碼
繼續上述場景,不少狀況下都須要開發 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、
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 模塊導出的是一個對象(module.exports 屬性),該對象只在腳本運行完纔會生成。 ES6 的模塊機制是 JS 引擎對腳本進行靜態分析的時候,遇到模塊加載命令 import,就會生成一個只讀引用,等到腳本真正執行時,再根據這個只讀引用到被加載的模塊中取值,
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 是 Sea.JS 推廣的過程當中對模塊定義的規範化產出。CMD 推崇依賴就近。 CMD 規範儘可能保持簡單,並與 CommonJS 規範中的 Module 保持兼容,經過 CMD 規範編寫的模塊,能夠在 NodeJS 中運行。
CMD 中 require 依賴的描述用數組,則是異步加載,若是是單個依賴使用字符串,則是同步加載。
AMD 是 RequireJS 在推廣過程當中對模塊定義的規範化產出,CMD是SeaJS 在推廣過程當中被普遍認知。SeaJS 出自國內螞蟻金服玉伯。兩者的區別,玉伯在12年如是說:
RequireJS 和 SeaJS 都是很不錯的模塊加載器,二者區別以下:
二者定位有差別。RequireJS 想成爲瀏覽器端的模塊加載器,同時也想成爲 Rhino / Node 等環境的模塊加載器。SeaJS 則專一於 Web 瀏覽器端,同時經過 Node 擴展的方式能夠很方便跑在 Node 服務器端
二者遵循的標準有差別。RequireJS 遵循的是 AMD(異步模塊定義)規範,SeaJS 遵循的是 CMD (通用模塊定義)規範。規範的不一樣,致使了二者API 的不一樣。SeaJS 更簡潔優雅,更貼近 CommonJS Modules/1.1 和 Node Modules 規範。
二者社區理念有差別。RequireJS 在嘗試讓第三方類庫修改自身來支持 RequireJS,目前只有少數社區採納。SeaJS 不強推,而採用自主封裝的方式來「海納百川」,目前已有較成熟的封裝策略。
二者代碼質量有差別。RequireJS 是沒有明顯的 bug,SeaJS 是明顯沒有 bug。
二者對調試等的支持有差別。SeaJS 經過插件,能夠實現 Fiddler 中自動映射的功能,還能夠實現自動 combo 等功能,很是方便便捷。RequireJS無這方面的支持。
二者的插件機制有差別。RequireJS 採起的是在源碼中預留接口的形式,源碼中留有爲插件而寫的代碼。SeaJS 採起的插件機制則與 Node 的方式一致開放自身,讓插件開發者可直接訪問或修改,從而很是靈活,能夠實現各類類型的插件。
UMD(Universal Module Definition)是隨着大前端的趨勢產生,但願提供一個先後端跨平臺的解決方案(支持 AMD、CMD、CommonJS 模塊方式)。
實現原理:
// 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 能夠方便的打出一個 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 原生規範的優點就又減小了一點。