chaojidan

导航

seajs的CMD模式的优势以及使用

之前有一篇博客非常详细的介绍了sea.js的加载流程,以及源代码实现,链接地址:http://www.cnblogs.com/chaojidan/p/4123980.html

这篇博客我主要讲下sea.js的介绍和使用。

首先,先介绍下sea.js的CMD规范,以及跟其他规范的区别。

CommonJS 原来叫 ServerJS,推出 Modules/1.0 规范后,在 Node.js 等环境下取得了很不错的实践。

09年下半年这帮充满干劲的小伙子们想把 ServerJS 的成功经验进一步推广到浏览器端,于是将社区改名叫 CommonJS,同时激烈争论 Modules 的下一版规范。分歧和冲突由此诞生,逐步形成了三大流派:

  1. Modules/1.x 流派。这个观点觉得 1.x 规范已经够用,只要移植到浏览器端就好。主流代表是服务端的开发人员。
  2. Modules/Async 流派。这个观点觉得浏览器有自身的特征,不应该直接用 Modules/1.x 规范。这个观点下的典型代表是 AMD 规范及其实现 RequireJS
  3. Modules/2.0 流派。这个观点觉得浏览器有自身的特征,不应该直接用 Modules/1.x 规范,但应该尽可能与 Modules/1.x 规范保持一致。这个观点下的典型代表是 BravoJS 和 FlyScript 的作者。BravoJS 作者对 CommonJS 的社区的贡献很大,这份 Modules/2.0-draft 规范花了很多心思。FlyScript 的作者提出了 Modules/Wrappings 规范,这规范是 CMD 规范的前身。可惜的是 BravoJS 太学院派,FlyScript 后来做了自我阉割,将整个网站(flyscript.org)下线了。

第二流派:AMD 与 RequireJS

AMD 风格下,通过参数传入依赖模块,破坏了就近声明 (需要时,才声明)原则。比如:

define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { 
    // 等于在最前面声明并初始化了要用到的所有模块
   if (false) {
       // 即便没用到某个模块 b,但 b 还是提前执行了
       b.foo()
   } 
})

第三流派:Modules/2.0  CMD模块

CMD 里,默认推荐的是

define(function(require, exports, module) {     //a,b模块只下载好了,并且只执行了模块中的define方法,而define方法中的function要等到require时,才会执行
  var a = require('a');     //延迟执行了a,b模块
  var b = require('b');        
  // do sth
})

 

区别:

1. 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible.

2. CMD 推崇依赖就近,AMD 推崇依赖前置。看代码:

// CMD

define(function(require, exports, module) {

  var a = require('./a');

  a.doSomething()

  //此处略去 100 行

  var b = require('./b')

  // 依赖可以就近书写

  b.doSomething();

})

// AMD 默认推荐的是

  define(['./a', './b'], function(a, b) {

    // 依赖必须一开始就写好

    a.doSomething();

   // 此处略去 100 行

    b.doSomething();

  }) 

虽然 AMD 也支持 CMD 的写法,同时还支持将 require 作为依赖项传递,但 RequireJS 的作者默认是最喜欢上面的写法,也是官方文档里默认的模块定义写法。

3. AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一

比如 AMD 里,require 分全局 require 和局部 require,都叫 require。CMD 里,没有全局 require,而是根据模块系统的完备性,提供 seajs.use 来实现模块系统的加载启动。CMD 里,每个 API 都简单纯粹

CMD 可以使得构建时的复杂度降低。

目前 Sea.js 拥有 plugin-combo 插件,模块的合并可以放在线上动态做。有些情况下(比较容易出现),动态 combo 的地址会很长:

https://a.alipaybojects.com/??path/to/a.js,path/to/b.js..................path/to/long-url.js

当 url 地址很长时,超过 2083(好像是这个值),在 IE 以及一些服务器配置下,过长的 url 会出问题。这时经典的解决办法是将 url 拆分成多段:

https://a.alipaybojects.com/??path/to/a.js,path/to/b.js..................path/to/u.js

https://a.alipaybojects.com/??path/to/f.js,path/to/g.js..................path/to/long-url.js

拆分后,在 CMD 规范下,上面两个 url 可以并发同时请求,谁先返回都没问题。但在 AMD 下,上面的需求,就挂了,很难实现。

你会说 RequireJS 鼓励的是项目上线前,通过构建工具先构建好,不需要线上 combo,也就不会遇到上面的问题。

Sea.js 放得更宽泛,提前合并好,还是线上时才动态 combo,对 CMD 模块来说都可行。很多时候,combo 真心省事,也更自然。前端开发并非处处要像 Java 一样引入严格的构建过程。

CMD 的懒执行策略,也更有利于页面性能。

RequireJS 2.0 后,不少理念也在悄悄地发生着变化,现在好像也支持懒执行了。

 

然后,介绍下sea.js的方法和使用。

type="text/javascript" src="js/seajs/2.0.0/sea-debug.js?t=123" data-config="sea-js-config.js?t=123"

上面的data-config是指sea.js的配置文件的路径。还有一个属性是data-main,它是项目的起始模块,如果定义了会先执行此模块。data-main是可选项。

首先我们来看看sea-js-config.js

