前言
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 module1 = { 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>
//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的特性:
使用SeaJS
下载及安装
要在项目中使用SeaJS,你所有需要做的准备工作就是下载sea.js然后放到你项目的某个位置
SeaJS项目目前托管在GitHub上,主页为https://github.com/seajs/seajs/。可以到其git库(https://github.com/seajs/seajs/tree/master/dist)下载sea.js(已压缩)或sea-debug.js(未压缩)。
下载完成后放到项目的相应位置,然后在页面中通过<script>标签引入,你就可以使用SeaJS了。
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对于不同参数个数的解析规则如下:
define(function(require, exports, module) { //code of the module... });个人建议遵循SeaJS官方示例的标准,用一个参数的define定义模块。那么id和deps会怎么处理呢?
这三个参数可以根据需要选择是否需要显示指定。
下面说一下module。module是一个对象,存储了模块的元信息,具体如下:
1. module.id —— 模块的ID。
2. module.dependencies —— 一个数组,存储了此模块依赖的所有模块的ID列表。
3. 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数据的模块非常合适。
模块的载入和引用
require("http://example/js/a");就代表载入“http://example/js/a.js”。
require("./c");则载入“http://example/js/c.js”。
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('./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更加简洁。
var m = require('/path/to/module/file');这里简要介绍一下SeaJS的自动加载机制。上文说过,使用SeaJS后html只要包含sea.js即可,那么其它js文件是如何加载进来的呢?SeaJS会首先下载入口模块,然后顺着入口模块使用正则表达式匹配代码中所有的require,再根据require中的文件路径标识下载相应的js文件,对下载来的js文件再迭代进行类似操作。整个过程类似图的遍历操作(因为可能存在交叉循环依赖所以整个依赖数据结构是一个图而不是树)。
require('module' + '1'); require('Module'.toLowerCase());这都会造成SeaJS无法进行正确的正则匹配以下载相应的js文件。
require.async('/path/to/module/file', function(m) { //code of callback... });这样只有在用到这个模块时,对应的js文件才会被下载,也就实现了JavaScript代码的按需加载。
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”。
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例子把这些知识点串起来,方便朋友们归纳回顾。这个例子包含如下文件:
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://leoo2sk.cnblogs.com' });css:
.author{color:red;font-size:10pt;} .blog{font-size:10pt;}
运行效果如下:
主要参考文献&SeaJS学习资源
[1] SeaJS主页 – http://seajs.com
[2] SeaJS的GitHub库(可获取源码) – https://github.com/seajs/seajs
[3] SeaJS作者玉伯的博客 - http://lifesinger.wordpress.com/
本文基于 署名-非商业性使用 3.0 许可协议发布,欢迎转载,演绎,但是必须保留本文的署名 张洋 (包含链接),且不得用于商业目的。如您有任何疑问或者授权方面的协商,请 与我联系 。