有這樣一個場景,客戶端運行好久,可是法務部和數據部須要收集用戶的一些信息,這些信息收集好以後須要進行相應的數據處理,以後上報到服務端。客戶端提供一個純粹的 JS 執行引擎,不須要 WebView 容器。iOS 端有成熟的 JavaScriptCore、Android 可使用 V8 引擎。這樣一個引擎配套有一個 SDK,訪問 Native 的基礎能力和數據運算能力,能夠當作是一個閹割版的 Hybrid SDK 額外增長了一些數據處理能力。前端
問題結束了嗎?處理邏輯的時候還須要用到2個庫:cheerio 和 sql。由於都是 Node 工程,因此純粹的 JS 環境是沒辦法直接執行。因此需求就進行了轉變 ———— 將 Node 項目打包成 UMD 規範。這樣就能夠在純粹的 JS 環境下運行。接下來的文章就分析下各類規範。其實也就是前端模塊化的幾種規範。git
隨着互聯網的飛速發展,前端開發愈來愈複雜。本文將從實際項目中遇到的問題出發,講述模塊化能解決哪些問題,以及以 Sea.js 爲例講解如何進行前端的模塊化開發。github
咱們從一個簡單的習慣出發。我作項目時,經常會將一些通用的、底層的功能抽象出來,獨立成一個個函數,好比sql
function each(arr) { // 實現代碼 } function log(str) { // 實現代碼 }
並像模像樣的將這些代碼抽取出來並統一到 util.js
中,在須要使用的地方引入該文件,看起來很棒,團隊內的同事很感激我提供了這麼便利的工具包。後端
直到團隊愈來愈大,開始有人抱怨數組
小楊:我定義了一個 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 都是很不錯的模塊加載器,二者區別以下:
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 原生規範的優點就又減小了一點。