SeaJS由國內的牛人lifesinger開發。目前版本是1.1.1,源碼不到1500行,壓縮后才4k,質量極高。
這篇會講述SeaJS的一些基本用法,不會面面俱到,但會就個人的理解講述官方文檔沒有提到的一些細節。
一、SeaJS的全局接口
SeaJS向全局公開了兩個標識符: seajs 和 define。
如果你的項目中已經用了標識符seajs,又不想改。這時SeaJS可以讓出全局的seajs。如
var boot = seajs.noConflict();
這時boot就相當於先前的seajs。
如果你的項目中連標識符define也用到了,也不想改。SeaJS是很寬容的,它的define也可以讓出。如
var boot = seajs.noConflict(true);
較上面僅多傳了一個true。這時全局的define也沒了。這時需要用boot.define來代替之前的define。
用過jQuery的同學應該很熟悉$.noConflict方法,SeaJS的noConflict與之類似。
二、SeaJS的模塊寫法
SeaJS默認使用全局的define函數寫模塊(可把define當成語法關鍵字),define定義了三個形參id, deps, factory。
define(id?, deps?, factory);
這個define很容易讓你想起AMD的唯一API:define函數。 或者說讓人費解,導致搞不懂SeaJS和 RequireJS define的區別。
它們都有個全局的define,形參都是三個,且對應的形參名也一樣,會誤認為SeaJS也是AMD的實現。
事實上SeaJS和RequireJS的define前兩個參數的確一樣。
id都為字符串,都遵循 Module Identifiers。deps都是指依賴模塊,類型都為數組。區別僅在於第三個參數factory,雖然類型也都是函數,但factory的參數意義卻不同。
RequireJS中factory的參數有兩種情況
a、和deps(數組)元素一一對應。即deps有幾個,factory的實參就有幾個。
define(['a', 'b'], function(a, b){ // todo });
b、固定為require,exports, module(modules/wrappings格式)。
define(function(require, exports, module){ // todo });
這種方式是RequireJS后期向 Modules/Wrappings 的妥協,即兼容了它。而SeaJS的define僅支持RequireJS的第二種寫法:Modules/Wrappings。
注意:SeaJS遵循的是 Modules/Wrappings 和 Modules/1.1.1。這兩個規范中都沒有提到define關鍵字,Modules/Wrapping中要求定義模塊使用module.declare而非define。而恰恰只有AMD規范中有define的定義。即雖然SeaJS不是AMD的實現,但它卻采用了讓人極容易誤解的標識符define。
說了這么多,還沒開始寫一個模塊。下面我們從最簡單的開始
1、簡單模塊
define({ addEvent: function(el, type, fn){}, removeEvent: function(el, type, fn){}, fireEvent: function(el, type){} });
這樣就寫了一個事件模塊,這和寫一個單例沒有區別。更多的時候用該方式定義純數據模塊。它類似於
var E = { addEvent: function(el, type, fn){}, removeEvent: function(el, type, fn){}, fireEvent: function(el, type){} };
2、簡單的包裝模塊
define(function() { // 一些內部輔助函數 // ... function addEvent() { // .. } function removeEvent() { // .. } function fireEvent() { // .. } return { addEvent: addEvent, removeEvent: removeEvent, fireEvent: fireEvent }; });
您懂的,在這個匿名函數中可以做很多事情。最后只需公開必要的接口。它類似於
var E = function() { // 一些內部輔助函數 // ... function addEvent() { // .. } function removeEvent() { // .. } function fireEvent() { // .. } return { addEvent: addEvent, removeEvent: removeEvent, fireEvent: fireEvent }; }();
3、NodeJS風格的包裝模塊
上面兩種寫法看不到一絲NodeJS風格(Modules/1.1.1),改寫下與“方式2”等價的。
define(function(require, exports) { // 一些內部輔助函數 // ... function addEvent() { // .. } function removeEvent() { // .. } function fireEvent() { // .. } // 使用exports導出模塊接口,而非返回一個對象 exports.addEvent = addEvent; exports.addEvent = removeEvent; exports.addEvent = fireEvent; });
可以看到與“方式2”區別在於:
1:匿名函數有兩個參數require、exports。
2:導出接口不是return一個對象而是使用exports。
而exports不正是NodeJS的風格嗎? 細心的同學可能發現這個示例中require參數沒有用到,這正是下面要講的。
4、有依賴的模塊
SeaJS中“依賴”都需要使用require函數去獲取,雖然SeaJS的define的第二個參數deps也有“依賴”的意思,但它是提供打包工具(SPM)用的。此外,SeaJS的require是作為參數傳入匿名函數內的,RequireJS的require則是全局變量。
上面定義的是一個沒有依賴的模塊,以下是有依賴的模塊。
define(function(require, exports) { var cache = require('cache'); // ... exports.bind = bind; exports.unbind = unbind; exports.trigger = trigger; });
該事件模塊依賴於cache模塊,函數有兩個形參require和exports。拋開外層的匿名函數及define,它就是標准的NodeJS格式:使用require函數取依賴模塊,使用exports導出現有模塊接口。
實際上在SeaJS中具有依賴的模塊必須按“方式4”寫,即必須是包裝模塊,且匿名函數的第一個參數必須是標識符 “require”。即可以把require當初語法關鍵字來使用,雖然它不是全局的。
下面我們看看匿名函數的參數require和exports的一些有趣現象
a、如果寫的不是require,改成req會是什么結果。
define(function(req, exports) { var cache = req('cache'); // ... exports.bind = bind; exports.unbind = unbind; exports.trigger = trigger; });
Firebug網絡請求如下
會看到依賴的“cache”沒有被加載,當然JS肯定會報錯了。
b、只把匿名函數的形參改成req,函數內部仍然使用require。
define(function(req, exports) { var cache = require('cache'); // ... exports.bind = bind; exports.unbind = unbind; exports.trigger = trigger; });
看網絡請求
這次“cache”模塊竟然請求下來了。
仔細看上面的匿名函數代碼中require沒聲明,且形參是req而非require。那
var cache = require('cache');
中的require從何而來?
看SeaJS源碼可知,它的define函數中會取該匿名函數的toString,使用正則匹配解析出其中的“cache”(私有的parseDependencies函數)。
我們也看到,雖然cache請求下來了,卻仍然報錯,因為在執行階段require是未定義的。因此寫依賴模塊時匿名函數的第一個參數必須為require且不能更改。
正因為使用factory.toString和正則解析依賴,因此require的參數不能是表達式,如
// require的參數不能是表達式運算 require("ui-" + "dialog");
也不能使用require的別名,如
// 不能將require賦值給另外一個變量 var req = require; req("ui-dialog");
另據說Opera移動版不支持function的toString,這樣SeaJS無法在該版本中使用了(未測)。
c、修改exports為expo
define(function(require, expo) { var cache = require('cache'); // ... expo.bind = bind; expo.unbind = unbind; expo.trigger = trigger; });
運行是沒有問題的。即第二個參數“exports”是可以自定義的。顯然SeaJS不贊成改“exports”為其它,這樣明顯破壞了NodeJS風格(Modules/1.1.1)的模塊規范---它們正是使用“exports”導出模塊接口。但這點在SeaJS中卻無法被強制執行,只能是人為約定。
5、混合寫法的模塊
上面已經介紹了各種情形中的模塊寫法。為了與NodeJS風格保持一致:使用require獲取“依賴”,使用exports導出“接口”。SeaJS在獲取依賴這一塊做了限制,即必須使用require。但導出則不一定非得使用exports,即exports可以改為其它。甚至還可以直接使用 “返回值”。
define(function(require) { var cache = require('cache'); // ... // 使用返回值導出接口 return { bind: function() {}, unbind: function() {}, fire: function() {} }; });
我們知道在NodeJS中模塊只能是一個對象。即總是往exports上掛方法。SeaJS中如果使用exports導出接口,那么也一樣,模塊也只能是JS對象。如果使用“返回值”導出接口的話,那么模塊可以是任意的JS類型。如下將返回一個函數類型的模塊。
define(function(require) { var cache = require('cache'); function ad() { //... } // 函數類型的模塊 return ad; });
三、SeaJS的加載方式
雖然它提供各種方式(同步異步)加載,最簡單的莫過於直接在頁面中寫script標簽。引入SeaJS后,入門多數時候就是seajs.use方法。
seajs.use有兩個參數,第一個參數可以為字符串(模塊名)或數組(多個模塊)。第二個參數是回調函數。模塊加載后的回調。回調函數的參數與第一個參數一一對應。
seajs.use('dom', function(dom) { // todo with dom });
如下將在回調函數中使用dom模塊。當然它也提供了快捷方式data-main(同RequireJS)。
下面的demo中寫了一個完整的事件模塊event,它依賴於cache模塊。