首页 > 代码库 > 加载器 seaJS

加载器 seaJS

一步步学会使用SeaJS 2.0
本文分为以下8步,熟悉之后就能够熟练使用SeaJS,从此之后你的生活会变得更加轻松愉悦!

1、SeaJS是什么?

2、下载并检阅SeaJS

3、建立工程和各种目录

4、引入SeaJS库

5、编写自己的代码

6、引入自己的代码

7、压缩合并

8、总结展望

 

--------------------------------------------------

 

1、SeaJS是什么?

 

你一定听过前端模块化开发吧?神马,你没听过?我只能说你out了……

你应该知道Java的import吧?神马,你又不知道?那你应该知道CSS中的import吧……

在这里我不想展开说前端模块化的含义和价值,因为这里有一篇好文(https://github.com/seajs/seajs/issues/547),详细说明了前端模块化。

我知道你看到那么长的文章肯定会望而却步,也许你是希望能够快速开始敲代码(程序员的通病……)。没关系,如果实在读不下去,只要记住模块化要解决的问题即可:命名冲突、文件依赖关系。

这两个闹心的问题应该遇到过吧,如果没遇到过……我只能说你太牛X了

好了,前端模块化就扯到这里,写过前端的人应该基本都知道JavaScript自身是不支持模块化开发的,所以就有了SeaJS这款神器,为前端从业者提供了一个强大易用的模块化开发工具。

 

 

 

2、下载并检阅SeaJS

 

SeaJS现在已经是2.0版本啦,到这里下载:https://github.com/seajs/seajs

 

解压后会看到下列目录:

 

其中:

dist —— 压缩好的、用于浏览器端的SeaJS代码

docs —— 文档

src —— 源代码

package.json + Gruntfile.js —— Grunt构建工具所需要的文件,这个在第七步压缩合并会介绍到

其他目录或文件可以暂且不管

 

 

 

3、建立工程和各种目录

 

准备工作已经完成,我们终于可以开始进行开发啦!来,跟我走:

a. 建立工程

用你最喜欢的IDE建立工程,名字为HelloSeaJS

b. 准备各种目录

在这里把JavaScript、Image、CSS都放在统一的资源文件(assets)中,建好之后的目录如下:

(我使用了Sublime2.0,在这里强烈推荐)

 

c. 把刚刚下好的seajs/dist中的文件都放在scripts/seajs目录下

注意:SeaJS会根据自身的URI来决定URL base,而SeaJS在加载其他模块的时候会根据这个URL base来计算路径。SeaJS会忽略掉seajs、seajs/2.0.0/seajs这两种目录,照上述的目录结构,此处的URL base就是HelloSeaJS/assets/scripts,这样其他模块就可以与seajs目录并行存放。

 

至此,工程和文件都已准备完成。

 

 

 

4、引入SeaJS库

 

与引入其他js库并无太大区别:

<script src="http://www.mamicode.com/assets/scripts/seajs/sea.js" id="seajsnode"></script>

你可能注意到,这里加上了id="seajsnode",原因如下:

a. SeaJS加载自身的script标签的其他属性(如data-config、data-main)等来实现不同的功能

b. SeaJS内部通过document.getElementById("seajsnode")来获取这个script标签(其实SeaJS内部还有一种方式,不过另一种方式的效率比较低,所以不推荐,如果有兴趣,可以看一下源码https://github.com/seajs/seajs/blob/master/src/util-path.js)

 

 

 

 

5、编写自己的代码

 

这里作为示范,只做了一个非常简单的效果,点击查看:http://liuda101.github.io/HelloSeaJS/

在编写自己代码的时候,要时刻记住”模块化“,而操作起来也非常简单,因为在SeaJS中一个文件就是一个模块。

下面是代码逻辑的模块application.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
define( function (require,exports,module){
   
      var  util = {};
   
      var  colorRange = [ ‘0‘ , ‘1‘ , ‘2‘ , ‘3‘ , ‘4‘ , ‘5‘ , ‘6‘ , ‘7‘ , ‘8‘ , ‘9‘ , ‘A‘ , ‘B‘ , ‘C‘ , ‘D‘ , ‘E‘ , ‘F‘ ];
   
      util.randomColor =  function (){
           return  ‘#‘  +
                colorRange[Math.floor(Math.random() * 16)] +
                colorRange[Math.floor(Math.random() * 16)] +
                colorRange[Math.floor(Math.random() * 16)] +
                colorRange[Math.floor(Math.random() * 16)] +
                colorRange[Math.floor(Math.random() * 16)] +
                colorRange[Math.floor(Math.random() * 16)];
      };
   
   
      var  helloSeaJS = document.getElementById( ‘hello-seajs‘ );
      helloSeaJS.style.color = util.randomColor();
      window.setInterval( function (){
           helloSeaJS.style.color = util.randomColor();
      },1500);
});

我们看到,所有代码都放在define(function(require,exports,module){});函数体里面。

define是SeaJS定义的一个全局函数,用来定义一个模块。

至于require,exports,module都是什么,可以暂且不管,到此,我们的代码已经完成,很简单吧。嗯,花个几十秒钟,看一下代码。

……

看完之后,你会说,这算什么啊!这就完了么?

不要怪我,为了简单易懂,我们就按照”一步步“的节奏慢慢来。

随着代码的增多,你肯定会遇到util越来越多的情况。很好,这样看来,我们就有了两个模块:util模块和application模块。SeaJS中,文件即模块,所以当然要将其分为两个文件。先看util.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
define( function (require,exports,module){
      var  util = {};
   
      var  colorRange = [ ‘0‘ , ‘1‘ , ‘2‘ , ‘3‘ , ‘4‘ , ‘5‘ , ‘6‘ , ‘7‘ , ‘8‘ , ‘9‘ , ‘A‘ , ‘B‘ , ‘C‘ , ‘D‘ , ‘E‘ , ‘F‘ ];
   
      util.randomColor =  function (){
           return  ‘#‘  +
                colorRange[Math.floor(Math.random() * 16)] +
                colorRange[Math.floor(Math.random() * 16)] +
                colorRange[Math.floor(Math.random() * 16)] +
                colorRange[Math.floor(Math.random() * 16)] +
                colorRange[Math.floor(Math.random() * 16)] +
                colorRange[Math.floor(Math.random() * 16)];
      };
   
      module.exports = util;
});

 

除了define之外,我们看到module.exports = util;这一句比较特殊。这句是在说,我util模块向外暴露的接口就这些,其他所有的东西都是我内部用的,尔等就无需担心了,我会照顾好的。

再看application.js:

1
2
3
4
5
6
7
8
9
10
define( function (require,exports,module){
   
      var  util = require( ‘./util‘ );
   
      var  helloSeaJS = document.getElementById( ‘hello-seajs‘ );
      helloSeaJS.style.color = util.randomColor();
      window.setInterval( function (){
           helloSeaJS.style.color = util.randomColor();
      },1500);
});

 

我们看到var util = require(‘./util‘);这句比较特殊。这句就是在说,我application模块由于业务需要,想请util模块来帮忙,所以把util给require进来。

 

 

至此,我们经历了一个模块到两个模块的转变,在日后漫长的日子中,我们的模块也许会越来越多,不过不用担心,有了SeaJS提供的define、require、module.exports,我们都可以方便的应对。

 

 

 

6、引入自己的代码

 

你看到这个小标题,你可能会极力的鄙视我,这等工作还需要你来示范?于是,你啪啪啪啪,在引入SeaJS的script标签后引入了util.js和application.js:

<script src="http://www.mamicode.com/assets/scripts/application/util.js"></script>

<script src="http://www.mamicode.com/assets/scripts/application/application.js"></script>

然后你不停的F5……

你看不到效果吧?这就是这个小节存在的理由。

 

SeaJS提供了模块化的能力,前面我们已经看到了SeaJS定义模块、引用模块的方法,而这里就要用到SeaJS加载并启动模块的两种方式:

a、使用data-main

为<script src="http://www.mamicode.com/assets/scripts/seajs/sea.js" id="seajsnode"></script>添加data-main="application/application"属性即可:

<script src="http://www.mamicode.com/assets/scripts/seajs/sea.js" id="seajsnode" data-main="application/application"></script>

SeaJS会根据data-main指定的模块来作为整个应用的入口模块。SeaJS找到这个模块之后,就会加载执行这个模块对应的文件。

那么,SeaJS又是怎么找到这个文件呢?也就是说,这个模块对应的加载路径是多少?

“算法”是:SeaJS_URL_base + data-main

如上文,该例子的SeaJS_URL_base是HelloSeaJS/assets/scripts/

那么,加载路径就是HelloSeaJS/assets/scripts/application/application.js(SeaJS会自动加上.js后缀)

 

b、使用seajs.use

在<script src="http://www.mamicode.com/assets/scripts/seajs/sea.js" id="seajsnode">后面加上:

<script> seajs.use("application/application"); </script>

其实这两种效果在这个例子中是一样的,data-main通常用在只有一个入口的情况,use可以用在多个入口的情况,具体用法,看这里:https://github.com/seajs/seajs/issues/260

如果你对你的程序有完全的控制权,建议使用data-main的方式,这样整个页面就只有一段script标签!作为一名前端开发人员,我不得不惊叹:干净、完美!

 

无论使用哪种方式,跟着我一起F5一下!

在打开Chrome的debug工具,查看Network这个tab:

我们看到,SeaJS已经帮我们加载好了application.js和util.js,舒服吧~

嗯,我第一次试用SeaJS的时候,到这里也感到了无比的舒心

 

 

 

7、压缩合并

 

正当我伸伸懒腰,打算上个厕所的时候,突然想到一件事情:如果模块越来越多,那么多文件都要分开加载?那岂不严重影响性能!?(啥,你不知道为啥?)

要压缩合并JavaScript呀!于是,我强忍住那股液体,开始用YUICompressor来压缩,并手动合并了两个文件。

这里就不展示结果了,因为很蛋疼,完全对不住我刚才忍住液体的勇气!结果当然是,失败。

为什么会失败呢?自己想了想,同时打开压缩后的代码一看,才发现原因:

压缩后之后,require变量变成了a变量。SeaJS是通过require字面来判断模块之间的依赖关系的,所以,require变量不能被简化。

 

嗯,SeaJS已经替我们想到了这个问题,于是我们就采用SeaJS提供的方式来合并压缩吧(当然你也可以自己用别的方式压缩)。

SeaJS在2.0之前,是采用SPM作为压缩合并工具的,到了2.0,改为Grunt.js,SPM变为包管理工具,类似NPM(不知道NPM?Google一下吧)

 

自动化不仅是科技带给社会的便利,也是Grunt带给前端的瑞士军刀。使用Grunt,可以很方便的定制各种任务,如压缩、合并等。使用Grunt之前,需要安装node环境和grunt工具,Google一下,十分钟后回来。

……

 

Grunt最核心的就两个部分,package.json、Gruntfile.js。

 

a. package.json

    Grunt把一个项目/目录视为一个npm模块,package.json就是用来描述这个模块的信息,包括name、version、author等等。

这里强调一下,Grunt既然将该目录视为一个模块,那么该模块当然可以依赖其他模块。

我们看本示例的:

1
2
3
4
5
6
7
8
9
10
11
12
{
      "name" : "HelloSeaJS",
      "version" : "1.0.0",
      "author" : "Qifeng Liu",
      "devDependencies" : {
           "grunt" : "0.4.1",
           "grunt-cmd-transport" : "0.1.1",
           "grunt-cmd-concat" : "0.1.0",
           "grunt-contrib-uglify" : "0.2.0",
           "grunt-contrib-clean" : "0.4.0"
      }
}

 

 

devDependencies就是用来描述自身所依赖的模块

其中:

grunt模块用来跑Gruntfile.js中定义的任务

grunt-cmd-transport模块用来对SeaJS定义的模块进行依赖提取等任务

grunt-cmd-concat模块用来对文件进行合并

grunt-contrib-uglify模块用来压缩JavaScript

grunt-contrib-clean模块用来清除临时目录

 

b. Gruntfile.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
module.exports =  function (grunt){
   
      grunt.initConfig({
           transport : {
                options : {
                     format :  ‘application/dist/{{filename}}‘   //生成的id的格式
                },
                application : {
                     files : {
                          ‘.build‘  : [ ‘application.js‘ , ‘util.js‘ ]    //将application.js、util.js合并且提取依赖,生成id,之后放在.build目录下
                     }
                }
           },
           concat : {
                main : {
                     options : {
                          relative :  true
                     },
                     files : {
                          ‘dist/application.js‘  : [ ‘.build/application.js‘ ],   // 合并.build/application.js文件到dist/application.js中
                          ‘dist/application-debug.js‘  : [ ‘.build/application-debug.js‘ ]
                     }
                }
           },
           uglify : {
                main : {
                     files : {
                          ‘dist/application.js‘  : [ ‘dist/application.js‘ //对dist/application.js进行压缩,之后存入dist/application.js文件
                     }
                }
           },
           clean : {
                build : [ ‘.build‘ //清除.build文件
           }
      });
   
      grunt.loadNpmTasks( ‘grunt-cmd-transport‘ );
      grunt.loadNpmTasks( ‘grunt-cmd-concat‘ );
      grunt.loadNpmTasks( ‘grunt-contrib-uglify‘ );
      grunt.loadNpmTasks( ‘grunt-contrib-clean‘ );
   
      grunt.registerTask( ‘build‘ ,[ ‘transport‘ , ‘concat‘ , ‘uglify‘ , ‘clean‘ ])
};


 

定义好两个文件之后,就可以进入到application目录下,首先运行:

npm install

该命令会下载好package.json中依赖的模块

然后运行

grunt build

该命令会运行grunt.registerTask方法中指定的任务

 

不出差错的话,会在application目录下生成一个dist目录,里面包含了合并但没压缩的application-debug.js和合并且压缩好的application.js。

修改index.html的

<script src="http://www.mamicode.com/assets/scripts/seajs/sea.js" id="seajsnode" data-main="application/application"></script>

<script src="http://www.mamicode.com/assets/scripts/seajs/sea.js" id="seajsnode" data-main="application/dist/application"></script>

大功告成!

 

 

 

8、总结展望

 

SeaJS秉着简单实用的原则,API设计职责单一,使用起来得心应手。

SeaJS为前端提供了模块化能力,可以简单优雅的解决命名冲突、依赖关系等常见且棘手的问题。通过使用SeaJS,我的生活质量一下次上升好几个档次。

当然,除了本文介绍的,SeaJS还有一些其他功能,相信只要你入了门,肯定能够迅速掌握,这里给个链接http://seajs.org/docs/#docs

其实,许多语言自身已经有了模块化的功能,如Java的import,C++的#include等,但是由于各种原因,JavaScript自身并没有这个功能。虽然借助于SeaJS可以很方便的进行模块化开发了,但总觉得这个能力应该是语言自身的。好在,下个版本的JavaScript貌似在计划引入模块化的概念,让我们拭目以待吧!

 

 

 

最后,感谢SeaJS作者玉伯。

 

 

PS,本文参考了SeaJS提供的使用范例https://github.com/seajs/examples/tree/master/static/hello

 

 

 

 

 

 

 

 

前言

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文件代码如下:

 
 
  1. //module1.js

  2. var module1 = {

  3.    run: function() {

  4.        return $.merge([‘module1‘], $.merge(module2.run(), module3.run()));

  5.    }

  6. }

  7.  

  8. //module2.js

  9. var module2 = {

  10.    run: function() {

  11.        return [‘module2‘];

  12.    }

  13. }

  14.  

  15. //module3.js

  16. var module3 = {

  17.    run: function() {

  18.        return $.merge([‘module3‘], module4.run());

  19.    }

  20. }

  21.  

  22. //module4.js

  23. var module4 = {

  24.    run: function() {

  25.        return [‘module4‘];

  26.    }

  27. }

此时index.html需要引用module1.js及其所有下层依赖(注意顺序):

 
 
  1. <!DOCTYPE HTML>

  2. <html lang="zh-CN">

  3. <head>

  4.    <meta charset="UTF-8">

  5.    <title>TinyApp</title>

  6.    <script src="./jquery-min.js"></script>

  7.    <script src="./module4.js"></script>

  8.    <script src="./module2.js"></script>

  9.    <script src="./module3.js"></script>

  10.    <script src="./module1.js"></script>

  11. </head>

  12. <body>

  13.    <p class="content"></p>

  14.    <script>

  15.        $(‘.content‘).html(module1.run());

  16.    </script>

  17. </body>

  18. </html>

随着项目的进行,js文件会越来越多,依赖关系也会越来越复杂,使得js代码和html里的script列表往往变得难以维护。

SeaJS模块化开发

下面看看如何使用SeaJS实现相同的功能。

首先是index.html:

 
 
  1. <!DOCTYPE HTML>

  2. <html lang="zh-CN">

  3. <head>

  4.    <meta charset="UTF-8">

  5.    <title>TinyApp</title>

  6. </head>

  7. <body>

  8.    <p class="content"></p>

  9.    <script src="./sea.js"></script>

  10.    <script>

  11.        seajs.use(‘./init‘, function(init) {

  12.            init.initPage();

  13.        });

  14.    </script>

  15. </body>

  16. </html>

可以看到html页面不再需要引入所有依赖的js文件,而只是引入一个sea.js,sea.js会处理所有依赖,加载相应的js文件,加载策略可以选择在渲染页面时一次性加载所有js文件,也可以按需加载(用到时才加载响应js),具体加载策略使用方法下文讨论。

index.html加载了init模块,并使用此模块的initPage方法初始化页面数据,这里先不讨论代码细节。

下面看一下模块化后JavaScript的写法:

 
 
  1. //jquery.js

  2. define(function(require, exports, module) = {

  3.  

  4.    //原jquery.js代码...

  5.  

  6.    module.exports = $.noConflict(true);

  7. });

  8.  

  9. //init.js

  10. define(function(require, exports, module) = {

  11.    var $ = require(‘jquery‘);

  12.    var m1 = require(‘module1‘);

  13.  

  14.    exports.initPage = function() {

  15.        $(‘.content‘).html(m1.run());    

  16.    }

  17. });

  18.  

  19. //module1.js

  20. define(function(require, exports, module) = {

  21.    var $ = require(‘jquery‘);

  22.    var m2 = require(‘module2‘);

  23.    var m3 = require(‘module3‘);

  24.  

  25.    exports.run = function() {

  26.        return $.merge([‘module1‘], $.merge(m2.run(), m3.run()));    

  27.    }

  28. });

  29.  

  30. //module2.js

  31. define(function(require, exports, module) = {

  32.    exports.run = function() {

  33.        return [‘module2‘];

  34.    }

  35. });

  36.  

  37. //module3.js

  38. define(function(require, exports, module) = {

  39.    var $ = require(‘jquery‘);

  40.    var m4 = require(‘module4‘);

  41.  

  42.    exports.run = function() {

  43.        return $.merge([‘module3‘], m4.run());    

  44.    }

  45. });

  46.  

  47. //module4.js

  48. define(function(require, exports, module) = {

  49.    exports.run = function() {

  50.        return [‘module4‘];

  51.    }

  52. });

乍看之下代码似乎变多变复杂了,这是因为这个例子太简单,如果是大型项目,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可以接收三个参数:

 
 
  1. /**

  2. * Defines a module.

  3. * @param {string=} id The module id.

  4. * @param {Array.|string=} deps The module dependencies.

  5. * @param {function()|Object} factory The module factory function.

  6. */

  7. fn.define = function(id, deps, factory) {

  8.    //code of function…

  9. }

上面是我从SeaJS源码中摘录出来的,define可以接收的参数分别是模块ID,依赖模块数组及工厂函数。我阅读源代码后发现define对于不同参数个数的解析规则如下:

如果只有一个参数,则赋值给factory。

如果有两个参数,第二个赋值给factory;第一个如果是array则赋值给deps,否则赋值给id。

如果有三个参数,则分别赋值给id,deps和factory。

但是,包括SeaJS的官方示例在内几乎所有用到define的地方都只传递一个工厂函数进去,类似与如下代码:

 
 
  1. define(function(require, exports, module) {

  2.    //code of the module...

  3. });

个人建议遵循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的模式:

 
 
  1. define(function(require, exports, module) {

  2.    var a = require(‘a‘); //引入a模块

  3.    var b = require(‘b‘); //引入b模块

  4.  

  5.    var data1 = 1; //私有数据

  6.  

  7.    var func1 = function() { //私有方法

  8.        return a.run(data1);

  9.    }

  10.  

  11.    exports.data2 = 2; //公共数据

  12.  

  13.    exports.func2 = function() { //公共方法

  14.        return ‘hello‘;

  15.    }

  16. });

上面是一种比较“正宗”的模块定义模式。除了将公共数据和方法附加在exports上,也可以直接返回一个对象表示模块,如下面的代码与上面的代码功能相同:

 
 
  1. define(function(require) {

  2.    var a = require(‘a‘); //引入a模块

  3.    var b = require(‘b‘); //引入b模块

  4.  

  5.    var data1 = 1; //私有数据

  6.  

  7.    var func1 = function() { //私有方法

  8.        return a.run(data1);

  9.    }

  10.  

  11.    return {

  12.        data2: 2,

  13.        func2: function() {

  14.            return ‘hello‘;

  15.        }

  16.    };

  17. });

如果模块定义没有其它代码,只返回一个对象,还可以有如下简化写法:

 
 
  1. define({

  2.    data: 1,

  3.    func: function() {

  4.        return ‘hello‘;

  5.    }

  6. });

第三种方法对于定义纯JSON数据的模块非常合适。

模块的载入和引用

模块的寻址算法

上文说过一个模块对应一个js文件,而载入模块时一般都是提供一个字符串参数告诉载入函数需要的模块,所以就需要有一套从字符串标识到实际模块所在文件路径的解析算法。SeaJS支持如下标识:

绝对地址——给出js文件的绝对路径。

 
 
  1. require("http://example/js/a");

就代表载入 http://example/js/a.js 。

相对地址——用相对调用载入函数所在js文件的相对地址寻找模块。

例如在 http://example/js/b.js 中载入

 
 
  1. require("./c");

则载入 http://example/js/c.js 。

基址地址——如果载入字符串标识既不是绝对路径也不是以”./”开头,则相对SeaJS全局配置中的“base”来寻址,这种方法稍后讨论。

注意上面在载入模块时都不用传递后缀名“.js”,SeaJS会自动添加“.js”。但是下面三种情况下不会添加:

载入css时,如

 
 
  1. require("./module1-style.css");

路径中含有”?”时,如

 
 
  1. require(<a href="http://example/js/a.json?cb=func">http://example/js/a.json?cb=func</a>);

路径以”#”结尾时,如

 
 
  1. require("http://example/js/a.json#");

根据应用场景的不同,SeaJS提供了三个载入模块的API,分别是seajs.use,require和require.async,下面分别介绍。

seajs.use

seajs.use主要用于载入入口模块。入口模块相当于C程序的main函数,同时也是整个模块依赖树的根。上面在TinyApp小例子中,init就是入口模块。seajs.use用法如下:

 
 
  1. //单一模式

  2. seajs.use(‘./a‘);

  3.  

  4. //回调模式

  5. seajs.use(‘./a‘, function(a) {

  6.  a.run();

  7. });

  8.  

  9. //多模块模式

  10. seajs.use([‘./a‘, ‘./b‘], function(a, b) {

  11.  a.run();

  12.  b.run();

  13. });

一般seajs.use只用在页面载入入口模块,SeaJS会顺着入口模块解析所有依赖模块并将它们加载。如果入口模块只有一个,也可以通过给引入sea.js的script标签加入”data-main”属性来省略seajs.use,例如,上面TinyApp的index.html也可以改为如下写法:

 
 
  1. <!DOCTYPE HTML>

  2. <html lang="zh-CN">

  3. <head>

  4.    <meta charset="UTF-8">

  5.    <title>TinyApp</title>

  6. </head>

  7. <body>

  8.    <p class="content"></p>

  9.    <script src="./sea.js" data-main="./init"></script>

  10. </body>

  11. </html>

这种写法会令html更加简洁。

require

require是SeaJS主要的模块加载方法,当在一个模块中需要用到其它模块时一般用require加载:

 
 
  1. var m = require(‘/path/to/module/file‘);

这里简要介绍一下SeaJS的自动加载机制。上文说过,使用SeaJS后html只要包含sea.js即可,那么其它js文件是如何加载进来的呢?SeaJS会首先下载入口模块,然后顺着入口模块使用正则表达式匹配代码中所有的require,再根据require中的文件路径标识下载相应的js文件,对下载来的js文件再迭代进行类似操作。整个过程类似图的遍历操作(因为可能存在交叉循环依赖所以整个依赖数据结构是一个图而不是树)。

明白了上面这一点,下面的规则就很好理解了:

传给require的路径标识必须是字符串字面量,不能是表达式,如下面使用require的方法是错误的:

 
 
  1. require(‘module‘ + ‘1‘);

  2.  

  3. require(‘Module‘.toLowerCase());

这都会造成SeaJS无法进行正确的正则匹配以下载相应的js文件。

require.async

上文说过SeaJS会在html页面打开时通过静态分析一次性记载所有需要的js文件,如果想要某个js文件在用到时才下载,可以使用require.async:

 
 
  1. require.async(‘/path/to/module/file‘, function(m) {

  2.    //code of callback...

  3. });

这样只有在用到这个模块时,对应的js文件才会被下载,也就实现了JavaScript代码的按需加载。

SeaJS的全局配置

SeaJS提供了一个seajs.config方法可以设置全局配置,接收一个表示全局配置的配置对象。具体使用方法如下:

 
 
  1. seajs.config({

  2.    base: ‘path/to/jslib/‘,

  3.    alias: {

  4.      ‘app‘: ‘path/to/app/‘

  5.    },

  6.    charset: ‘utf-8‘,

  7.    timeout: 20000,

  8.    debug: false

  9. });

其中base表示基址寻址时的基址路径。例如base设置为 http://example.com/js/3-party/ ,则

 
 
  1. 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的封装方法:

 
 
  1. define(function() {

  2.  

  3. //{{{jQuery原有代码开始

  4. /*!  

  5. * jQuery JavaScript Library v1.6.1

  6. * http://jquery.com/

  7. *

  8. * Copyright 2011, John Resig

  9. * Dual licensed under the MIT or GPL Version 2 licenses.

  10. * http://jquery.org/license

  11. *

  12. * Includes Sizzle.js

  13. * http://sizzlejs.com/

  14. * Copyright 2011, The Dojo Foundation

  15. * Released under the MIT, BSD, and GPL Licenses.

  16. *

  17. * Date: Thu May 12 15:04:36 2011 -0400

  18. */

  19. //...

  20. //}}}jQuery原有代码结束

  21.  

  22. return $.noConflict();

  23. });

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:

 
 
  1. <!DOCTYPE HTML>

  2. <html lang="zh-CN">

  3. <head>

  4.    <meta charset="UTF-8">

  5.    <title></title>

  6. </head>

  7. <body>

  8. <div id="content">

  9.    <p class="author"></p>

  10.    <p class="blog"><a href="#">Blog</a></p>

  11. </div>

  12.  

  13. <script src="./sea.js" data-main="./init"></script>

  14. </body>

  15. </html>

javascript:

 
 
  1. //init.js

  2. define(function(require, exports, module) {

  3.    var $ = require(‘./jquery‘);

  4.    var data = require(‘./data‘);

  5.    var css = require(‘./style.css‘);

  6.  

  7.    $(‘.author‘).html(data.author);

  8.    $(‘.blog‘).attr(‘href‘, data.blog);

  9. });

  10.  

  11. //data.js

  12. define({

  13.    author: ‘ZhangYang‘,

  14.    blog: ‘http://blog.codinglabs.org‘

  15. });

css:

 
 
  1. .author{color:red;font-size:10pt;}

  2. .blog{font-size:10pt;}

运行效果如下:

加载器 seaJS