AMD及requireJS

前面的话

  由CommonJS组织提出了许多新的JavaScript架构方案和标准,但愿能为前端开发提供统一的指引。AMD规范就是其中比较著名一个,全称是Asynchronous Module Definition,即异步模块加载机制,完整描述了模块的定义,依赖关系,引用关系以及加载机制。而AMD规范的做者亲自实现了符合AMD规范的requireJS。本文将详细介绍AMD及requireJSjavascript

 

AMD规范

  AMD(Asynchronous Module Definition)翻译为异步模块定义。异步强调的是,在加载模块以及模块所依赖的其它模块时,都采用异步加载的方式,避免模块加载阻塞了网页的渲染进度css

  AMD做为一个规范,只需定义其语法API,而不关心其实现。AMD规范简单到只有一个API,即define函数html

define([module-name?], [array-of-dependencies?], [module-factory-or-object]);

  module-name: 模块标识,能够省略前端

  array-of-dependencies: 所依赖的模块,能够省略java

  module-factory-or-object: 模块的实现,或者一个JavaScript对象node

  define函数具备异步性。当define函数执行时,首先会异步的去调用第二个参数中列出的依赖模块,当全部的模块被载入完成以后,若是第三个参数是一个回调函数则执行;而后告诉系统模块可用,也就通知了依赖于本身的模块本身已经可用jquery

 

加载

  使用require.js的第一步,是先去官方网站下载最新版本。下载后,假定把它放在js子目录下面,就能够加载了git

<script src="js/require.js"></script>

  HTML中的script标签在加载和执行过程当中会阻塞网页的渲染,因此通常要求尽可能将script标签放置在body元素的底部,以便加快页面显示的速度,还有一种方式就是经过异步加载的方式来加载js文件,这样能够避免js文件对html渲染的阻塞github

<script src="js/require.js" defer async></script>

 

入口文件

  require.js在加载的时候会检查data-main属性,当requireJS自身加载执行后,就会再次异步加载data-main属性指向的main.js。这个main.js是当前网页全部逻辑的入口,理想状况下,整个网页只须要这一个script标记,利用requireJS加载依赖的其它文件编程

<script data-main="scripts/main" src="js/require.js"></script>

  [注意]在main.js中所设置的脚本是异步加载的。因此若是在页面中配置了其它JS加载,则不能保证它们所依赖的JS已经加载成功

<script data-main="scripts/main" src="js/require.js"></script>
<script src="js/other.js"></script>

【内部机制】

  在RequireJS内部,会使用head.appendChild()将每个模块依赖加载为一个script标签。RequireJS等待全部的依赖加载完毕,计算出模块定义函数正确调用顺序,而后依次调用它们

 

模块

  模块不一样于传统的脚本文件,它良好地定义了一个做用域来避免全局名称空间污染。它能够显式地列出其依赖关系,并以函数(定义此模块的那个函数)参数的形式将这些依赖进行注入,而无需引用全局变量。RequireJS的模块是模块模式的一个扩展,其好处是无需全局地引用其余模块

  RequireJS的模块语法容许它尽快地加载多个模块,虽然加载的顺序不定,但依赖的顺序最终是正确的。同时由于无需建立全局变量,甚至能够作到在同一个页面上同时加载同一模块的不一样版本

  一个文件应该只定义1个模块。多个模块可使用内置优化工具将其组织打包

  若是咱们的代码不依赖任何其余模块,那么能够直接写入javascript代码

//main.js
console.log(1);

  但这样的话,就不必使用require.js了。真正常见的状况是,主模块依赖于其余模块,这时就要使用AMD规范定义的的require()函数

// main.js
require(['moduleA'], function(a){
  console.log(a);
});
//moduleA.js
define(function(){
    return 1;
})

  这里抛出一个问题,为何主模块使用的是require()函数,而模块moduleA使用define()函数呢?由于define()定义的模块能够被调用,而require()不能够。主模块main.js是入口文件,须要调用别的模块,而不须要被别的模块调用,因此使用require()或define()均可以。而moduleA须要被调用,因此只能使用define()

  若是把moduleA.js中的define()方法改成require()方法,则返回undefined

