为什么要使用 Web 模块?
- 问题§ 1
- 解决方案§ 2
- 脚本加载 API§ 3
- 异步与同步§ 4
- 脚本加载:XHR§ 5
- 脚本加载:Web Workers§ 6
- 脚本加载:document.write()§ 7
- 脚本加载:head.appendchild(script)§ 8
- 函数包装§ 9
本页讨论了为什么 Web 上的模块很有用,以及当今 Web 上可以用来启用它们的机制。 还有一个单独的页面讨论了 设计力量 用于 RequireJS 使用的特定函数包装格式。
问题 § 1
- 网站正在变成 Web 应用程序
- 随着网站规模的扩大,代码复杂性也在增加
- 组装变得更加困难
- 开发人员想要离散的 JS 文件/模块
- 部署希望在一次或几次 HTTP 调用中优化代码
脚本加载 API§ 3
首先要解决的是脚本加载 API。 以下是一些候选者
- Dojo:dojo.require("some.module")
- LABjs:$LAB.script("some/module.js")
- CommonJS:require("some/module")
所有这些都映射到加载 some/path/some/module.js。 理想情况下,我们可以选择 CommonJS 语法,因为它可能会随着时间的推移而变得更加普遍,并且我们希望重用代码。
我们还希望某种语法允许加载当今存在的普通 JavaScript 文件——开发人员不应该为了获得脚本加载的好处而重写所有 JavaScript。
但是,我们需要一些在浏览器中运行良好的东西。 CommonJS require() 是一个同步调用,它应该立即返回模块。 这在浏览器中不能很好地工作。
异步与同步§ 4
这个例子应该说明了浏览器面临的基本问题。 假设我们有一个 Employee 对象,并且我们希望 Manager 对象从 Employee 对象派生。 以这个例子为例,我们可以使用我们的脚本加载 API 编写如下代码
var Employee = require("types/Employee");
function Manager () {
this.reports = [];
}
//Error if require call is async
Manager.prototype = new Employee();
如上面的注释所示,如果 require() 是异步的,则此代码将不起作用。 但是,在浏览器中同步加载脚本会降低性能。 那么,该怎么办?
脚本加载:XHR§ 5
使用 XMLHttpRequest (XHR) 加载脚本很诱人。 如果使用 XHR,那么我们可以修改上面的文本——我们可以执行正则表达式来查找 require() 调用,确保我们加载了这些脚本,然后使用 eval() 或其正文文本设置为通过 XHR 加载的脚本的文本的脚本元素。
使用 eval() 来评估模块是不好的
- 开发人员已经被告知 eval() 是不好的。
- 某些环境不允许使用 eval()。
- 更难调试。 Firebug 和 WebKit 的检查器有一个 //@ sourceURL= 约定,这有助于为 evaled 文本命名,但这种支持在浏览器中并不普遍。
- eval 上下文在不同的浏览器中是不同的。 您也许可以在 IE 中使用 execScript 来解决这个问题,但这意味着更多的活动部件。
使用正文文本设置为文件文本的脚本标签是不好的
- 调试时,您为错误获取的行号不会映射到原始源文件。
XHR 在跨域请求方面也存在问题。 一些浏览器现在支持跨域 XHR,但这并不普遍,并且 IE 决定为跨域调用创建一个不同的 API 对象 XDomainRequest。 更多的活动部件和更多出错的地方。 特别是,您需要确保不发送任何非标准 HTTP 标头,否则可能会执行另一个“预检”请求,以确保允许跨域访问。
Dojo 使用了基于 XHR 的加载器和 eval(),虽然它有效,但它一直是开发人员沮丧的根源。 Dojo 有一个 xdomain 加载器,但它需要通过构建步骤修改模块以使用函数包装器,以便可以使用脚本 src="" 标签来加载模块。 有很多边缘情况和活动部件会给开发人员带来负担。
如果我们要创建一个新的脚本加载器,我们可以做得更好。
脚本加载:Web Workers§ 6
Web Workers 可能是另一种加载脚本的方式,但是
- 它没有强大的跨浏览器支持
- 它是一个消息传递 API,脚本可能想与 DOM 交互,因此这意味着只使用工作线程来获取脚本文本,但将文本传递回主窗口,然后使用 eval/script 和文本正文来执行脚本。 这具有上面提到的 XHR 的所有问题。
脚本加载:document.write()§ 7
document.write() 可用于加载脚本——它可以从其他域加载脚本,并且它映射到浏览器通常如何使用脚本,因此它允许轻松调试。
但是,在 异步与同步示例 中,我们不能直接执行该脚本。 理想情况下,我们可以在执行脚本之前知道 require() 依赖项,并确保首先加载这些依赖项。 但我们在执行脚本之前无法访问它。
此外,document.write() 在页面加载后不起作用。 获得网站感知性能的一个好方法是按需加载代码,因为用户在下一步操作中需要它。
最后,通过 document.write() 加载的脚本会阻塞页面渲染。 当希望为您的网站获得最佳性能时,这是不可取的。
脚本加载:head.appendChild(script)§ 8
我们可以按需创建脚本并将它们添加到头部
var head = document.getElementsByTagName('head')[0],
script = document.createElement('script');
script.src = url;
head.appendChild(script);
除了上面的代码片段之外,还有一些更复杂的内容,但这是基本思路。 这种方法优于 document.write,因为它不会阻塞页面渲染并且在页面加载后工作。
但是,它仍然存在 异步与同步示例 问题:理想情况下,我们可以在执行脚本之前知道 require() 依赖项,并确保首先加载这些依赖项。
函数包装§ 9
所以我们需要知道依赖关系并确保在执行脚本之前加载它们。 最好的方法是用函数包装器构建我们的模块加载 API。 像这样
define(
//The name of this module
"types/Manager",
//The array of dependencies
["types/Employee"],
//The function to execute when all dependencies have loaded. The
//arguments to this function are the array of dependencies mentioned
//above.
function (Employee) {
function Manager () {
this.reports = [];
}
//This will now work
Manager.prototype = new Employee();
//return the Manager constructor function so it can be used by
//other modules.
return Manager;
}
);
这就是 RequireJS 使用的语法。 如果您只想加载一些未定义模块的普通 JavaScript 文件,则还有一种简化语法
require(["some/script.js"], function() {
//This function is called after some/script.js has loaded.
});
选择这种语法是因为它简洁并且允许加载器使用 head.appendChild(script) 类型的加载。
它与普通的 CommonJS 语法不同,因为它需要在浏览器中良好地工作。 有人建议,如果服务器进程将模块转换为具有函数包装器的传输格式,则可以使用普通的 CommonJS 语法和 head.appendChild(script) 类型的加载。
我认为重要的是不要强制使用运行时服务器进程来转换代码
- 它使调试变得很奇怪,行号与源文件不符,因为服务器正在注入函数包装器。
- 它需要更多的装备。 前端开发应该可以使用静态文件。
有关此函数包装格式(称为异步模块定义 (AMD))的设计力量和用例的更多详细信息,请参阅 为什么使用 AMD? 页面。