seajs.config({
     // 配置插件
   plugins: ['shim'],
     // 配置别名
   alias: {
       // 配置 jquery 的 shim 配置,这样我们就可以通过 require('jquery') 来获取 jQuery
     'jquery': {
          src: 'libs/jquery/1.9.1/jquery.js',  //注意,这里是从sea.js所在目录的上两节目录开始查找文件
            exports: 'jQuery'
       }
   }
});

plugins选项配置插件,这里使用了shim插件。由于jquery不是一个标准的CMD模块,所以直接加载jquery是错误的。这里使用了shim插件,会自动把jquery转换成一个标准的CMD模块。不用人工改动jquery源码。alias是配置别名,方便加载的。

看个例子:


项目主模块app.js
define(function(require, exports, module) {
    //加载jquery, 并把它$设为全局变量
   window.$ = window.jQuery = $ = require('jquery');
      //定义一个全局的模块加载函数.module为模块名,options为参数
    exports.script_load = function(module, options) {
        //使用require.async异步加载模块。模块需要提供一个固定的对外调用接口,这里定义为run。
        require.async('modules/' + module, function(module) {
       if (typeof(module.run) === 'function') {
                  module.run(options);
       }
         });
    }
    window.script_load = exports.script_load
});
上面我们加载了jquery, 并且定义了一个模块加载函数。现在我们在html页面加入如下代码:
<script type="text/javascript">
     seajs.use('modules/app', function(app) {
           app.script_load('index');
  });
</script>
use方法执行时,会先加载app模块,加载并执行完后,就进入function中,这时就会调用app.script_load方法,此方法就会去加载index模块,加载完成后,执行index中的代码,index中会返回run方法。index执行完毕后,会调用require.async的回调方法:

if (typeof(module.run) === 'function') {
                  module.run(options);
}

因此index模块中返回了run方法,因此就执行index中的run方法。

index.js
define(function(require, exports, module) {
   exports.run = function() {
         $('#alert').click(function() {
        alert('弹出了一个框!');
         });
  }
});

 

SeaJS中使用“define”函数定义一个模块,define可以接收三个参数,
define可以接收的参数分别是模块ID,依赖模块数组及工厂函数。
我阅读源代码后发现define对于不同参数个数的解析规则如下:
如果只有一个参数,则赋值给factory。
如果有两个参数,第二个赋值给factory;第一个如果是array则赋值给deps,否则赋值给id。
如果有三个参数,则分别赋值给id,deps和factory。
id是一个模块的标识字符串,define只有一个参数时,id会被默认赋值为此js文件的绝对路径。
如example.com下的a.js文件中使用define定义模块,则这个模块的ID会赋值为 http://example.com/a.js ,没有特别的必要建议不要传入id。deps一般也不需要传入,需要用到的模块用require加载即可。
工厂函数function是模块的主体和重点。在只传递一个参数给define时(推荐写法),这个参数就是工厂函数,此时工厂函数的三个参数分别是:
• require——模块加载函数,用于记载依赖模块。
• exports——接口点,将数据或方法定义在其上则将其暴露给外部调用。
• 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, exports, module) {
  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';
    }
  };

});

如果模块定义没有其它代码,只返回一个对象,还可以有如下简化写法。第三种方法对于定义纯JSON数据的模块非常合适。

define({

    data: 1,

    func: function() {

        return 'hello';

   }

});

 

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

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

就代表载入 http://example/js/a.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函数,同时也是整个模块依赖树的根。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,例如,

<!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>

 

传给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原有代码结束

   return $.noConflict();
});

 

特别注意:下面这种写法是错误的!
define(function(require, exports) {

  // 错误用法!!!
  exports = {
    foo: 'bar',
    doSomething: function() {}
  };

});
正确的写法是用 return 或者给 module.exports 赋值:
define(function(require, exports, module) {

  // 正确写法
  module.exports = {
    foo: 'bar',
    doSomething: function() {}
  };

});
提示:exports 仅仅是 module.exports 的一个引用。在 factory 内部给 exports 重新赋值时,并不会改变 module.exports 的值。因此给 exports 赋值是无效的,不能用来更改模块接口。
传给 factory 构造方法的 exports 参数是 module.exports 对象的一个引用。

只通过 exports 参数来提供接口,有时无法满足开发者的所有需求。 比如当模块的接口是某个类的实例时,需要通过module.exports 来实现:
define(function(require, exports, module) {

  // exports 是 module.exports 的一个引用
  console.log(module.exports === exports); // true

  // 重新给 module.exports 赋值
  module.exports = new SomeClass();     //当模块的接口是某个类的实例时

  // exports 不再等于 module.exports
  console.log(module.exports === exports); // false

});
注意:对 module.exports 的赋值需要同步执行,不能放在回调函数里。下面这样是不行的:

define(function(require, exports, module) {

  // 错误用法
  setTimeout(function() {
    module.exports = { a: "hello" };
  }, 0);

});
seajs.config({
  alias: {
  'jquery': 'jquery/1.7.2/jquery-debug.js'
}
});

seajs.use(['./a','jquery'],function(a,$){
  var num = a.a;
  $('#J_A').text(num);
})

use方法将会从我们的config配置信息中查看 ,是否有预先需要被加载的模块。如果有,就先加载,没有就加载a和jquery模块。

posted on 2014-12-06 10:14  chaojidan  阅读(1731)  评论(0编辑  收藏  举报