// main.js
require(['moduleA'], function(a){
  console.log(a);
});
//moduleA.js
require(function(){
    return 1;
})

【简单的值对】

  上面的模块moduleA中,回调函数返回了一个数字。而实际上,模块能够有多种形式,好比一个简单的值对

define({
    color: "black",
    size: "unisize"
});

  返回的结果以下:

【函数式定义】

  若是一个模块没有任何依赖,但须要一个作setup工做的函数,则在define()中定义该函数,并将其传给define()

define(function () {
    //Do setup work here
    return {
        color: "black",
        size: "unisize"
    }
});

  返回的结果以下:

【存在依赖的函数式定义】

  若是模块存在依赖:则第一个参数是依赖的名称数组;第二个参数是函数,在模块的全部依赖加载完毕后,该函数会被调用来定义该模块,所以该模块应该返回一个定义了本模块的object。依赖关系会以参数的形式注入到该函数上,参数列表与依赖名称列表一一对应

//moduleA.js
define(['moduleB'], function(b) {
    var num = 10;
    return b.add(num);
    }
);
////moduleB.js
define({
    add: function(n){
        return n+1;
    }
});

【命名模块】

  define()中能够包含一个模块名称做为首个参数

//moduleA.js
define("moduleA",['moduleB'], function(b) {
    var num = 10;
    return b.add(num);
    }
);

  这些常由优化工具生成。也能够本身显式指定模块名称,但这使模块更不具有移植性——就是说若将文件移动到其余目录下,就得重命名。通常最好避免对模块硬编码,而是交给优化工具去生成。优化工具须要生成模块名以将多个模块打成一个包,加快到浏览器的载入速度

 

路径配置

  html中的base元素用于指定文档里全部相对URL地址的基础URL,requireJS的baseUrl跟这个base元素起的做用是相似的,因为requireJS老是动态地请求依赖的JS文件,因此必然涉及到一个JS文件的路径解析问题,requireJS默认采用一种baseUrl + moduleID的解析方式,requireJS对它的处理遵循以下规则:

  一、在没有使用data-main和config的状况下,baseUrl默认为当前页面的目录

  二、在有data-main的状况下,main.js前面的部分就是baseUrl,好比上面的js/

  三、在有config的状况下,baseUrl以config配置的为准

  上述三种方式,优先级由低到高排列

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

<script data-main="js/main.js" src="scripts/require.js"></script>

  在模块章节的示例中,代码以下所示

// main.js
require(['moduleA'], function(a){
  console.log(a);
});
//moduleA.js
define(function(){
    return 1;
})

  入口文件main.js依赖于moduleA,直接写成['moduleA'],默认状况下,require.js假定moduleA与main.js在同一个目录,即'js/moduleA.js',文件名为moduleA.js,而后自动加载

  使用require.config()方法,咱们能够对模块的加载行为进行自定义。require.config()就写在主模块(main.js)的头部。参数就是一个对象,这个对象的paths属性指定各个模块的加载路径

  下面在demo文件夹下新建一个test文件夹,并在test文件夹下新建一个moduleA.js文件,内容以下

//moduleA.js
define(function(){
    return 2;
})

  而在原来的js文件夹下,依然存在一个moduleA.js文件,内容以下

//moduleA.js
define(function() {
    return 1;
});

  当js文件夹下的main.js进行config配置时

// main.js
require.config({
    baseUrl: 'test'
})
require(['moduleA'], function(a){
    console.log(a);
});

   结果为2,说明识别的是'test/moduleA.js'文件

  当js文件夹下的main.js不进行config配置时

// main.js
require(['moduleA'], function(a){
    console.log(a);
});

  结果为1,说明识别的是'js/moduleA.js'文件

  RequireJS默认假定全部的依赖资源都是js脚本,所以无需在module ID上再加".js"后缀,RequireJS在进行module ID到path的解析时会自动补上后缀 

  若是一个模块的路径比较深,或者文件名特别长,好比'js/lib/moduleA.min.js',则可使用config配置对象中的paths属性

// main.js
require.config({
    paths:{
        'moduleA':'lib/moduleA.min'
    }
})
require(['moduleA'], function(a){
    console.log(a);
});

//moduleA-min.js
define(function(){
    return 3;
})

  结果为3

  要注意的是,这里的paths的'moduleA'设置的是'lib/moduleA.min',而不是'js/lib/moduleA.min',是由于requireJS中的文件解析是一个"baseUrl + paths"的解析过程

  在index.html的入口文件设置的是'js/main',因此baseURL是'js'。所以'baseUrl + paths' = 'js/lib/moduleA.min'

<script src="require.js" data-main="js/main" defer async></script>

  若是在config配置对象中设置了baseUrl,则以此为准

// main.js
require.config({
    baseUrl: 'js/lib',
    paths:{
        'moduleA':'moduleA.min'
    }
})
require(['moduleA'], function(a){
    console.log(a);
});

  结果一样为3,baseURL是'js/lib',paths是'moduleA.min'。所以'baseUrl + paths' = 'js/lib/moduleA.min'

  若是一个module ID符合下述规则之一,其ID解析会避开常规的"baseUrl + paths"配置,而是直接将其加载为一个相对于当前HTML文档的脚本:一、以 ".js" 结束;二、包含 URL 协议,如 "http:" or "https:"

  以下所示,require()函数所依赖的模块路径为'js/moduleA.js'

// main.js
require.config({
    baseUrl: 'js/lib',
    paths:{
        'moduleA':'moduleA.min'
    }
})
require(['js/moduleA.js'], function(a){
    console.log(a);
});

  而该文件的代码以下,路径为'js/moduleA.js',而不是'js/lib/moduleA.min',因此,最终结果为1

//moduleA.js
define(function() {
    return 1;
});

  通常来讲,最好仍是使用baseUrl及"paths" config去设置module ID。它会带来额外的灵活性,如便于脚本的重命名、重定位等。 同时,为了不凌乱的配置,最好不要使用多级嵌套的目录层次来组织代码,而是要么将全部的脚本都放置到baseUrl中,要么分置为项目库/第三方库的一个扁平结构,以下

www/
    index.html
    js/
        app/
            sub.js
        lib/
            jquery.js
            canvas.js
        main.js

 

CommonJS

  前面提到过,commonJS主要应用于服务器端编程,如nodejs。使用打包工具Browserify能够对CommonJS进行格式转换,使其能够在浏览器端进行

  而requireJS支持一种简单包装CommonJS的方式,只要在commonJS代码的外层简单包裹一层函数,就能够在浏览器端直接运行

define(function(require, exports, module) {

});

  若是该模块还依赖其余模块,如依赖模块moduleA,则代码以下

define(['moduleA'],function(require, exports, module) {

});

  a.js和b.js的commonJS形式的代码以下

// a.js
var a = 100;
module.exports.a = a;

// b.js
var result = require('./a');
console.log(result.a);

  index.html直接引用b.js会报错,提示require没有被定义

<script src="b.js"></script> 

  将a.js和b.js进行改造以后,代码以下

// a.js
define(function(require, exports, module) {
    var a = 100;
    module.exports.a = a;
});

// b.js
define(function(require, exports, module) {
    var result = require('./a');
    console.log(result.a);
});

  index.html将入口文件设置为'js/b',则结果为100

<script src="require.js" data-main="js/b" defer async></script>

 

懒加载

  有以下例子,入口文件main.js代码以下

// main.js
require(['a'], function(a){
    console.log('main');
    document.onclick = function(){
        a.test();
    }
});

  所依赖的模块a.js的代码以下

define(function(){
    console.log('a');
    return {
        test : function(){
            console.log('a.test');
        }
    }
})

  在浏览器端执行时,即便不点击页面,浏览器也会下载a.js文件。这个性能消耗是不容忽视的

  AMD保留了commonjs中的require、exprots、module这三个功能。能够不把依赖罗列在dependencies数组中。而是在代码中用require来引入

  重写后的代码以下

// main.js
define(function(){
    console.log('main');
    document.onclick = function(){
        require(['a'],function(a){
            a.test();
        });
    }
});
//a.js
define(function(){
  console.log('a');
return { test : function(){ console.log('a.test'); } } })

  在浏览器端执行时,若是不点击页面,浏览器就不会下载a.js文件,这样就实现懒加载

 

其余配置

  在requireJS中,除了路径配置以外,还有一些其余配置

【配置设置】

  在前面的例子中,咱们配置requireJS中的路径是经过入口文件main.js中的config对象来配置的。实际上,不经过入口文件,也能够进行requireJS的配置

  一、在index.html文件嵌入javascript代码

  在HTML文件中,加载requireJS文件以后,当即对requireJS进行配置,至关于将main.js文件变为内嵌的javascript文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
<script src="require.js"></script>
<script>
require.config({
    baseUrl: 'js/lib',
    paths:{
        'moduleA':'moduleA.min'
    }
})    
require(['moduleA'], function(a){
    console.log(a);
});
</script>
</body>
</html>

  二、将配置做为全局变量"require"在require.js加载以前进行定义,它会被自动应用

  这里有一个问题是,若是require做为全局变量被提早定义,则data-main入口文件,是以baseUrl为基础进行设置的

  [注意]使用 var require = {} 的形式而不是 window.require = {}的形式。后者在IE中运行不正常

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
<script>
var require = {
    baseUrl: 'js/lib',
    paths:{
        'moduleA':'moduleA.min'
    }    
}    
</script>
<script src="require.js" data-main="../main"></script>
</body>
</html>

【shim】

  shim属性为那些没有使用define()来声明依赖关系、设置模块的"浏览器全局变量注入"型脚本作依赖和导出配置,即加载非规范的模块

  举例来讲,underscore和backbone这两个库,都没有采用AMD规范编写。若是要加载它们的话,必须先定义它们的特征。具体来讲,每一个模块要定义(1)exports值(输出的变量名),代表这个模块外部调用时的名称;(2)deps数组,代表该模块的依赖性

  经过以下配置后,如今能够经过_调用underscore的api,使用Backbone来调用backbone的api

  require.config({
    shim: {

      'underscore':{
        exports: '_'
      },
      'backbone': {
        deps: ['underscore', 'jquery'],
        exports: 'Backbone'
      }
    }
  });

  jQuery的插件能够以下这样定义,如今能够经过jQuery.fn.scroll来调用该插件的api

  shim: {
    'jquery.scroll': {
      deps: ['jquery'],
      exports: 'jQuery.fn.scroll'
    }
  }

 

插件

  require.js还提供一系列插件,实现一些特定的功能

【dom ready】 

  RequireJS加载模块速度很快,颇有可能在页面DOM Ready以前脚本已经加载完毕。须要与DOM交互的工做应等待DOM Ready。现代的浏览器经过DOMContentLoaded事件来知会

  可是,不是全部的浏览器都支持DOMContentLoaded。domReady模块实现了一个跨浏览器的方法来断定什么时候DOM已经ready

// main.js
require(['domready!'], function(){
    console.log('ready');
});

【text】

  text插件能够用来加载如.html、.css等文本文件,能够经过该插件来实现完整组件(结构+逻辑+样式)的组件化开发

require(["some/module", "text!some/module.html", "text!some/module.css"],
    function(module, html, css) {
    }
);
相关文章
相关标签/搜索