AMD requireJs

184 阅读11分钟

一、AMD/CMD/Commjs/ES6 Module

前言:模块化的开发方式可以提高代码复用率,方便进行代码的管理。通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。目前流行的js模块化规范有CommonJS、AMD、CMD以及ES6的模块系统

1.AMD

AMD: (Asynchronous ModuleDefinition)异步模块定义,采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖模块的语句,都定义在一个回调函数中,等到模块加载完成之后,这个回调函数才会运行

代表:require.js

require.js在申明依赖的模块时会在第一之间加载并执行模块内的代码:

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

/** 网页中引入require.js及main.js **/
<script src="js/require.js" data-main="js/main"></script>

/** main.js 入口文件/主模块 **/
// 首先用config()指定各模块路径和引用名
require.config({
  baseUrl: "js/lib",
  paths: {
    "jquery": "jquery.min",  //实际路径为js/lib/jquery.min.js
    "underscore": "underscore.min",
  }
});
// 执行基本操作
require(["jquery","underscore"],function($,_){
  // some code here
});

2.CMD

CMD:(Common Module Definition)通用模块定义,CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。此规范其实是在sea.js推广过程中产生的。

代表:sea.js


/** CMD写法 **/
define(function(require, exports, module) {
    var a = require('./a'); //在需要时申明
    a.doSomething();
    if (false) {
        var b = require('./b');
        b.doSomething();
    }
});

/** sea.js **/
// 定义模块 math.js
define(function(require, exports, module) {
    var $ = require('jquery.js');
    var add = function(a,b){
        return a+b;
    }
    exports.add = add;
});
// 加载模块
seajs.use(['math.js'], function(math){
    var sum = math.add(1+2);
});

3.CommonJS

Node.js是commonJS规范的主要实践者,它有四个重要的环境变量为模块化的实现提供支持:module、exports、require、global。实际使用时,用module.exports定义当前模块对外输出的接口(不推荐直接用exports),用require加载模块。

// 定义模块math.js
var basicNum = 0;
function add(a, b) {
  return a + b;
}
module.exports = { //在这里写上需要向外暴露的函数、变量
  add: add,
  basicNum: basicNum
}

// 引用自定义的模块时,参数包含路径,可省略.js
var math = require('./math');
math.add(2, 5);

// 引用核心模块时,不需要带路径
var http = require('http');
http.createService(...).listen(3000);

commonJS用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。

3.ES6 Module

ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,旨在成为浏览器和服务器通用的模块解决方案。其模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {
    return a + b;
};
export { basicNum, add };

/** 引用模块 **/
import { basicNum, add } from './math';
function test(ele) {
    ele.textContent = add(99 + basicNum);
}

如上例所示,使用import命令的时候,用户需要知道所要加载的变量名或函数名。其实ES6还提供了export default命令,为模块指定默认输出,对应的import语句不需要使用大括号。这也更趋近于ADM的引用写法。

/** export default **/
//定义输出
export default { basicNum, add };
//引入
import math from './math';
function test(ele) {
    ele.textContent = math.add(99 + math.basicNum);
}

ES6的模块不是对象,import命令会被JavaScript引擎静态分析,在编译时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载。也正因为这个,使得静态分析成为可能。

二、requireJs

1.介绍

前言:RequireJS的目标是鼓励代码的模块化,它使用了不同于传统<script>标签的脚本加载步骤。可以用它来加速、优化代码,但其主要目的还是为了代码的模块化。

RequireJS以一个相对于baseUrl的地址来加载所有的代码。 页面顶层<script>标签含有一个特殊的属性data-main,require.js使用它来启动脚本加载过程,而baseUrl一般设置到与该属性相一致的目录。

RequireJS默认假定所有的依赖资源都是js脚本,因此无需在module ID上再加".js"后缀,RequireJS在进行module ID到path的解析时会自动补上后缀。你可以通过paths config设置一组脚本,这些有助于我们在使用脚本时码更少的字。

您通常会使用一个数据主要脚本来设置配置选项。然后加载第一个应用模块。 注意:script标记require.js生成的data-main模块包括异步属性。 这意味着,你不能想当然地认为加载和执行你的data-main脚本,将先于在相同的页面中引用的其他脚本。

