参考资料
RequireJS 中文网
Javascript模块化编程(三):require.js的用法——阮一峰
前言
本人菜鸟,入IT只为当鼓励师。本编文章意在总结 RequireJS 的目的和用法。
一、require.js 的 目的
1.1 管理模块间的依赖性,便于代码的编写和维护
RequireJS 鼓励代码的模块化,主要目的是为了代码的模块化。
-
如果一个文件需要依赖另外一些文件中定义的东西时,这个文件依赖的所有文件都要在它之前导入。过于复杂的系统,依赖关系可能出现相互交叉的情况,依赖关系的管理就更加难了。
例如:51行到60行,分别是编写的10个模块。第61行引入的 main.js 是主模块,编写的是程序运行的过程。main.js 这个文件用到了从51行到60行的模块,而它并没有被其他模块使用,故可以且必须放在最末尾导入。而上面的10个模块,要确保模块的依赖在该模块导入之前就要导入,为了解除导入顺序的限制,只能让各模块间解耦。
- 而 RequireJS 使用了不同于传统
标签 的脚本加载步骤。后续我们会看到 其引入js文件的方式是怎样的。
1.2 实现js文件的异步加载,避免网页失去响应
使用了 标签 这种传统的引入js文件的方式,在加载js文件的时候,浏览器会停止网页渲染,加载文件越多,网页失去响应的时间就会越长。
若读者对js模块化还不甚理解,可先阅读本小姐写的另一篇文章 浅谈JavaScript 模块化。
二、下载RequireJS
2.1 链接下载
require.js 2.1.11
require.js 2.1.11 压缩版
r.js 2.1.11
r.js 可以让你进行优化并能够在 Node, Rhino 或者 xpcshell 中运行。
2.2 npm下载
若当前目录下没有找到 package.json 文件,则输入 package init
,输入完后设置好一些参数,即可生成 package.json 文件。
输入 npm install requirejs --save-dev
,下载完成后,可以在
package.json 文件中找到如下这行依赖。
在当前目录下找到 node_modules\requirejs 路径,即可找到 require.js 文件,找到 node_modules\requirejs\bin 路径,即可找到 r.js 文件。
三、require.js 的加载
下载完 require.js 文件后,为了方便,我把文件复制到了 ./scripts/libs
目录中(. 表示当前目录,相对于 index.html 文件)。
RequireJS以一个相对于 baseUrl 的地址来加载所有的代码。如果没有显式指定 config
及 data-main
,则默认的 baseUrl 为包含 RequireJS 的那个 HTML 页面的所属目录。
3.1 引入 require.js 文件
加载这个文件,因为浏览器是同步加载的,也可能会造成网页失去响应。
那么,你可以把它放在网页底部加载:
或者,你可以将它写成:
async属性表示该文件需异步加载,避免网页失去响应。但IE浏览器不支持async,只支持defer,所以把defer也写上。
3.2 引入网页程序的主模块
data-main 入口点
页面顶层 标签含有一个特殊的属性
data-main
,require.js使用它来启动脚本加载过程,即指定网页程序的主模块。主模块文件的命名一般可为 main.js。我们把 script 目录下的 main.js 引入:
注意:你在main.js中所设置的脚本是异步加载的。所以如果你在页面中配置了其它JS加载,则不能保证它们所依赖的JS已经加载成功。例如:
// main.js
require.config({
paths: {
foo: 'libs/foo-1.1.3'
}
});
// other.js
/**
* 由于 main.js 中的 foo 模块是异步加载的,
* other.js 可能在 foo 模块加载完之前就已经加载执行,
* 此时 other.js 中的 foo 模块 指的是 scripts/foo.js 而非 libs/foo-1.1.3。
*/
require( ['foo'], function( foo ) {
});
四、主模块的写法
main.js "主模块",是整个网页的入口代码。它有点像 C语言 的 main() 函数,所有代码都从这里开始运行。
- main.js 若不依赖其他模块,则可以直接写js代码:
// main.js
console.log("加载成功!");
这就相当于 C语言 中,直接把代码写在 main() 函数中:
void main(int arg[], char* agvs[]) {
printf("加载成功!");
} - 但正常情况下,主模块会依赖于其他模块,这时就要使用AMD规范定义的 require() 函数。
// main.js
require(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){
// 运行代码
});
/**
* dependency_array:依赖数组
* callback_func:回调函数
* require(dependency_array, callback_func);
/
require() 函数接收两个参数:
①第一个参数是一个数组,表示所依赖的模块有哪些;
②第二个参数是一个回调函数*,当前面指定的模块都加载成功后,它将被调用。加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块。
require() 异步加载 moduleA、moduleB 和 moduleC,浏览器不会失去响应;它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。
五、模块的加载
使用 require.config()
方法,我们可以对模块的加载行为进行自定义。require.config()
方法接收一个参数,该参数为包含一些指定属性的原生对象,我们可通过设置对应属性的值来修改加载行为。这些属性有:baseUrl
、path
、shim
、map
、config
等等。
5.1 设置 path(module ID)
RequireJS 鼓励在使用脚本时 以 module ID 替代 URL 地址,默认假定所有的依赖资源都是 js 脚本,因此无需在 module ID上再加 ".js" 后缀。
我们可以设置 require.config()
方法 中 传入对象 的 path
属性,来设置各模块的 module ID。
requirejs.config({
paths: {
Bird: 'scripts/views/Bird',
Block: 'scripts/views/Block',
Counter: 'scripts/views/Counter',
GameBg: 'scripts/views/GameBg',
GameOver: 'scripts/views/GameOver',
GrassLand: 'scripts/views/GrassLand',
StartBtn: 'scripts/views/StartBtn',
StartInfo: 'scripts/views/StartInfo',
RandomCreator: 'scripts/utils/RandomCreator'
}
});
// 加载 scripts/views/Bird.js 和 scripts/utils/randomCreator.js
require(['Bird', 'randomCreator'], function (Bird, randomCreator) {
// 代码
});
5.2 设置 baseUrl
baseUrl 可通过 requirejs.config()
手动设置。如果没有显式指定 config
及 data-main
,则默认的 baseUrl
为包含 RequireJS 的那个 HTML 页面的所属目录。设置以后,加载模块时,路径都会被解析为 baseUrl + path
。若想避开该解析过程,设置 path
时可以:
- 以 ".js" 结束;
- 以 "/" 开始;
- 包含 URL 协议, 如 "http:" or "https:"。
requirejs.config({
baseUrl: "scripts/views",
paths: {
Bird: 'scripts/views/Bird',
Block: 'scripts/views/Block',
Counter: 'scripts/views/Counter',
GameBg: 'scripts/views/GameBg',
GameOver: 'scripts/views/GameOver',
GrassLand: 'scripts/views/GrassLand',
StartBtn: 'scripts/views/StartBtn',
StartInfo: 'scripts/views/StartInfo',
RandomCreator: '../utils/RandomCreator'
}
});
// 加载 scripts/views/Bird.js 和 scripts/utils/randomCreator.js
require(['Bird', 'RandomCreator'], function (Bird, randomCreator) {
// 代码
});
如果某个模块在另一台主机上,也可直接指定其网址:
require.config({
paths: {
"jquery": "https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min"
}
});
require.js 要求,每个模块是一个单独的 js 文件。如果加载多个模块,就会发出多次HTTP请求,影响网页的加载速度。因此,require.js 提供了一个 优化工具,当模块部署完毕后,可用该工具将多个模块合并成一个文件,减少HTTP请求数。
5.3 设置 shim
理论上,require.js 加载的模块,必须是按照AMD规范、用 define()
函数定义的模块。但是实际上,虽然已经有一部分流行的函数库(比如jQuery)符合AMD规范,更多的库并不符合。
为了能够加载非规范的模块,可设置 require.config()
方法 传入对象 中的 shim
属性。shim
属性的值是一个对象,这个对象里包含一些模板对象,而这些模板对象有两个属性:
① exports值(输出的变量名):表明这个模块外部调用时的名称;
② deps数组:表明该模块的依赖什么模块。
require.config({
shim: {
// underscore 模块,外部调用时使用 _ 指代该模块
'underscore': {
exports: '_'
},
// backbone模块,外部调用时使用 Backbone 指代该模块
// 这个模块依赖于 underscore , jquery 模块
'backbone': {
deps: ['underscore', 'jquery'],
exports: 'Backbone'
}
}
});
5.4 设置 map
对于给定的模块前缀,使用一个不同的模块ID来加载该模块。
requirejs.config({
map: {
'some/newmodule': {
'foo': 'foo1.2'
},
'some/oldmodule': {
'foo': 'foo1.0'
}
}
});
另外在map中支持
*
,意思是 "对于所有的模块加载,使用本map配置"。如果还有更细化的map配置,会优先于
*
配置。
requirejs.config({
map: {
'*': {
'foo': 'foo1.2'
},
'some/oldmodule': {
'foo': 'foo1.0'
}
}
});
意思是: 除了 some/oldmodule
外的所有模块,当要用 foo
时,使用 foo1.2
来替代。对于 some/oldmodule
自己,则使用 foo1.0
。
5.5 设置 config
若想设置一些配置信息(变量,方法)供对应模块使用,可以设置 require.config()
方法 传入对象 中的 config
属性。要获取这些信息的模块可以加载特殊的依赖 module
,并调用 module.config()
。如:
// main.js
requirejs.config({
config: {
'bar': {
size: 'large'
},
'baz': {
color: 'blue'
}
}
});
// bar.js
define( function (require, exports, module) {
var size = module.config().size;
console.log(size); // large
});
// baz.js
define(['module'], function (module) {
var color = module.config().color;
console.log(color); // blue
});
六、定义模块
模块不同于传统的脚本文件,它良好地定义了一个作用域来避免全局名称空间污染。它可以显式地列出其依赖关系,并以函数(定义此模块的那个函数)参数的形式将这些依赖进行注入,而无需引用全局变量。
RequireJS 定义模块采用AMD规范,使用 define()
方法。
假定现在有一个GameBg.js文件,它定义了一个 GameBg 模块:
// GameBg.js
define( function () {
var GameBg = {};
return GameBg;
});
一个磁盘文件应该只定义 1 个模块。多个模块可以使用内置优化工具将其组织打包。
6.1 简单的值对
如果一个模块仅含值对,没有任何依赖,可在 define()
中直接定义这些值对:
define({
color: "black",
size: "unisize"
});
6.2 函数式定义
如果一个模块没有任何依赖,但需要一个做初始化或配置工作的函数,则在 define()
中定义该函数,并将其传给 define()
:
// GameBg.js
define( function () {
// 一些初始化或配置工作
// ...
// 界面背景单例对象
var GameBg = (function () {
var _element = document.querySelector('#game-bg'),
_width = document.querySelector('#game-bg').offsetWidth,
_height = document.querySelector('#game-bg').offsetHeight;
return {
getElement : function () {
return _element;
},
getWidth : function () {
return _width;
},
getHeight : function () {
return _height;
},
// ...
}
})();
return GameBg;
});
6.3 存在依赖的函数式定义
模块函数以参数 GameBg
及 GrassLand
使用这两个以 ./scripts/views/GameBg
及 ./scripts/views/GrassLand
名称指定的模块。在这两个模块加载完毕之前,模块函数不会被调用。RequireJS 不鼓励模块定义全局变量,返回的 object 定义了 Bird
模块。这种定义模式下,Bird
不作为一个全局变量而存在。
// Bird.js
define(['GameBg', 'GrassLand'], function (GameBg, GrassLand) {
// Bird对象的构造函数
var Bird = function (idName) {
// ...
};
return Bird;
});
6.4 将模块定义为一个函数
模块的返回值类型 不一定是 一个对象,也可以是 一个函数。此处是一个返回了函数的模块定义:
// Bird.js
define(['GameBg', 'GrassLand'], function (GameBg, GrassLand) {
// Bird对象的构造函数
return function (idName) {
// ...
};
});
6.5 简单包装CommonJS来定义模块
define(function(require, exports, module) {
var a = require('a'),
b = require('b');
// 返回模块
return function () {};
});
6.6 定义一个命名模块
你可能会看到一些define()中包含了一个模块名称作为首个参数。
这些常由优化工具生成。你也可以自己显式指定模块名称,但这使模块更不具备移植性——就是说若你将文件移动到其他目录下,你就得重命名。一般最好避免对模块硬编码,而是交给优化工具去生成。优化工具需要生成模块名以将多个模块打成一个包,加快到浏览器的载人速度。
define("FlappyBird",
["GameBg", "GrassLand"],
function(GameBg, GrassLand) {
//Define foo/title object in here.
}
);