SeaJS是一個遵循CommonJS規範的JavaScript模塊加載框架,能夠實現JavaScript的模塊化開發及加載機制。與jQuery等JavaScript框架不一樣,SeaJS不會擴展封裝語言特性,而只是實現JavaScript的模塊化及按模塊加載。SeaJS的主要目的是令JavaScript開發模塊化並能夠輕鬆愉悅進行加載,將前端工程師從繁重的JavaScript文件及對象依賴處理中解放出來,能夠專一於代碼自己的邏輯。SeaJS能夠與jQuery這類框架完美集成。使用SeaJS能夠提升JavaScript代碼的可讀性和清晰度,解決目前JavaScript編程中廣泛存在的依賴關係混亂和代碼糾纏等問題,方便代碼的編寫和維護。javascript
SeaJS的做者是淘寶前端工程師玉伯。css
SeaJS自己遵循KISS(Keep It Simple, Stupid)理念進行開發,其自己僅有個位數的API,所以學習起來毫無壓力。在學習SeaJS的過程當中,到處能感覺到KISS原則的精髓——僅作一件事,作好一件事。html
本文首先經過一個例子直觀對比傳統JavaScript編程和使用SeaJS的模塊化JavaScript編程,而後詳細討論SeaJS的使用方法,最後給出一些與SeaJS相關的資料。前端
假設咱們如今正在開發一個Web應用TinyApp,咱們決定在TinyApp中使用jQuery框架。TinyApp的首頁會用到module1.js,module1.js依賴module2.js和module3.js,同時module3.js依賴module4.js。java
使用傳統的開發方法,各個js文件代碼以下:jquery
//module1.js var module1 = { run: function() { return $.merge(['module1'], $.merge(module2.run(), module3.run())); } } //module2.js var module2 = { run: function() { return ['module2']; } } //module3.js var module3 = { run: function() { return $.merge(['module3'], module4.run()); } } //module4.js var module4 = { run: function() { return ['module4']; } }
此時index.html須要引用module1.js及其全部下層依賴(注意順序):git
<!DOCTYPE HTML> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>TinyApp</title> <script src="./jquery-min.js"></script> <script src="./module4.js"></script> <script src="./module2.js"></script> <script src="./module3.js"></script> <script src="./module1.js"></script> </head> <body> <p class="content"></p> <script> $('.content').html(module1.run()); </script> </body> </html>
隨着項目的進行,js文件會愈來愈多,依賴關係也會愈來愈複雜,使得js代碼和html裏的script列表每每變得難以維護。github
下面看看如何使用SeaJS實現相同的功能。正則表達式
首先是index.html:算法
<!DOCTYPE HTML> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>TinyApp</title> </head> <body> <p class="content"></p> <script src="./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,你全部須要作的準備工做就是下載sea.js而後放到你項目的某個位置。
SeaJS項目目前託管在GitHub上,主頁爲 https://github.com/seajs/seajs/ 。能夠到其git庫的build目錄下下載sea.js(已壓縮)或sea-debug.js(未壓縮)。
下載完成後放到項目的相應位置,而後在頁面中經過<script>標籤引入,你就可使用SeaJS了。
在討論SeaJS的具體使用前,先介紹一下SeaJS的模塊化理念和開發原則。
使用SeaJS開發JavaScript的基本原則就是:一切皆爲模塊。引入SeaJS後,編寫JavaScript代碼就變成了編寫一個又一個模塊,SeaJS中模塊的概念有點相似於面向對象中的類——模塊能夠擁有數據和方法,數據和方法能夠定義爲公共或私有,公共數據和方法能夠供別的模塊調用。
另外,每一個模塊應該都定義在一個單獨js文件中,即一個對應一個模塊。
下面介紹模塊的編寫和調用。
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加載便可。
工廠函數是模塊的主體和重點。在只傳遞一個參數給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主要用於載入入口模塊。入口模塊至關於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> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>TinyApp</title> </head> <body> <p class="content"></p> <script src="./sea.js" data-main="./init"></script> </body> </html>
這種寫法會令html更加簡潔。
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文件。
上文說過SeaJS會在html頁面打開時經過靜態分析一次性記載全部須要的js文件,若是想要某個js文件在用到時才下載,可使用require.async:
require.async('/path/to/module/file', function(m) { //code of callback... });
這樣只有在用到這個模塊時,對應的js文件纔會被下載,也就實現了JavaScript代碼的按需加載。
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表示是否工做在調試模式下。
要將現有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原本集成了一個打包部署工具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> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title></title> </head> <body> <div id="content"> <p class="author"></p> <p class="blog"><a href="#">Blog</a></p> </div> <script src="./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;}
運行效果以下:
[1] SeaJS主頁 – http://seajs.org
[2] SeaJS的GitHub庫(可獲取源碼) – https://github.com/seajs/seajs
[3] SeaJS做者玉伯的博客 - http://lifesinger.wordpress.com/