如果模块存在依赖,则第一个参数是依赖模块的名称数组,第二个参数是模块定义函数,一旦所有依赖的模块加载完毕后,该函数会被调用来定义该模块。定义模块的函数应该返回一个object(模块的返回值类型并没有强制为一定是个object,任何函数的返回值都是允许的)。依赖关系会以参数的形式注入到该函数中,参数列表与依赖模块的名称数组是一一对应的。

define(["./cart", "./inventory"], function(cart, inventory) {
        //return an object to define the "my/shirt" module.
        return {
            color: "blue",
            size: "large",
            addToCart: function() {
                inventory.decrement(this);
                cart.add(this);
            }
        }
    }
);
define(["my/cart", "my/inventory"],
    function(cart, inventory) {
        //return a function to define "foo/title".
        //It gets or sets the window title.
        return function(title) {
            return title ? (window.title = title) :
                   inventory.storeName + ' ' + cart.name;
        }
    }
);

兼容性情况

2.配置项

baseUrl: 所有模块的查找根路径。所以上面的示例中,"my/module"的标签src值是"/another/path/my/module.js"。当加载纯.js文件(依赖字串以/开头,或者以.js结尾,或者含有协议),不会使用baseUrl。因此a.js和b.js都会在包含上述代码段的HTML页面的同一目录下加载。

如baseUrl在配置中没有明确的设置,那么默认值是加载require.js的HTML所处的位置。如果用了data-main属性,则该路径就变成baseUrl。

baseUrl可以是一个跟 加载require.js的页面 处于不同域下的 URL,RequireJS可以跨域加载脚本。唯一的限制是使用text! plugins加载文本内容时,这些路径应跟页面同域,至少在开发时应这样。优化工具会将text! plugin资源内联,因此在使用优化工具之后你可以使用跨域引用text! plugin资源的那些资源。

paths: path映射那些不直接放置于baseUrl下的模块名。设置path时起始位置是相对于baseUrl的,除非该path设置以"/"开头或含有URL协议(如http:)。在上述的配置下,"some/module"的script标签src值是"/another/path/some/v1.0/module.js"。

用于模块名的path不应含有.js后缀,因为一个path有可能映射到一个目录。路径解析机制会自动在映射模块名到path时添加上.js后缀。在文本模版之类的场景中使用require.toUrl()时它也会添加合适的后缀。

在浏览器中运行时,可指定路径的备选(fallbacks),以实现诸如首先指定了从CDN中加载,一旦CDN加载失败则从本地位置中加载这类的机制。

shim: 为那些没有使用define()来声明依赖关系、设置模块的"浏览器全局变量注入"型脚本做依赖和导出配置。 通过require加载的模块一般都需要符合AMD规范即使用define来申明模块,但是部分时候需要加载非AMD规范的js,这时候就需要用到另一个功能:shim,shim解释起来也比较难理解,shim直接翻译为"垫",其实也是有这层意思的,主要用在两个地方

1.非AMD模块输出,将非标准的AMD模块"垫"成可用的模块,例如:在老版本的jquery中,是没有继承AMD规范的,所以不能直接require["jquery"],这时候就需要shim,比如我要是用underscore类库,但是他并没有实现AMD规范,那我们可以这样配置

require.config({
    shim: {
        "underscore" : {
            exports : "_";
        }
    }
})

这样配置后,我们就可以在其他模块中引用underscore模块:

require(["underscore"], function(_){
    _.each([1,2,3], alert);
})

2.插件形式的非AMD模块,我们经常会用到jquery插件,而且这些插件基本都不符合AMD规范,比如jquery.form插件,这时候就需要将form插件"垫"到jquery中:

require.config({
    shim: {
        "underscore" : {
            exports : "_";
        },
        "jquery.form" : {
            deps : ["jquery"]
        }
    }
})
// 可以简写成
require.config({
    shim: {
        "underscore" : {
            exports : "_";
        },
        "jquery.form" : ["jquery"]
    }
})
// 配置之后就可以使用加载插件后的jquery了
require.config(["jquery", "jquery.form"], function($){
    $(function(){
        $("#form").ajaxSubmit({...});
    })
})

