1. 程式人生 > >理解JS中的模塊規範(CommonJS,AMD,CMD)

理解JS中的模塊規範(CommonJS,AMD,CMD)

site 繼續 arr 包管理器 color sea 文件依賴 避免 說我

  隨著互聯網的飛速發展,前端開發越來越復雜。本文將從實際項目中遇到的問題出發,講述模塊化能解決哪些問題,以及如何使用 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 無法正常工作。同樣不要以為我上面的故事是虛構的,在我待過的公司裏,至今依舊有類似的腳本報錯,特別是在各種快速制作的營銷頁面中。

  上面的文件依賴還在可控範圍內。當項目越來越復雜,眾多文件之間的依賴經常會讓人抓狂。下面這些問題,我相信每天都在真實地發生著。

  1. 通用組更新了前端基礎類庫,卻很難推動全站升級。
  2. 業務組想用某個新的通用組件,但發現無法簡單通過幾行代碼搞定。
  3. 一個老產品要上新功能,最後評估只能基於老的類庫繼續開發。
  4. 公司整合業務,某兩個產品線要合並。結果發現前端代碼沖突。
  5. ……

  以上很多問題都是因為文件依賴沒有很好的管理起來。在前端頁面裏,大部分腳本的依賴目前依舊是通過人肉的方式保證。當團隊比較小時,這不會有什麽問題。當團隊越來越大,公司業務越來越復雜後,依賴問題如果不解決,就會成為大問題。

  文件的依賴,目前在絕大部分類庫框架裏,比如國外的 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# 等等語言,都有 includeimport 等功能。JavaScript 語言本身也有類似功能,但目前還處於草案階段,需要等到 ES6 標準得到主流瀏覽器支持後才能使用。

  這樣,在頁面中使用 dialog.js 將變得非常簡單。

<script src="sea.js"></script>
<script>
seajs.use(dialog, function(Dialog) {
  Dialog.init(/* 傳入配置 */);
});
</script>

  首先要在頁面中引入 sea.js 文件,這一般通過頁頭全局把控,也方便更新維護。想在頁面中使用某個組件時,只要通過 seajs.use 方法調用。

  好好琢磨以上代碼,我相信你已經看到了 Sea.js 帶來的兩大好處:

1、通過 exports 暴露接口。這意味著不需要命名空間了,更不需要全局變量。這是一種徹底的命名沖突解決方案。

2、通過 require 引入依賴。這可以讓依賴內置,開發者只需關心當前模塊的依賴,其他事情 Sea.js 都會自動處理好。對模塊開發者來說,這是一種很好的關註度分離,能讓程序員更多地享受編碼的樂趣。

小結

  除了解決命名沖突和依賴管理,使用 Sea.js 進行模塊化開發還可以帶來很多好處:

1、模塊的版本管理。通過別名等配置,配合構建工具,可以比較輕松地實現模塊的版本管理。

2、提高可維護性。模塊化可以讓每個文件的職責單一,非常有利於代碼的維護。Sea.js 還提供了 nocache、debug 等插件,擁有在線調試等功能,能比較明顯地提升效率。

3、前端性能優化。Sea.js 通過異步加載模塊,這對頁面性能非常有益。Sea.js 還提供了 combo、flush 等插件,配合服務端,可以很好地對頁面性能進行調優。

4、跨環境共享模塊。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對象就代表模塊本身。

  比如說我們就可以這樣用了:

//sum.js
exports.sum = function(){...做加操作..};

//calculate.js
var math = require(sum);
exports.add = function(n){
    return math.sum(val,n);
};

  雖說Node遵循CommonJS的規範,但是相比也是做了一些取舍,填了一些新東西的。不過,說了CommonJS也說了Node,那麽我覺得也得先了解下NPM了。NPM作為Node的包管理器,不是為了幫助Node解決依賴包的安裝問題嘛,那它肯定也要遵循CommonJS規範啦,它遵循包規範(還是理論)的。

二、AMD

  CommonJS是主要為了JS在後端的表現制定的,他是不適合前端的,為什麽這麽說呢?

  這需要分析一下瀏覽器端的js和服務器端js都主要做了哪些事,有什麽不同了:

----------------------------------------------服務器端JS | 瀏覽器端JS--------------------------------------------------

相同的代碼需要多次執行 | 代碼需要從一個服務器端分發到多個客戶端執行

CPU和內存資源是瓶頸 | 帶寬是瓶頸

加載時從磁盤中加載 | 加載時需要通過網絡加載

---------------------------------------------------------------------------------------------------------------------------------------

  於是乎,AMD(異步模塊定義)出現了,它就主要為前端JS的表現制定規範。

  AMD就只有一個接口:define(id?,dependencies?,factory);

  它要在聲明模塊的時候制定所有的依賴(dep),並且還要當做形參傳到factory中,像這樣:

define([dep1,dep2],function(dep1,dep2){...});

  要是沒什麽依賴,就定義簡單的模塊,下面這樣就可以啦:

define(function(){
    var exports = {};
    exports.method = function(){...};
    return exports;
});

  這裏有define,把東西包裝起來啦,那Node實現中怎麽沒看到有define關鍵字呢,它也要把東西包裝起來呀,其實吧,只是Node隱式包裝了而已.....

  RequireJS就是實現了AMD規範的呢。

  這有AMD的WIKI中文版,講了很多蠻詳細的東西,用到的時候可以查看:AMD的WIKI中文版

三、CMD

  大名遠揚的玉伯寫了seajs,就是遵循他提出的CMD規範,與AMD蠻相近的,不過用起來感覺更加方便些,最重要的是中文版。

define(function(require,exports,module){...});

理解JS中的模塊規範(CommonJS,AMD,CMD)