再談 JS中的模組規範(CommonJS,AMD,CMD)來自玉伯的seajs分析
隨著網際網路的飛速發展,前端開發越來越複雜。本文將從實際專案中遇到的問題出發,講述模組化能解決哪些問題,以及如何使用 Sea.js 進行前端的模組化開發。
惱人的命名衝突
我們從一個簡單的習慣出發。我做專案時,常常會將一些通用的、底層的功能抽象出來,獨立成一個個函式,比如
function each(arr) { // 實現程式碼}function log(str) { // 實現程式碼}
並像模像樣地把這些函式統一放在 util.js 裡。需要用到時,引入該檔案就行。這一切工作得很好,同事也很感激我提供了這麼便利的工具包。
直到團隊越來越大,開始有人抱怨。
小楊:我想定義一個 each 方法遍歷物件,但頁頭的 util.js 裡已經定義了一個,我的只能叫 eachObject 了,好無奈。
小高:我自定義了一個 log 方法,為什麼小明寫的程式碼就出問題了呢?誰來幫幫我。
抱怨越來越多。團隊經過一番激烈的討論,決定參照 Java 的方式,引入名稱空間來解決。於是 util.js 裡的程式碼變成了
var org = {};org.CoolSite = {};org.CoolSite.Utils = {};org.CoolSite.Utils.each = function (arr) { // 實現程式碼};org.CoolSite.Utils.log = function (str) { // 實現程式碼};
不要認為上面的程式碼是為了寫這篇文章而故意捏造的。將名稱空間的概念在前端中發揚光大,首推 Yahoo! 的 YUI2 專案。下面是一段真實程式碼,來自 Yahoo! 的一個開源專案。
if (org.cometd.Utils.isString(response)) { return org.cometd.JSON.fromJSON(response);}if (org.cometd.Utils.isArray(response)) { return response;}
通過名稱空間,的確能極大緩解衝突。但每每看到上面的程式碼,都忍不住充滿同情。為了呼叫一個簡單的方法,需要記住如此長的名稱空間,這增加了記憶負擔,同時剝奪了不少編碼的樂趣。
作為前端業界的標杆,YUI 團隊下定決心解決這一問題。在 YUI3 專案中,引入了一種新的名稱空間機制。
YUI().use('node', function (Y) { // Node 模組已載入好 // 下面可以通過 Y 來呼叫 var foo = Y.one('#foo');});
YUI3 通過沙箱機制,很好的解決了名稱空間過長的問題。然而,也帶來了新問題。
YUI().use('a', 'b', function (Y) { Y.foo(); // foo 方法究竟是模組 a 還是 b 提供的? // 如果模組 a 和 b 都提供 foo 方法,如何避免衝突?});
看似簡單的命名衝突,實際解決起來並不簡單。如何更優雅地解決?我們按下暫且不表,先來看另一個常見問題。
煩瑣的檔案依賴
繼續上面的故事。基於 util.js,我開始開發 UI 層通用元件,這樣專案組同事就不用重複造輪子了。
其中有一個最被大家喜歡的元件是 dialog.js,使用方式很簡單。
<script src="util.js"></script><script src="dialog.js"></script><script> org.CoolSite.Dialog.init({ /* 傳入配置 */ });</script>
可是無論我怎麼寫文件,以及多麼鄭重地發郵件宣告,時不時總會有同事來詢問為什麼 dialog.js 有問題。通過一番排查,發現導致錯誤的原因經常是
<script src="dialog.js"></script><script> org.CoolSite.Dialog.init({ /* 傳入配置 */ });</script>
在 dialog.js 前沒有引入 util.js,因此 dialog.js 無法正常工作。同樣不要以為我上面的故事是虛構的,在我待過的公司裡,至今依舊有類似的指令碼報錯,特別是在各種快速製作的營銷頁面中。
上面的檔案依賴還在可控範圍內。當專案越來越複雜,眾多檔案之間的依賴經常會讓人抓狂。下面這些問題,我相信每天都在真實地發生著。
- 通用組更新了前端基礎類庫,卻很難推動全站升級。
- 業務組想用某個新的通用元件,但發現無法簡單通過幾行程式碼搞定。
- 一個老產品要上新功能,最後評估只能基於老的類庫繼續開發。
- 公司整合業務,某兩個產品線要合併。結果發現前端程式碼衝突。
- ……
以上很多問題都是因為檔案依賴沒有很好的管理起來。在前端頁面裡,大部分指令碼的依賴目前依舊是通過人肉的方式保證。當團隊比較小時,這不會有什麼問題。當團隊越來越大,公司業務越來越複雜後,依賴問題如果不解決,就會成為大問題。
檔案的依賴,目前在絕大部分類庫框架裡,比如國外的 YUI3 框架、國內的 KISSY 等類庫,目前是通過配置的方式來解決。
YUI.add('my-module', function (Y) { // ...}, '0.0.1', { requires: ['node', 'event']});
上面的程式碼,通過 requires
等方式來指定當前模組的依賴。這很大程度上可以解決依賴問題,但不夠優雅。當模組很多,依賴很複雜時,煩瑣的配置會帶來不少隱患。
命名衝突和檔案依賴,是前端開發過程中的兩個經典問題。下來我們看如何通過模組化開發來解決。為了方便描述,我們使用 Sea.js 來作為模組化開發框架。
使用 Sea.js 來解決
Sea.js 是一個成熟的開源專案,核心目標是給前端開發提供簡單、極致的模組化開發體驗。這裡不多做介紹,有興趣的可以訪問 seajs.org 檢視官方文件。
使用 Sea.js,在書寫檔案時,需要遵守 CMD (Common Module Definition)模組定義規範。一個檔案就是一個模組。前面例子中的 util.js 變成
define(function(require, exports) { exports.each = function (arr) { // 實現程式碼 }; exports.log = function (str) { // 實現程式碼 };});
通過 exports
就可以向外提供介面。這樣,dialog.js 的程式碼變成
define(function(require, exports) { var util = require('./util.js'); exports.init = function() { // 實現程式碼 };});
關鍵部分到了!我們通過 require('./util.js')
就可以拿到 util.js 中通過 exports
暴露的介面。這裡的require 可以認為是 Sea.js 給 JavaScript 語言增加的一個 語法關鍵字,通過 require
可以獲取其他模組提供的介面。
這其實一點也不神奇。作為前端工程師,對 CSS 程式碼一定也不陌生。
@import url("base.css");#id { ... }.class { ... }
Sea.js 增加的 require
語法關鍵字,就如 CSS 檔案中的 @import
一樣,給我們的原始碼賦予了依賴引入功能。
如果你是後端開發工程師,更不會陌生。Java、Python、C# 等等語言,都有 include
、import
等功能。JavaScript 語言本身也有類似功能,但目前還處於草案階段,需要等到 ES6 標準得到主流瀏覽器支援後才能使用。
這樣,在頁面中使用 dialog.js 將變得非常簡單。
<script src="sea.js"></script><script>seajs.use('dialog', function(Dialog) { Dialog.init(/* 傳入配置 */);});</script>
首先要在頁面中引入 sea.js 檔案,這一般通過頁頭全域性把控,也方便更新維護。想在頁面中使用某個元件時,只要通過 seajs.use
方法呼叫。
好好琢磨以上程式碼,我相信你已經看到了 Sea.js 帶來的兩大好處:
通過
exports
暴露介面。這意味著不需要名稱空間了,更不需要全域性變數。這是一種徹底的命名衝突解決方案。通過
require
引入依賴。這可以讓依賴內建,開發者只需關心當前模組的依賴,其他事情 Sea.js 都會自動處理好。對模組開發者來說,這是一種很好的 關注度分離,能讓程式設計師更多地享受編碼的樂趣。
小結
除了解決命名衝突和依賴管理,使用 Sea.js 進行模組化開發還可以帶來很多好處:
模組的版本管理。通過別名等配置,配合構建工具,可以比較輕鬆地實現模組的版本管理。
提高可維護性。模組化可以讓每個檔案的職責單一,非常有利於程式碼的維護。Sea.js 還提供了 nocache、debug 等外掛,擁有線上除錯等功能,能比較明顯地提升效率。
前端效能優化。Sea.js 通過非同步載入模組,這對頁面效能非常有益。Sea.js 還提供了 combo、flush 等外掛,配合服務端,可以很好地對頁面效能進行調優。
跨環境共享模組。CMD 模組定義規範與 Node.js 的模組規範非常相近。通過 Sea.js 的 Node.js 版本,可以很方便實現模組的跨伺服器和瀏覽器共享。
模組化開發並不是新鮮事物,但在 Web 領域,前端開發是新生崗位,一直處於比較原始的刀耕火種時代。直到最近兩三年,隨著 Dojo、YUI3、Node.js 等社群的推廣和流行,前端的模組化開發理念才逐步深入人心。
前端的模組化構建可分為兩大類。一類是以 Dojo、YUI3、國內的 KISSY 等類庫為代表的大教堂模式。在大教堂模式下,所有元件都是顆粒化、模組化的,各元件之間層層分級、環環相扣。另一類是以 jQuery、RequireJS、國內的 Sea.js、OzJS 等類庫為基礎的集市模式。在集市模式下,所有元件彼此獨立、職責單一,各元件通過組合鬆耦合在一起,協同完成開發。
這兩類模組化構建方式各有應用場景。從長遠來看,小而美更具備寬容性和競爭力,更能形成有活力的生態圈。
總之,模組化能給前端開發帶來很多好處。如果你還沒有嘗試,不妨從試用 Sea.js 開始。
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------模組話的原理和必要性: 來自某博主的分享和總結
如果你聽過js模組化這個東西,那麼你就應該聽過或CommonJS或AMD甚至是CMD這些規範咯,我也聽過,但之前也真的是聽聽而已。
現在就看看吧,這些規範到底是啥東西,幹嘛的。
一、CommonJS
CommonJS就是為JS的表現來制定規範,因為js沒有模組的功能所以CommonJS應運而生,它希望js可以在任何地方執行,不只是瀏覽器中。
CommonJS能有一定的影響力,我覺得絕對離不開Node的人氣,不過喔,Node,CommonJS,瀏覽器甚至是W3C之間有什麼關係呢,我找到了個貼切的圖:
|---------------瀏覽器----- ------------------| |--------------------------CommonJS----------------------------------|
| BOM | | DOM | | ECMAScript | | FS | | TCP | | Stream | | Buffer | |........|
|-------W3C-----------| |---------------------------------------Node--------------------------------------------------|
CommonJS定義的模組分為:{模組引用(require)} {模組定義(exports)} {模組標識(module)}
require()用來引入外部模組;exports物件用於匯出當前模組的方法或變數,唯一的匯出口;module物件就代表模組本身。
比如說我們就可以這樣用了:
1 //sum.js2 exports.sum = function(){...做加操作..};3 4 //calculate.js5 var math = require('sum');6 exports.add = function(n){7 return math.sum(val,n);8 };
雖說Node遵循CommonJS的規範,但是相比也是做了一些取捨,填了一些新東西的。
不過,說了CommonJS也說了Node,那麼我覺得也得先了解下NPM了。NPM作為Node的包管理器,不是為了幫助Node解決依賴包的安裝問題嘛,那它肯定也要遵循CommonJS規範啦,它遵循包規範(還是理論)的。
CommonJS WIKI講了它的歷史,還介紹了modules和packages等。
二、AMD
CommonJS是主要為了JS在後端的表現制定的,他是不適合前端的,為什麼這麼說呢?
這需要分析一下瀏覽器端的js和伺服器端js都主要做了哪些事,有什麼不同了:
---------------------------------------伺服器端JS | 瀏覽器端JS-------------------------------------------
相同的程式碼需要多次執行 | 程式碼需要從一個伺服器端分發到多個客戶端執行
CPU和記憶體資源是瓶頸 | 頻寬是瓶頸
載入時從磁碟中載入 | 載入時需要通過網路載入
---------------------------------------------------------------------------------------------------------------
於是乎,AMD(非同步模組定義)出現了,它就主要為前端JS的表現制定規範。
AMD就只有一個介面:define(id?,dependencies?,factory);
它要在宣告模組的時候制定所有的依賴(dep),並且還要當做形參傳到factory中,像這樣:
1 define(['dep1','dep2'],function(dep1,dep2){...});
要是沒什麼依賴,就定義簡單的模組,下面這樣就可以啦:
1 define(function(){2 var exports = {};3 exports.method = function(){...};4 return exports;5 });
咦,這裡有define,把東西包裝起來啦,那Node實現中怎麼沒看到有define關鍵字呢,它也要把東西包裝起來呀,其實吧,只是Node隱式包裝了而已.....
RequireJS就是實現了AMD規範的呢。
這有AMD的WIKI中文版,講了很多蠻詳細的東西,用到的時候可以檢視:AMD的WIKI中文版
三、CMD
大名遠揚的玉伯寫了seajs,就是遵循他提出的CMD規範,與AMD蠻相近的,不過用起來感覺更加方便些,最重要的是中文版,應有盡有:seajs官方doc
1 define(function(require,exports,module){...});
用過seajs吧,這個不陌生吧,對吧。
前面說AMD,說RequireJS實現了AMD,CMD看起來與AMD好像呀,那RequireJS與SeaJS像不像呢?
雖然CMD與AMD蠻像的,但區別還是挺明顯的,官方非官方都有闡述和理解,我覺得吧,說的都挺好:
如轉載,敬請註明地址。
JS前端實用開發QQ群 :147250970 歡迎加入~!
入坑方式:
歡迎加入~!氣氛熱情,歡樂多,妹子多!
web前端 聚集地,匯聚了全國頂尖的web前端熱愛者,最新技術,最炫潮流,最靠譜的話題: 做好現在!技術只是為了改變生活!JS前端實用開發QQ群 :147250970
掃描螢幕下方的二維碼,可以關注 我的前端公眾號 。聽說妹子挺多的,及時更新一些前端解惑和段子