打一個通用UMD包


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

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

 

前端模塊化開發的價值

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

  1. 惱人的命名沖突

我們從一個簡單的習慣出發。我做項目時,常常會將一些通用的、底層的功能抽象出來,獨立成一個個函數,比如

function each(arr) { // 實現代碼 } function log(str) { // 實現代碼 } 

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

直到團隊越來越大,開始有人抱怨

小楊:我定義了一個 each 方法遍歷對象,但是 util.js 中已經存在一個 each 方法,每次都需要改方法名,我只能叫 eachObject 方法。<br>張三:我定義了一個 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 可以方便的打出一個 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' } } 

廣州VI設計公司https://www.houdianzi.com

總結

手機端(無論 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 原生規范的優勢就又減少了一點。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM