使用SeaJS實現模塊化JavaScript開發

前言

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.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及其所有下層依賴(注意順序):

<!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列表往往變得難以維護。

SeaJS模塊化開發

下面看看如何使用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

下載及安裝

要在項目中使用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>
<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

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>
<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;}

運行效果如下:


主要參考文獻&SeaJS學習資源

原文出處

本文非原創,原文出處 傳送門

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,125評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,506評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,402評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,934評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,168評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,690評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,596評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,027評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,398評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,743評論 2 370

推薦閱讀更多精彩內容