zoukankan      html  css  js  c++  java
  • seajs的CMD模式的优势以及使用

    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模块。

  • 相关阅读:
    Grails
    Grails
    Grails
    Grails
    Grails
    Grails
    PG
    TopShelf安装多实例
    Js 实现自定义事件
    HttpContext未null处理
  • 原文地址:https://www.cnblogs.com/leole/p/4208561.html
Copyright © 2011-2022 走看看