淺談JS前端模塊化的幾種規(guī)范
前言
有這樣一個場景,客戶端運行很久,但是法務部和數(shù)據(jù)部需要收集用戶的一些信息,這些信息收集好之后需要進行相應的數(shù)據(jù)處理,之后上報到服務端??蛻舳颂峁┮粋€純粹的js執(zhí)行引擎,不需要 WebView 容器。iOS 端有成熟的JavaScriptCore、Android 可以使用 V8 引擎。這樣一個引擎配套有一個 SDK,訪問 Native 的基礎能力和數(shù)據(jù)運算能力,可以看成是一個閹割版的 Hybrid SDK 額外增加了一些數(shù)據(jù)處理能力。
問題結束了嗎?處理邏輯的時候還需要用到2個庫:cheerio和sql。因為都是 Node 工程,所以純粹的js環(huán)境是沒辦法直接執(zhí)行。所以需求就進行了轉變 ———— 將 Node 項目打包成 UMD 規(guī)范。這樣就可以在純粹的 JS 環(huán)境下運行。接下來的文章就分析下各種規(guī)范。其實也就是前端模塊化的幾種規(guī)范。
前端模塊化開發(fā)的價值
隨著互聯(lián)網(wǎng)的飛速發(fā)展,前端開發(fā)越來越復雜。本文將從實際項目中遇到的問題出發(fā),講述模塊化能解決哪些問題,以及以 Sea.js 為例講解如何進行前端的模塊化開發(fā)。
惱人的命名沖突
我們從一個簡單的習慣出發(fā)。我做項目時,常常會將一些通用的、底層的功能抽象出來,獨立成一個個函數(shù),比如
function each(arr) { // 實現(xiàn)代碼 } function log(str) { // 實現(xiàn)代碼 }
并像模像樣的將這些代碼抽取出來并統(tǒng)一到util.js中,在需要使用的地方引入該文件,看起來很棒,團隊內的同事很感激我提供了這么便利的工具包。
直到團隊越來越大,開始有人抱怨
小楊:我定義了一個 each 方法遍歷對象,但是 util.js 中已經(jīng)存在一個 each 方法,每次都需要改方法名,我只能叫 eachObject 方法。<br>張三:我定義了一個 log 方法,可是王武的代碼出問題了,誰來看看?
抱怨越來越多,最后參照 Java 的方式,引入命名空間解決問題。于是 util.js 代碼變成了
var org = {}; org.Utils = {}; org.Utils.each = function (arr) { // 實現(xiàn)代碼 }; org.Utils.log = function (str) { // 實現(xiàn)代碼 };
可能看上去的代碼很 low,其實命名空間在前端領域的布道者是 Yahoo!的 YUI2 項目,看看下面的代碼,是 Yahoo!的一個開源項目
if (org.cometd.Utils.isString(response)) { return org.cometd.JSON.fromJSON(response); } if (org.cometd.Utils.isArray(response)) { return response; }
通過命名空間雖然可以極大的解決沖突問題,但是每次在調用一個方法時都需要寫一大堆命名空間相關的代碼,剝奪了編碼樂趣。
另一種方式是一個自執(zhí)行函數(shù)來實現(xiàn)。
(function (args) { //... })(this);
繁瑣的文件依賴
繼續(xù)上述場景,很多情況下都需要開發(fā) UI 層通用組件,這樣項目組就不需要重復造輪子。其中有一個高頻使用的組件就是 dialog.js
<script src="util.js"></script> <script src="dialog.js"></script> <script> org.Dialog.init({ /* 傳入配置 */ }); </script>
雖然公共組做項目都會編寫使用文檔、發(fā)送郵件告知全員(項目地址、使用方式等),但是還是有人問「為什么 dialog.js 有問題」,最后排查的結果基本都是沒有引入 util.js
<script src="dialog.js"></script> <script> org.Dialog.init({ /* 傳入配置 */ }); </script>
命名沖突和文件依賴是前端開發(fā)中2個經(jīng)典問題,經(jīng)過開發(fā)者不斷的思考和研究,誕生了模塊化的解決方案,以 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.模塊的版本管理:通過別名等配置,配合構建工具,可以輕松實現(xiàn)模塊的版本管理
2.提高可維護性: 模塊化可以實現(xiàn)每個文件的職責單一,非常有利于代碼的維護。
3.前端性能優(yōu)化: 對于前端開發(fā)來說,異步加載模塊對于頁面性能非常有益。
4.跨環(huán)境共享模塊: CMD 模塊定義規(guī)范與 NodeJS 的模塊規(guī)范非常相近,所以通過 Sea.JS 的 NodeJS 版本,可以方便的實現(xiàn)模塊的跨服務器和瀏覽器共享。
CommonJS 規(guī)范
CommonJS 是服務器端模塊的規(guī)范。NodeJS 采用了這個規(guī)范。CommonJS 加載模塊是同步的,所以只有加載完成后才能執(zhí)行后面的操作。
因為服務器的特點,加載的模塊文件一般都存在在本地硬盤,所以加載起來比較快,不用考慮異步的方式。
CommonJS 模塊化的餓規(guī)范中,每個文件都是一個模塊,擁有獨立的作用域、變量、以及方法等,對其他模塊不可見。 CommonJS 規(guī)范規(guī)定,每個模塊內部,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 引擎對腳本進行靜態(tài)分析的時候,遇到模塊加載命令 import,就會生成一個只讀引用,等到腳本真正執(zhí)行時,再根據(jù)這個只讀引用到被加載的模塊中取值,
AMD 規(guī)范
AMD(Asynchronous Module Definition) 是在 Require.JS 推廣的過程中對模塊定義的規(guī)范化產(chǎn)出。AMD 推崇依賴前置。它是 CommonJS 模塊化規(guī)范的超集,作用在瀏覽器上。它的特點是異步,利用了瀏覽器的并發(fā)能力,讓模塊的依賴阻塞變少。
AMD 的 API
define(id?, dependencyies?, factory);
id 是模塊的名字,是可選參數(shù)。 dependencies 指定了該模塊所依賴的模塊列表,是一個數(shù)組,也是可選參數(shù)。每個依賴的模塊的輸出都將作為參數(shù)依次傳入 factory 中。
require([module], callback)
AMD 規(guī)范允許輸出模塊兼容 CommonJS 規(guī)范,這時 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 }; });
優(yōu)點: 適合在瀏覽器環(huán)境中加載模塊,可以實現(xiàn)并行加載多個模塊
缺點: 提高了開發(fā)成本,并不能按需加載,而是提前加載所有的依賴
CMD 規(guī)范
CMD 是 Sea.JS 推廣的過程中對模塊定義的規(guī)范化產(chǎn)出。CMD 推崇依賴就近。
CMD 規(guī)范盡量保持簡單,并與 CommonJS 規(guī)范中的 Module 保持兼容,通過 CMD 規(guī)范編寫的模塊,可以在 NodeJS 中運行。
CMD 模塊定義規(guī)范
CMD 中 require 依賴的描述用數(shù)組,則是異步加載,如果是單個依賴使用字符串,則是同步加載。
AMD 是 RequireJS 在推廣過程中對模塊定義的規(guī)范化產(chǎn)出,CMD是SeaJS 在推廣過程中被廣泛認知。SeaJS 出自國內螞蟻金服玉伯。二者的區(qū)別,玉伯在12年如是說:
RequireJS 和 SeaJS 都是很不錯的模塊加載器,兩者區(qū)別如下:
- 兩者定位有差異。RequireJS 想成為瀏覽器端的模塊加載器,同時也想成為 Rhino / Node 等環(huán)境的模塊加載器。SeaJS 則專注于 Web 瀏覽器端,同時通過 Node 擴展的方式可以很方便跑在 Node 服務器端
- 兩者遵循的標準有差異。RequireJS 遵循的是 AMD(異步模塊定義)規(guī)范,SeaJS 遵循的是 CMD (通用模塊定義)規(guī)范。規(guī)范的不同,導致了兩者API 的不同。SeaJS 更簡潔優(yōu)雅,更貼近 CommonJS Modules/1.1 和 Node Modules 規(guī)范。
- 兩者社區(qū)理念有差異。RequireJS 在嘗試讓第三方類庫修改自身來支持 RequireJS,目前只有少數(shù)社區(qū)采納。SeaJS 不強推,而采用自主封裝的方式來“海納百川”,目前已有較成熟的封裝策略。
- 兩者代碼質量有差異。RequireJS 是沒有明顯的 bug,SeaJS 是明顯沒有 bug。
- 兩者對調試等的支持有差異。SeaJS 通過插件,可以實現(xiàn) Fiddler 中自動映射的功能,還可以實現(xiàn)自動 combo 等功能,非常方便便捷。RequireJS無這方面的支持。
- 兩者的插件機制有差異。RequireJS 采取的是在源碼中預留接口的形式,源碼中留有為插件而寫的代碼。SeaJS 采取的插件機制則與 Node 的方式一致開放自身,讓插件開發(fā)者可直接訪問或修改,從而非常靈活,可以實現(xiàn)各種類型的插件。
UMD 規(guī)范
UMD(Universal Module Definition)是隨著大前端的趨勢產(chǎn)生,希望提供一個前后端跨平臺的解決方案(支持 AMD、CMD、CommonJS 模塊方式)。
實現(xiàn)原理:
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 規(guī)范,使用 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 執(zhí)行環(huán)境中。
借助webpack可以方便的打出一個 umd 規(guī)范的包。
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 引擎在執(zhí)行 JS 代碼時,是將代碼先以 MacroAssembler 匯編庫在內存中先編譯成機器碼再送往 CPU 執(zhí)行的,并不是像其它 JS 引擎那樣解析一行執(zhí)行一行。所以,靜態(tài)加載的 ES6 模塊規(guī)范,更有助于 v8 引擎發(fā)揮價值。而運行時加載的 CommonJS、AMD、CMD 規(guī)范等,均不利于 v8 引擎施展拳腳。
在 NodeJS 開發(fā)項目中,Node9 已經(jīng)支持 ES6語法,完全可以使用 ES6 模塊規(guī)范。NodeJS 的誕生,本身就基于 Google 的 v8 引擎,沒有理由不考慮發(fā)揮 v8 的最大潛能。
在瀏覽器 JS 開發(fā)項目中,因為從服務器加載文件需要時間,使用 CommonJS 規(guī)范肯定是不合適了。至于是使用原生的 ES 模塊規(guī)范,還是使用 Sea.js,要看具體場景。如果想頁面盡快加載,Sea.js 適合;如果是單頁面網(wǎng)站,適合使用原生的 ES6 模塊規(guī)范。還有一點,瀏覽器并非只有 Chrome 一家,對于沒有使用 v8 引擎的瀏覽器,使用 ES6 原生規(guī)范的優(yōu)勢就又減少了一點。
以上就是淺談JS前端模塊化的幾種規(guī)范的詳細內容,更多關于JS前端模塊化的資料請關注腳本之家其它相關文章!
相關文章
nodejs不用electron實現(xiàn)打開文件資源管理器并選擇文件
最近在開發(fā)一些小腳本,用 nodejs 實現(xiàn),其中很多功能需要選擇一個/多個文件,或者是選擇一個文件夾,這種情況下網(wǎng)上給出的解決方案都是 electron,但是我一個小腳本用 electron 屬實有點夸張了,后來轉念一想可以通過 powershell 來實現(xiàn)類似的功能,需要的朋友可以參考下2024-01-01Windows安裝Node.js報錯:2503、2502的解決方法
這篇文章主要給大家介紹了關于在Windows系統(tǒng)下安裝Node.js報錯:2503、2502的解決方法,文中將解決的方法一步步介紹的非常詳細,需要的朋友可以參考借鑒,下面隨著小編來一起學習學習吧。2017-10-10Node.js中的npm單獨與批量升級依賴包的方式超詳細講解
npm outdated僅檢查所有已安裝包的依賴關系,并將當前版本遠程倉庫中的最新版本進行對比,不會升級,這篇文章主要介紹了Node.js中的npm單獨與批量升級依賴包的方式超詳細講解,需要的朋友可以參考下2024-02-02解決Window10系統(tǒng)下Node安裝報錯的問題分析
今天電腦重裝了win10系統(tǒng),在安裝Node的過程中出現(xiàn)了下面的問題,下面就和大家分享下用來解決這種問題的小方法2016-12-12Ubuntu中搭建Nodejs開發(fā)環(huán)境過程分享
這篇文章主要介紹了Ubuntu中搭建Nodejs開發(fā)環(huán)境過程,比較郁悶的是apt-get安裝失敗了,如果有遇到一樣問題的朋友,可以參考一下本文2014-06-06詳解如何在vscode里面調試js和node.js的方法步驟
這篇文章主要介紹了詳解如何在vscode里面調試js和node.js的方法步驟,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-12-12Node.js一行代碼實現(xiàn)靜態(tài)文件服務器的方法步驟
這篇文章主要介紹了Node.js一行代碼實現(xiàn)靜態(tài)文件服務器的方法步驟,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-05-05