前言
SeaJS是一個遵循CommonJS規范的JavaScript模塊加載框架,可以實現JavaScript的模塊化開發及加載機制。與jQuery等JavaScript框架不同,SeaJS不會擴展封裝語言特性,而只是實現JavaScript的模塊化及按模塊加載。SeaJS的主要目的是令JavaScript開發模塊化並可以輕松愉悅進行加載,將前端工程師從繁重的JavaScript文件及對象依賴處理中解放出來,可以專注於代碼本身的邏輯。SeaJS可以與jQuery這類框架完美集成。使用SeaJS可以提高JavaScript代碼的可讀性和清晰度,解決目前JavaScript編程中普遍存在的依賴關系混亂和代碼糾纏等問題,方便代碼的編寫和維護。
SeaJS的作者是淘寶前端工程師玉伯。
SeaJS本身遵循KISS(Keep It Simple, Stupid)理念進行開發,其本身僅有個位數的API,因此學習起來毫無壓力。在學習SeaJS的過程中,處處能感受到KISS原則的精髓——僅做一件事,做好一件事。
本文首先通過一個例子直觀對比傳統JavaScript編程和使用SeaJS的模塊化JavaScript編程,然后詳細討論SeaJS的使用方法,最后給出一些與SeaJS相關的資料。
傳統模式 vs SeaJS模塊化
假設我們現在正在開發一個Web應用TinyApp,我們決定在TinyApp中使用jQuery框架。TinyApp的首頁會用到module1.js,module1.js依賴module2.js和module3.js,同時module3.js依賴module4.js。
傳統開發
使用傳統的開發方法,各個js文件代碼如下:
//module1.jsvar module1 ={ run:function(){return $.merge(['module1'], $.merge(module2.run(), module3.run()));}}//module2.jsvar module2 ={ run:function(){return['module2'];}}//module3.jsvar module3 ={ run:function(){return $.merge(['module3'], module4.run());}}//module4.jsvar module4 ={ run:function(){return['module4'];}}
此時index.html需要引用module1.js及其所有下層依賴(注意順序):
<!DOCTYPE HTML><htmllang="zh-CN"><head><metacharset="UTF-8"><title>TinyApp</title><scriptsrc="./jquery-min.js"></script><scriptsrc="./module4.js"></script><scriptsrc="./module2.js"></script><scriptsrc="./module3.js"></script><scriptsrc="./module1.js"></script></head><body><pclass="content"></p><script> $('.content').html(module1.run());</script></body></html>
隨着項目的進行,js文件會越來越多,依賴關系也會越來越復雜,使得js代碼和html里的script列表往往變得難以維護。
SeaJS模塊化開發
下面看看如何使用SeaJS實現相同的功能。
首先是index.html:
<!DOCTYPE HTML><htmllang="zh-CN"><head><metacharset="UTF-8"><title>TinyApp</title></head><body><pclass="content"></p><scriptsrc="./sea.js"></script><script> seajs.use('./init',function(init){ init.initPage();});</script></body></html>
可以看到html頁面不再需要引入所有依賴的js文件,而只是引入一個sea.js,sea.js會處理所有依賴,加載相應的js文件,加載策略可以選擇在渲染頁面時一次性加載所有js文件,也可以按需加載(用到時才加載響應js),具體加載策略使用方法下文討論。
index.html加載了init模塊,並使用此模塊的initPage方法初始化頁面數據,這里先不討論代碼細節。
下面看一下模塊化后JavaScript的寫法:
//jquery.js define(function(require, exports,module)={//原jquery.js代碼...module.exports = $.noConflict(true);});//init.js define(function(require, exports,module)={var $ =require('jquery');var m1 =require('module1'); exports.initPage =function(){ $('.content').html(m1.run());}});//module1.js define(function(require, exports,module)={var $ =require('jquery');var m2 =require('module2');var m3 =require('module3'); exports.run =function(){return $.merge(['module1'], $.merge(m2.run(), m3.run()));}});//module2.js define(function(require, exports,module)={ exports.run =function(){return['module2'];}});//module3.js define(function(require, exports,module)={var $ =require('jquery');var m4 =require('module4'); exports.run =function(){return $.merge(['module3'], m4.run());}});//module4.js define(function(require, exports,module)={ exports.run =function(){return['module4'];}});
乍看之下代碼似乎變多變復雜了,這是因為這個例子太簡單,如果是大型項目,SeaJS代碼的優勢就會顯現出來。不過從這里我們還是能窺探到一些SeaJS的特性:
一是html頁面不用再維護冗長的script標簽列表,只要引入一個sea.js即可。
二是js代碼以模塊進行組織,各個模塊通過require引入自己依賴的模塊,代碼清晰明了。
通過這個例子朋友們應該對SeaJS有了一個直觀的印象,下面本文具體討論SeaJS的使用。
使用SeaJS
下載及安裝
要在項目中使用SeaJS,你所有需要做的准備工作就是下載sea.js然后放到你項目的某個位置。
SeaJS項目目前托管在GitHub上,主頁為 https://github.com/seajs/seajs/ 。可以到其git庫的build目錄下下載sea.js(已壓縮)或sea-debug.js(未壓縮)。
下載完成后放到項目的相應位置,然后在頁面中通過<script>標簽引入,你就可以使用SeaJS了。
SeaJS基本開發原則
在討論SeaJS的具體使用前,先介紹一下SeaJS的模塊化理念和開發原則。
使用SeaJS開發JavaScript的基本原則就是:一切皆為模塊。引入SeaJS后,編寫JavaScript代碼就變成了編寫一個又一個模塊,SeaJS中模塊的概念有點類似於面向對象中的類——模塊可以擁有數據和方法,數據和方法可以定義為公共或私有,公共數據和方法可以供別的模塊調用。
另外,每個模塊應該都定義在一個單獨js文件中,即一個對應一個模塊。
下面介紹模塊的編寫和調用。
模塊的定義及編寫
模塊定義函數define
SeaJS中使用“define”函數定義一個模塊。因為SeaJS的文檔並沒有關於define的完整參考,所以我閱讀了SeaJS源代碼,發現define可以接收三個參數:
/** * Defines a module. * @param {string=} id The module id. * @param {Array.|string=} deps The module dependencies. * @param {function()|Object} factory The module factory function. */ fn.define =function(id, deps, factory){//code of function…}
上面是我從SeaJS源碼中摘錄出來的,define可以接收的參數分別是模塊ID,依賴模塊數組及工廠函數。我閱讀源代碼后發現define對於不同參數個數的解析規則如下:
如果只有一個參數,則賦值給factory。
如果有兩個參數,第二個賦值給factory;第一個如果是array則賦值給deps,否則賦值給id。
如果有三個參數,則分別賦值給id,deps和factory。
但是,包括SeaJS的官方示例在內幾乎所有用到define的地方都只傳遞一個工廠函數進去,類似與如下代碼:
define(function(require, exports,module){//code of the module...});
個人建議遵循SeaJS官方示例的標准,用一個參數的define定義模塊。那么id和deps會怎么處理呢?
id是一個模塊的標識字符串,define只有一個參數時,id會被默認賦值為此js文件的絕對路徑。如example.com下的a.js文件中使用define定義模塊,則這個模塊的ID會賦值為 http://example.com/a.js ,沒有特別的必要建議不要傳入id。deps一般也不需要傳入,需要用到的模塊用require加載即可。
工廠函數factory解析
工廠函數是模塊的主體和重點。在只傳遞一個參數給define時(推薦寫法),這個參數就是工廠函數,此時工廠函數的三個參數分別是:
- require——模塊加載函數,用於記載依賴模塊。
- exports——接口點,將數據或方法定義在其上則將其暴露給外部調用。
- module——模塊的元數據。
這三個參數可以根據需要選擇是否需要顯示指定。
下面說一下module。module是一個對象,存儲了模塊的元信息,具體如下:
- module.id——模塊的ID。
- module.dependencies——一個數組,存儲了此模塊依賴的所有模塊的ID列表。
- module.exports——與exports指向同一個對象。
三種編寫模塊的模式
第一種定義模塊的模式是基於exports的模式:
define(function(require, exports,module){var a =require('a');//引入a模塊var b =require('b');//引入b模塊var data1 =1;//私有數據var func1 =function(){//私有方法return a.run(data1);} exports.data2 =2;//公共數據 exports.func2 =function(){//公共方法return'hello';}});
上面是一種比較“正宗”的模塊定義模式。除了將公共數據和方法附加在exports上,也可以直接返回一個對象表示模塊,如下面的代碼與上面的代碼功能相同:
define(function(require){var a =require('a');//引入a模塊var b =require('b');//引入b模塊var data1 =1;//私有數據var func1 =function(){//私有方法return a.run(data1);}return{ data2:2, func2:function(){return'hello';}};});
如果模塊定義沒有其它代碼,只返回一個對象,還可以有如下簡化寫法:
define({ data:1, func:function(){return'hello';}});
第三種方法對於定義純JSON數據的模塊非常合適。
模塊的載入和引用
模塊的尋址算法
上文說過一個模塊對應一個js文件,而載入模塊時一般都是提供一個字符串參數告訴載入函數需要的模塊,所以就需要有一套從字符串標識到實際模塊所在文件路徑的解析算法。SeaJS支持如下標識:
絕對地址——給出js文件的絕對路徑。
如
require("http://example/js/a");
就代表載入 http://example/js/a.js 。
相對地址——用相對調用載入函數所在js文件的相對地址尋找模塊。
例如在 http://example/js/b.js 中載入
require("./c");
則載入 http://example/js/c.js 。
基址地址——如果載入字符串標識既不是絕對路徑也不是以”./”開頭,則相對SeaJS全局配置中的“base”來尋址,這種方法稍后討論。
注意上面在載入模塊時都不用傳遞后綴名“.js”,SeaJS會自動添加“.js”。但是下面三種情況下不會添加:
載入css時,如
require("./module1-style.css");
路徑中含有”?”時,如
require(<a href="http://example/js/a.json?cb=func">http://example/js/a.json?cb=func</a>);
路徑以”#”結尾時,如
require("http://example/js/a.json#");
根據應用場景的不同,SeaJS提供了三個載入模塊的API,分別是seajs.use,require和require.async,下面分別介紹。
seajs.use
seajs.use主要用於載入入口模塊。入口模塊相當於C程序的main函數,同時也是整個模塊依賴樹的根。上面在TinyApp小例子中,init就是入口模塊。seajs.use用法如下:
//單一模式 seajs.use('./a');//回調模式 seajs.use('./a',function(a){ a.run();});//多模塊模式 seajs.use(['./a','./b'],function(a, b){ a.run(); b.run();});
一般seajs.use只用在頁面載入入口模塊,SeaJS會順着入口模塊解析所有依賴模塊並將它們加載。如果入口模塊只有一個,也可以通過給引入sea.js的script標簽加入”data-main”屬性來省略seajs.use,例如,上面TinyApp的index.html也可以改為如下寫法:
<!DOCTYPE HTML><htmllang="zh-CN"><head><metacharset="UTF-8"><title>TinyApp</title></head><body><pclass="content"></p><scriptsrc="./sea.js"data-main="./init"></script></body></html>
這種寫法會令html更加簡潔。
require
require是SeaJS主要的模塊加載方法,當在一個模塊中需要用到其它模塊時一般用require加載:
var m =require('/path/to/module/file');
這里簡要介紹一下SeaJS的自動加載機制。上文說過,使用SeaJS后html只要包含sea.js即可,那么其它js文件是如何加載進來的呢?SeaJS會首先下載入口模塊,然后順着入口模塊使用正則表達式匹配代碼中所有的require,再根據require中的文件路徑標識下載相應的js文件,對下載來的js文件再迭代進行類似操作。整個過程類似圖的遍歷操作(因為可能存在交叉循環依賴所以整個依賴數據結構是一個圖而不是樹)。
明白了上面這一點,下面的規則就很好理解了:
傳給require的路徑標識必須是字符串字面量,不能是表達式,如下面使用require的方法是錯誤的:
require('module'+'1');require('Module'.toLowerCase());
這都會造成SeaJS無法進行正確的正則匹配以下載相應的js文件。
require.async
上文說過SeaJS會在html頁面打開時通過靜態分析一次性記載所有需要的js文件,如果想要某個js文件在用到時才下載,可以使用require.async:
require.async('/path/to/module/file',function(m){//code of callback...});
這樣只有在用到這個模塊時,對應的js文件才會被下載,也就實現了JavaScript代碼的按需加載。
SeaJS的全局配置
SeaJS提供了一個seajs.config方法可以設置全局配置,接收一個表示全局配置的配置對象。具體使用方法如下:
seajs.config({base:'path/to/jslib/',alias:{'app':'path/to/app/'}, charset:'utf-8', timeout:20000, debug:false});
其中base表示基址尋址時的基址路徑。例如base設置為 http://example.com/js/3-party/ ,則
var $ =require('jquery');
會載入 http://example.com/js/3-party/jquery.js 。
alias可以對較長的常用路徑設置縮寫。
charset表示下載js時script標簽的charset屬性。
timeout表示下載文件的最大時長,以毫秒為單位。
debug表示是否工作在調試模式下。
SeaJS如何與現有JS庫配合使用
要將現有JS庫如jQuery與SeaJS一起使用,只需根據SeaJS的的模塊定義規則對現有庫進行一個封裝。例如,下面是對jQuery的封裝方法:
define(function(){//{{{jQuery原有代碼開始/*! * jQuery JavaScript Library v1.6.1 * http://jquery.com/ * * Copyright 2011, John Resig * Dual licensed under the MIT or GPL Version 2 licenses. * http://jquery.org/license * * Includes Sizzle.js * http://sizzlejs.com/ * Copyright 2011, The Dojo Foundation * Released under the MIT, BSD, and GPL Licenses. * * Date: Thu May 12 15:04:36 2011 -0400 *///...//}}}jQuery原有代碼結束return $.noConflict();});
SeaJS項目的打包部署
SeaJS本來集成了一個打包部署工具spm,后來作者為了更KISS一點,將spm拆出了SeaJS而成為了一個單獨的項目。spm的核心思想是將所有模塊的代碼都合並壓縮后並入入口模塊,由於SeaJS本身的特性,html不需要做任何改動就可以很方便的在開發環境和生產環境間切換。但是由於spm目前並沒有發布正式版本,所以本文不打算詳細介紹,有興趣的朋友可以參看其github項目主頁 https://github.com/seajs/spm/。
其實,由於每個項目所用的JS合並和壓縮工具不盡相同,所以spm可能並不是完全適合每個項目。在了解了SeaJS原理后,完全可以自己寫一個符合自己項目特征的合並打包腳本。
一個完整的例子
上文說了那么多,知識點比較分散,所以最后我打算用一個完整的SeaJS例子把這些知識點串起來,方便朋友們歸納回顧。這個例子包含如下文件:
- index.html——主頁面。
- sea.js——SeaJS腳本。
- init.js——init模塊,入口模塊,依賴data、jquery、style三個模塊。由主頁面載入。
- data.js——data模塊,純json數據模塊,由init載入。
- jquery.js——jquery模塊,對 jQuery庫的模塊化封裝,由init載入。
- style.css——CSS樣式表,作為style模塊由init載入。
- sea.js和jquery.js的代碼屬於庫代碼,就不贅述,這里只給出自己編寫的文件的代碼。
html:
<!DOCTYPE HTML><htmllang="zh-CN"><head><metacharset="UTF-8"><title></title></head><body><divid="content"><pclass="author"></p><pclass="blog"><ahref="#">Blog</a></p></div><scriptsrc="./sea.js"data-main="./init"></script></body></html>
javascript:
//init.js define(function(require, exports,module){var $ =require('./jquery');var data =require('./data');var css =require('./style.css'); $('.author').html(data.author); $('.blog').attr('href', data.blog);});//data.js define({ author:'ZhangYang', blog:'http://blog.codinglabs.org'});
css:
.author{color:red;font-size:10pt;}.blog{font-size:10pt;}
運行效果如下:
主要參考文獻&SeaJS學習資源
[1] SeaJS主頁 – http://seajs.org
[2] SeaJS的GitHub庫(可獲取源碼) – https://github.com/seajs/seajs
[3] SeaJS作者玉伯的博客 - http://lifesinger.wordpress.com/