"shim"配置的重要注意事项:

  • shim配置仅设置了代码的依赖关系,想要实际加载shim指定的或涉及的模块,仍然需要一个常规的require/define调用。设置shim本身不会触发代码的加载。
  • 请仅使用其他"shim"模块作为shim脚本的依赖,或那些没有依赖关系,并且在调用define()之前定义了全局变量(如jQuery或lodash)的AMD库。否则,如果你使用了一个AMD模块作为一个shim配置模块的依赖,在build之后,AMD模块可能在shim托管代码执行之前都不会被执行,这会导致错误。终极的解决方案是将所有shim托管代码都升级为含有可选的AMD define()调用。
  • 如果不能将shimmed代码升级到采用AMD define()调用, 在RequireJS2.1.11中, 优化器具有wrapShim 构建选项 来尝试自动包裹shimmed代码为define()版本。 这将改变 shimmed 依赖性的范围, 因此它不能保证可以正常工作, 但是,例如,对于Backbone的AMD版本的依赖关系,是有帮助的。 init函数将不会被AMD模块调用。 例如, 你不能用shim初始化函数来调用jQuery的noConflict。 查看映射模块使用noConflict的另一种方法。
  • 通过RequireJS在node上运行AMD模块时 , 不支持shim配置(它专门提供给优化器使用的)。 模块依赖被shimmed, 它可以在Node中失败,因为Node并不具有与浏览器相同的全局环境。 从RequireJS2.1.7开始, 它会在控制台提醒你不支持shim配置, 它可能会或可能不会正常工作。 如果你想不显示该消息, 你可以使用requirejs.config({ suppress: { nodeShim: true }})

map : 对于给定的模块前缀,使用一个不同的模块ID来加载该模块。

该手段对于某些大型项目很重要:如有两类模块需要使用不同版本的"foo",但它们之间仍需要一定的协同。

在那些基于上下文的多版本实现中很难做到这一点。而且,paths配置仅用于为模块ID设置root paths,而不是为了将一个模块ID映射到另一个。

map 示例:

requirejs.config({
    map: {
        'some/newmodule': {
            'foo': 'foo1.2'
        },
        'some/oldmodule': {
            'foo': 'foo1.0'
        }
    }
});
  • packages: 从CommonJS包(package)中加载模块。参见从包中加载模块。
  • waitSeconds: 在放弃加载一个脚本之前等待的秒数。设为0禁用等待超时。默认为7秒。
  • context: 命名一个加载上下文。这允许require.js在同一页面上加载模块的多个版本,如果每个顶层require调用都指定了一个唯一的上下文字符串。想要正确地使用,请参考多版本支持一节。
  • deps: 指定要加载的一个依赖数组。当将require设置为一个config object在加载require.js之前使用时很有用。一旦require.js被定义,这些依赖就已加载。使用deps就像调用require([]),但它在loader处理配置完毕之后就立即生效。它并不阻塞其他的require()调用,它仅是指定某些模块作为config块的一部分而异步加载的手段而已。
  • callback:在deps加载完毕后执行的函数。当将require设置为一个config object在加载require.js之前使用时很有用,其作为配置的deps数组加载完毕后为require指定的函数。
  • enforceDefine: 如果设置为true,则当一个脚本不是通过define()定义且不具备可供检查的shim导出字串值时,就会抛出错误。参考在IE中捕获加载错误一节 。
  • xhtml: 如果设置为true,则使用document.createElementNS()去创建script元素。
  • urlArgs: RequireJS获取资源时附加在URL后面的额外的query参数。作为浏览器或服务器未正确配置时的“cache bust”手段很有用。使用cache bust配置的一个示例:
urlArgs: "bust=" +  (new Date()).getTime()

在开发中这很有用,但请记得在部署到生成环境之前移除它。

  • scriptType: 指定RequireJS将script标签插入document时所用的type=""值。默认为“text/javascript”。想要启用Firefox的JavaScript 1.8特性,可使用值“text/javascript;version=1.8”。

参考博客: