[TOC]

AMD规范

模块的重要作用:有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块。

目前,通行的Javascript模块规范共有两种:CommonJS和AMD。

nodejs的模块系统,就是参照CommonJS规范实现的,由于nodejs是服务器环境,有了服务器端模块以后,很自然地,大家就想要客户端模块。而且最好两者能够兼容,一个模块不用修改,在服务器和浏览器都可以运行。

nodejs的所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。

因此,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous)。这就是AMD规范诞生的背景。

1. AMD说明

AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

define和require这两个定义模块、调用模块的方法,合称为AMD模式。它的模块定义的方法非常清晰,不会污染全局环境,能够清楚地显示依赖关系。

AMD模式可以用于浏览器环境,并且允许非同步加载模块,也可以根据需要动态加载模块。

目前,主要有两个Javascript库实现了AMD规范:require.jscurl.js

2. RequireJS库实现了AMD标准

RequireJS是一个工具库,主要用于客户端的模块管理。它可以让客户端的代码分成一个个模块,实现异步或动态加载,从而提高代码的性能和可维护性。

2.1 require.js的加载

首先,需要去require官网下载require.js文件,比如2.3.6版本,下载好后,放到一个文件夹中,然后在文件夹中新建一个html文件:

<!DOCTYPE html>
<html lang="en">
<head>
  <title>require测试</title>
</head>
<script data-main="main" src="require.js"></script>
</html>

在html文件中的script元素中,src属性加载了require文件,data-main属性指定网页程序的主模块,即项目文件夹中的main.js文件。main.js文件内容:

console.log('主函数文件1');
require([], function(params) {
  console.log('主函数文件2');
})
console.log('主函数文件3');

在浏览器中打开html文件,打开终端就能看见输出内容了。

3. 模块定义:define方法

define方法用于定义模块,RequireJS要求每个模块放在一个单独的文件里。

define(id?, dependencies?, factory);

define方法有三个参数:

id:第一个参数,id,是个字符串。它指的是定义中模块的名字,这个参数是可选的。如果没有提供该参数,模块的名字应该默认为模块加载器请求的指定脚本的名字。如果提供了该参数,模块名必须是“顶级”的和绝对的(不允许相对名字)。

dependencies:第二个参数,是个定义中模块所依赖模块的数组。这个参数是可选的。依赖模块必须根据模块的工厂方法优先级执行,并且执行的结果应该按照依赖数组中的位置顺序以参数的形式传入(定义中模块的)工厂方法中。

factory:第三个参数,为模块初始化要执行的函数或对象。这个参数必填。如果为函数,它应该只被执行一次。如果是对象,此对象应该为模块的输出值。

模块按照是否依赖其他模块,可以分为两种情况:1、不依赖其他模块的独立模块。2、依赖其他模块的模块。

3.1 独立模块的定义

如果被定义的模块是一个独立模块,不需要依赖任何其他模块,可以直接用define方法生成。

define定义的模块可以返回任何值,不限于对象。

define({
    method1: function() {},
    method2: function() {},
}); // 定义拥有method1、method2两个方法的模块

另一种等价的写法是,把对象写成一个函数,该函数的返回值就是输出的模块。

define(function () {
  // 这种写法自由度更高一点,可以在函数体内写一些模块初始化代码
  return {
    method1: function() {},
    method2: function() {},
  };
});

3.2 非独立模块的定义

如果被定义的模块需要依赖其他模块,则define方法必须采用下面的格式:

define(['module1', 'module2'], function(m1, m2) {
  return {
    method: function() {
      m1.methodA();
      m2.methodB();
    }
  };
});

define方法的第一个参数是一个数组,它的成员是当前模块所依赖的模块。比如上面的代码中定义的新模块依赖于module1模块和module2模块,只有先加载这两个模块,新模块才能正常运行。一般情况下,module1模块和module2模块指的是,当前目录下的module1.js文件和module2.js文件,等同于写成[’./module1’, ‘./module2’]。

define方法的第二个参数是一个函数,当前面数组的所有成员加载成功后,它将被调用。它的参数与数组的成员一一对应,比如function(m1, m2)就表示,这个函数的第一个参数m1对应module1模块,第二个参数m2对应module2模块。这个函数必须返回一个对象,供其他模块调用。

上面代码表示新模块返回一个对象,该对象的method方法就是外部调用的接口,menthod方法内部调用了m1模块的methodA方法和m2模块的methodB方法。

需要注意的是,回调函数必须返回一个对象,这个对象就是你定义的模块。

如果依赖的模块很多,参数与模块一一对应的写法非常麻烦。为了避免像上面代码那样繁琐的写法,RequireJS提供一种更简单的写法。

define(
    function (require) {
        var dep1 = require('dep1'),
            dep2 = require('dep2'),
            dep3 = require('dep3'),
            dep4 = require('dep4'),
            dep5 = require('dep5'),
            dep6 = require('dep6'),
            dep7 = require('dep7'),
            ...
    }
});

一个例子,通过判断浏览器是否为IE,而选择加载zepto或jQuery。

define(('__proto__' in {} ? ['zepto'] : ['jquery']), function($) {
    return $;
});

上面代码定义了一个中间模块,该模块先判断浏览器是否支持__proto__属性(除了IE,其他浏览器都支持),如果返回true,就加载zepto库,否则加载jQuery库。

4. 调用模块:require方法

require方法用于调用模块。它的参数与define方法类似。

第一个参数,是一个表示依赖关系的数组。
第二个参数,是一个回调函数,当依赖模块都加载完成后,会当成参数传给回调函数,然后执行这个回调函数。
第三个参数,处理错误的回调函数,接受一个error对象作为参数。
require对象还允许指定一个全局性的Error事件的监听函数。所有没有被上面的方法捕获的错误,都会被触发这个监听函数。

// 所有没有被require第三个参数回调方法捕获的错误,都会被触发这个监听函数。
requirejs.onError = function (err) {
};
require(["backbone"], function ( Backbone ) {
    return Backbone.View.extend({ /* ... */ });
  }, 
  function (err) {
    // ...
  }
);

// 第一个参数灵活调用:判断浏览器是否支持原生JSON,
// 如果支持,则传入undefined,否则传入util目录下的json2模块
require( [ window.JSON ? undefined : 'util/json2' ], function ( JSON ) {
  JSON = JSON || window.JSON;
  console.log( JSON.parse( '{ "JSON" : "HERE" }' ) );
});

require方法也可以用在define方法内部。

define(function (require) {
  var otherModule = require('otherModule');
});

// 动态加载:内部加载了foo和bar两个模块,在没有加载完成前,isReady属性值为false,
//				  加载完成后就变成了true。因此,可以根据isReady属性的值,决定下一步的动作。
define(function ( require ) {
  var isReady = false, foobar;
  require(['foo', 'bar'], function (foo, bar) {
    isReady = true;
    foobar = foo() + bar();
  });
  return {
    isReady: isReady,
    foobar: foobar
  };
});

其他一些例子:

// 返回一个promise对象,可以在该对象的then方法,指定下一步的动作。
define(['lib/Deferred'], function( Deferred ){
  var defer = new Deferred(); 
  require(['lib/templates/?index.html','lib/data/?stats'],
          function( template, data ){
    defer.resolve({ template: template, data:data });
  }
         );
  return defer.promise();
});

// JSONP模式可以直接在require中调用,方法是指定JSONP的callback参数为define。
require( [ 
  "http://someapi.com/foo?callback=define"
], function (data) {
  console.log(data);
});

5. 配置require.js:config方法

require方法本身也是一个对象,它带有一个config方法,用来配置require.js运行参数。config方法接受一个对象作为参数。对象有以下主要成员:

1、paths:

paths参数指定各个模块的位置。这个位置可以是同一个服务器上的相对位置,也可以是外部网址。可以为每个模块定义多个位置,如果第一个位置加载失败,则加载第二个位置,上面的示例就表示如果CDN加载失败,则加载服务器上的备用脚本。需要注意的是,指定本地文件路径时,可以省略文件最后的js后缀名。

2、baseUrl:

baseUrl参数指定本地模块位置的基准目录,即本地模块的路径是相对于哪个目录的。该属性通常由require.js加载时的data-main属性指定。

3、shim:

有些库不是AMD兼容的,这时就需要指定shim属性的值。shim可以理解成“垫片”,用来帮助require.js加载非AMD规范的库。

require.config({
  baseUrl: 'src',
  paths: {
    moment: '/moment-2.8.3/moment',
    jquery: [
      '//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js',
      'lib/jquery'
    ],
    "backbone": "vendor/backbone", // 非AMD规范的库
    "underscore": "vendor/underscore" // 非AMD规范的库
  },
  shim: {
    "backbone": {
      deps: [ "underscore" ], // backbone依赖于underscore
      exports: "Backbone" // 导出的名字,别的模块使用这个名字引用这个模块
    },
    "underscore": {
      exports: "_"
    }
  }
});

5.1 关于paths属性中名字问题

如果一个模块在定义的时候,已经指定名字了:

define('jquery', [], function() { ... });

当在require的config中的paths使用别的名字引用:

myJquery: 'lib/jquery'

当引用文件后,在jquery.js文件中声明的名字jquery与配置的名字myJquery不同,便不会把jquery模块赋值给myJquery,导致myJquery的值是undefined。

所以我们在使用一个第三方的时候,一定要注意它是否声明了一个确定的模块名。

如果自己定义的模块,没有指定模块名字:

define([], function() {
});

我们可以在 requirejs.config 里,使用任意一个模块名来引用它。这样的话,就让我们的命名非常自由。

5.2 config的使用

第一种在html页面中使用script形式使用:

<!DOCTYPE html>
<html lang="en">
<script src="require.js"></script>
<script>
  require.config({
    baseUrl: 'src',
    paths: {
      jquery: './dep/jquery/dist/jquery.min',
    }
  });
</script>
<script>
  require(['main'], function () {
    // 执行
  });
</script>
</html>

第二种是在入口文件main.js里面定义:

require.config({
  baseUrl: 'src',
  paths: {
    jquery: './dep/jquery/dist/jquery.min',
  }
});

require([], function(params) {
  console.log('主函数文件');
})

6. require插件

RequireJS允许使用插件,加载各种格式的数据。完整的插件清单可以查看官方网站

下面是插入文本数据所使用的text插件的例子。

define([
  'backbone',
  'text!templates.html'
], function( Backbone, template ){
  // ...
});

上面代码加载的第一个模块是backbone,第二个模块则是一个文本,用’text!’表示。该文本作为字符串,存放在回调函数的template变量中。

参考资料

Javascript模块化编程(二):AMD规范

Javascript模块化编程(三):require.js的用法

RequireJS和AMD规范 阮一峰

AMD (中文版)

requirejs 官网

Last Updated: 5/27/2021, 8:44:20 AM