高级使用§ 4
从包中加载模块§ 4.1
RequireJS支持从CommonJS包结构中加载模块,但需要一些额外的配置。具体地,支持如下的CommonJS包特性:
- 一个包可以关联一个模块名/前缀。
- package config可为特定的包指定下述属性:
- name: 包名(用于模块名/前缀映射)
- location: 磁盘上的位置。位置是相对于配置中的baseUrl值,除非它们包含协议或以“/”开头
- main: 当以“包名”发起require调用后,所应用的一个包内的模块。默认为“main”,除非在此处做了另外设定。该值是相对于包目录的。
重要事项
- 虽然包可以有CommonJS的目录结构,但模块本身应为RequireJS可理解的模块格式。例外是:如果你在用r.js Node适配器,模块可以是传统的CommonJS模块格式。你可以使用CommonJS转换工具来将传统的CommonJS模块转换为RequireJS所用的异步模块格式。
- 一个项目上下文中仅能使用包的一个版本。你可以使用RequireJS的多版本支持来加载两个不同的模块上下文;但若你想在同一个上下文中使用依赖了不同版本的包C的包A和B,就会有问题。未来可能会解决此问题。
如果你使用了类似于入门指导中的项目布局,你的web项目应大致以如下的布局开始(基于Node/Rhino的项目也是类似的,只不过使用scripts目录中的内容作为项目的顶层目录):
- project-directory/
- project.html
- scripts/
- require.js
而下面的示例中使用了两个包,cart及store:
- project-directory/
- project.html
- scripts/
- cart/
- main.js
- store/
- main.js
- util.js
- main.js
- require.js
- cart/
project.html 会有如下的一个script标签:
<script data-main="scripts/main" src="scripts/require.js"></script>
这会指示require.js去加载scripts/main.js。main.js使用“packages”配置项来设置相对于require.js的各个包,此例中是源码包“cart”及“store”:
//main.js contents
//Pass a config object to require
require.config({
"packages": ["cart", "store"]
});
require(["cart", "store", "store/util"],
function (cart, store, util) {
//use the modules as usual.
});
对“cart”的依赖请求会从scripts/cart/main.js中加载,因为“main”是RequireJS默认的包主模块。对“store/util”的依赖请求会从scripts/store/util.js加载。
如果“store”包不采用“main.js”约定,如下面的结构:
- project-directory/
- project.html
- scripts/
- cart/
- main.js
- store/
- store.js
- util.js
- main.js
- package.json
- require.js
- cart/
则RequireJS的配置应如下:
require.config({
packages: [
"cart",
{
name: "store",
main: "store"
}
]
});
减少麻烦期间,强烈建议包结构遵从“main.js”约定。
多版本支持§ 4.2
如配置项一节中所述,可以在同一页面上以不同的“上下文”配置项加载同一模块的不同版本。require.config()返回了一个使用该上下文配置的require函数。下面是一个加载不同版本(alpha及beta)模块的示例(取自test文件中):
<script src="../require.js"></script>
<script>
var reqOne = require.config({
context: "version1",
baseUrl: "version1"
});
reqOne(["require", "alpha", "beta",],
function(require, alpha, beta) {
log("alpha version is: " + alpha.version); //prints 1
log("beta version is: " + beta.version); //prints 1
setTimeout(function() {
require(["omega"],
function(omega) {
log("version1 omega loaded with version: " +
omega.version); //prints 1
}
);
}, 100);
});
var reqTwo = require.config({
context: "version2",
baseUrl: "version2"
});
reqTwo(["require", "alpha", "beta"],
function(require, alpha, beta) {
log("alpha version is: " + alpha.version); //prints 2
log("beta version is: " + beta.version); //prints 2
setTimeout(function() {
require(["omega"],
function(omega) {
log("version2 omega loaded with version: " +
omega.version); //prints 2
}
);
}, 100);
});
</script>
注意“require”被指定为模块的一个依赖,这就允许传递给函数回调的require()使用正确的上下文来加载多版本的模块。如果“require”没有指定为一个依赖,则很可能会出现错误。
在页面加载之后加载代码§ 4.3
上述多版本示例中也展示了如何在嵌套的require()中迟后加载代码。
Web Worker 支持§ 4.4
从版本0.12开始,RequireJS可在Web Worker中运行。可以通过在web worker中调用importScripts()来加载require.js(或包含require()定义的JS文件),然后调用require就好了。
你可能需要设置baseUrl配置项来确保require()可找到待加载脚本。
你可以在unit test使用的一个文件中找到一个例子。
Rhino 支持§ 4.5
RequireJS可通过r.js适配器用在Rhino中。参见r.js的README。
处理错误§ 4.6
通常的错误都是404(未找到)错误,网络超时或加载的脚本含有错误。RequireJS有些工具来处理它们:require特定的错误回调(errback),一个“paths”数组配置,以及一个全局的requirejs.onError事件。
传入errback及requirejs.onError中的error object通常包含两个定制的属性:
- requireType: 含有类别信息的字串值,如“timeout”,“nodefine”, “scripterror”
- requireModules: 超时的模块名/URL数组。
如果你得到了requireModules错,可能意味着依赖于requireModules数组中的模块的其他模块未定义。
在IE中捕获加载错§ 4.6.1
Internet Explorer有一系列问题导致检测errbacks/paths fallbacks中的加载错 比较困难:
- IE 6-8中的script.onerror无效。没有办法判断是否加载一个脚本会导致404错;更甚地,在404中依然会触发state为complete的onreadystatechange事件。
- IE 9+中script.onerror有效,但有一个bug:在执行脚本之后它并不触发script.onload事件句柄。因此它无法支持匿名AMD模块的标准方法。所以script.onreadystatechange事件仍被使用。但是,state为complete的onreadystatechange事件会在script.onerror函数触发之前触发。
因此IE环境下很难两全其美:匿名AMD(AMD模块机制的核心优势)和可靠的错误检测。
但如果你的项目里使用了define()来定义所有模块,或者为其他非define()的脚本使用shim配置指定了导出字串,则如果你将enforceDefine配置项设为true,loader就可以通过检查define()调用或shim全局导出值来确认脚本的加载无误。
因此如果你打算支持Internet Explorer,捕获加载错,并使用了define()或shim,则记得将enforceDefine设置为true。参见下节的示例。
注意: 如果你设置了enforceDefine: true,而且你使用data-main=""来加载你的主JS模块,则该主JS模块必须调用define()而不是require()来加载其所需的代码。主JS模块仍然可调用require/requirejs来设置config值,但对于模块加载必须使用define()。
如果你使用了almond而不是require.js来build你的代码,记得在build配置项中使用insertRequire来在主模块中插入一个require调用 —— 这跟data-main的初始化require()调用起到相同的目的。
require([]) errbacks§ 4.6.2
当与requirejs.undef()一同使用errback时,允许你检测模块的一个加载错,然后undefine该模块,并重置配置到另一个地址来进行重试。
一个常见的应用场景是先用库的一个CDN版本,如果其加载出错,则切换到本地版本:
requirejs.config({
enforceDefine: true,
paths: {
jquery: 'http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min'
}
});
//Later
require(['jquery'], function ($) {
//Do something with $ here
}, function (err) {
//The errback, error callback
//The error has a list of modules that failed
var failedId = err.requireModules && err.requireModules[0];
if (failedId === 'jquery') {
//undef is function only on the global requirejs object.
//Use it to clear internal knowledge of jQuery. Any modules
//that were dependent on jQuery and in the middle of loading
//will not be loaded yet, they will wait until a valid jQuery
//does load.
requirejs.undef(failedId);
//Set the path to jQuery to local path
requirejs.config({
paths: {
jquery: 'local/jquery'
}
});
//Try again. Note that the above require callback
//with the "Do something with $ here" comment will
//be called if this new attempt to load jQuery succeeds.
require(['jquery'], function () {});
} else {
//Some other error. Maybe show message to the user.
}
});
使用“requirejs.undef()”,如果你配置到不同的位置并重新尝试加载同一模块,则loader会将依赖于该模块的那些模块记录下来并在该模块重新加载成功后去加载它们。
注意: errback仅适用于回调风格的require调用,而不是define()调用。define()仅用于声明模块。
paths备错配置§ 4.6.3
上述模式(检错,undef()模块,修改paths,重加载)是一个常见的需求,因此有一个快捷设置方式。paths配置项允许数组值:
requirejs.config({
//To get timely, correct error triggers in IE, force a define/shim exports check.
enforceDefine: true,
paths: {
jquery: [
'http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min',
//If the CDN location fails, load from this location
'lib/jquery'
]
}
});
//Later
require(['jquery'], function ($) {
});
上述代码先尝试加载CDN版本,如果出错,则退回到本地的lib/jquery.js。
注意: paths备错仅在模块ID精确匹配时工作。这不同于常规的paths配置,常规配置可匹配模块ID的任意前缀部分。备错主要用于非常的错误恢复,而不是常规的path查找解析,因为那在浏览器中是低效的。
全局 requirejs.onError§ 4.6.4
为了捕获在局域的errback中未捕获的异常,你可以重载requirejs.onError():
requirejs.onError = function (err) {
console.log(err.requireType);
if (err.requireType === 'timeout') {
console.log('modules: ' + err.requireModules);
}
throw err;
};
加载插件§ 5
RequireJS支持加载器插件。使用它们能够加载一些对于脚本正常工作很重要的非JS文件。RequireJS的wiki有一个插件的列表。本节讨论一些由RequireJS一并维护的特定插件:
指定文本文件依赖§ 5.1
如果都能用HTML标签而不是基于脚本操作DOM来构建HTML,是很不错的。但没有好的办法在JavaScript文件中嵌入HTML。所能做的仅是在js中使用HTML字串,但这一般很难维护,特别是多行HTML的情况下。.
RequireJS有个text.js插件可以帮助解决这个问题。如果一个依赖使用了text!前缀,它就会被自动加载。参见text.js的README文件。
页面加载事件及DOM Ready§ 5.2
RequireJS加载模块速度很快,很有可能在页面DOM Ready之前脚本已经加载完毕。需要与DOM交互的工作应等待DOM Ready。现代的浏览器通过DOMContentLoaded事件来知会。
但是,不是所有的浏览器都支持DOMContentLoaded。domReady模块实现了一个跨浏览器的方法来判定何时DOM已经ready。下载并在你的项目中如此用它:
require(['domReady'], function (domReady) {
domReady(function () {
//This function is called once the DOM is ready.
//It will be safe to query the DOM and manipulate
//DOM nodes in this function.
});
});
基于DOM Ready是个常规需求,像上述API中的嵌套调用方式,理想情况下应避免。domReady模块也实现了Loader Plugin API,因此你可以使用loader plugin语法(注意domReady依赖的!前缀)来强制require()回调函数在执行之前等待DOM Ready。当用作loader plugin时,domReady会返回当前的document:
require(['domReady!'], function (doc) {
//This function is called once the DOM is ready,
//notice the value for 'domReady!' is the current
//document.
});
注意: 如果document需要一段时间来加载(也许是因为页面较大,或加载了较大的js脚本阻塞了DOM计算),使用domReady作为loader plugin可能会导致RequireJS“超时”错。如果这是个问题,则考虑增加waitSeconds配置项的值,或在require()使用domReady()调用(将其当做是一个模块)。
Define an I18N Bundle§ 5.3
一旦你的web app达到一定的规模和流行度,提供本地化的接口和信息是十分有用的,但实现一个扩展良好的本地化方案又是很繁贅的。RequireJS允许你先仅配置一个含有本地化信息的基本模块,而不需要将所有的本地化信息都预先创建起来。后面可以将这些本地化相关的变化以值对的形式慢慢加入到本地化文件中。
i18n.js插件提供i18n bundle支持。在模块或依赖使用了i18n!前缀的形式(详见下)时它会自动加载。下载该插件并将其放置于你app主JS文件的同目录下。
将一个文件放置于一个名叫“nls”的目录内来定义一个bundle——i18n插件当看到一个模块名字含有“nls”时会认为它是一个i18n bundle。名称中的“nls”标记告诉i18n插件本地化目录(它们应当是nls目录的直接子目录)的查找位置。如果你想要为你的“my”模块集提供颜色名的bundle,应像下面这样创建目录结构:
- my/nls/colors.js
该文件的内容应该是:
//my/nls/colors.js contents:
define({
"root": {
"red": "red",
"blue": "blue",
"green": "green"
}
});
以一个含有“root”属性的object直接量来定义该模块。这就是为日后启用本地化所需的全部工作。你可以在另一个模块中,如my/lamps.js中使用上述模块:
//Contents of my/lamps.js
define(["i18n!my/nls/colors"], function(colors) {
return {
testMessage: "The name for red in this locale is: " + colors.red
}
});
my/lamps模块具备一个“testMessage”属性,它使用了colors.red来显示红色的本地化值。
日后,当你想要为文件再增加一个特定的翻译,如fr-fr,可以改变my/nls/colors内容如下:
//Contents of my/nls/colors.js
define({
"root": {
"red": "red",
"blue": "blue",
"green": "green"
},
"fr-fr": true
});
然后再定义一个my/nls/fr-fr/colors.js文件,含有如下内容:
//Contents of my/nls/fr-fr/colors.js
define({
"red": "rouge",
"blue": "bleu",
"green": "vert"
});
RequireJS会使用浏览器的navigator.language或navigator.userLanguage属性来判定my/nls/colors的本地化值,因此你的app不需要更改。如果你想指定一个本地化方式,你可使用模块配置将该方式传递给插件:
requirejs.config({
config: {
//Set the config for the i18n
//module ID
i18n: {
locale: 'fr-fr'
}
}
});
注意 RequireJS总是使用小写版本的locale值来避免大小写问题,因此磁盘上i18n的所有目录和文件都应使用小写的本地化值。 RequireJS有足够智能去选取合适的本地化bundle,使其尽量接近my/nls/colors提供的那一个。例如,如果locale值时“en-us”,则会使用“root” bundle。如果locale值是“fr-fr-paris”,则会使用“fr-fr” bundle
RequireJS也会将bundle合理组合,例如,若french bundle如下定义(忽略red的值):
//Contents of my/nls/fr-fr/colors.js
define({
"blue": "bleu",
"green": "vert"
});
则会应用“root”下的red值。所有的locale组件是如此。如果如下的所有bundle都已定义,则RequireJS会按照如下的优先级顺序(最顶的最优先)应用值:
- my/nls/fr-fr-paris/colors.js
- my/nls/fr-fr/colors.js
- my/nls/fr/colors.js
- my/nls/colors.js
如果你不在模块的顶层中包含root bundle,你可像一个常规的locale bundle那样定义它。这种情形下顶层模块应如下:
//my/nls/colors.js contents:
define({
"root": true,
"fr-fr": true,
"fr-fr-paris": true
});
root bundle应看起来如下:
//Contents of my/nls/root/colors.js
define({
"red": "red",
"blue": "blue",
"green": "green"
});