为什么要使用WEB模块
- 问题§ 1
- 解决方案§ 2
- 脚本APIs§ 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 = [];
}
//如果require调用是异步的,则出错
Manager.prototype = new Employee();
如上面的注释所示,如果require()是异步的,则此代码将不起作用。但是,在浏览器中同步加载脚本会降低性能。那么该怎么办?
脚本加载: XHR§ 5
使用XMLHttpRequest(XHR)加载脚本很诱人。如果使用XHR,那么我们可以对上面的文本进行处理-我们可以进行正则表达式来查找require()调用,确保我们加载了这些脚本,然后使用eval()或将正文文本设置为text的脚本元素通过XHR加载的脚本。
使用eval()评估模块是不好的:
- 开发人员被告知eval()不好。
- 某些环境不允许eval()。
- 很难调试。Firebug和WebKit的检查器具有// @ sourceURL =约定,该约定有助于为逃避的文本命名,但是这种支持在浏览器中并不普遍。
- 评估上下文因浏览器而异。您也许可以在IE中使用execScript来帮助解决此问题,但这意味着需要更多的移动部件。
在主体文本设置为文件文本的情况下使用脚本标签是不好的:
- 调试时,由于错误而获得的行号不会映射到原始源文件。
XHR也存在跨域请求的问题。现在,某些浏览器具有跨域XHR支持,但它不是通用的,IE决定为跨域调用XDomainRequest创建一个不同的API对象。更多的运动部件和更多出错的地方。特别是,您需要确保不发送任何非标准的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相比,此方法的优势在于它不会阻止页面呈现,并且在页面加载后可以工作。
但是,它仍然存在Async vs Sync示例问题:理想情况下,在执行脚本之前,我们可以知道require()依赖项,并确保首先加载这些依赖项。
函数包装§ 9
因此,我们需要了解依赖项,并确保在执行脚本之前先加载它们。最好的方法是使用函数包装器构造模块加载API。像这样:
define(
//此模块的名称
"types/Manager",
//依赖关系数组
["types/Employee"],
//加载所有依赖项时要执行的函数。此函数的参数是上面提到的依赖项数组。
function (Employee) {
function Manager () {
this.reports = [];
}
//现在可以了
Manager.prototype = new Employee();
//返回Manager构造函数,以便其他模块可以使用它。
return Manager;
}
);
这是RequireJS使用的语法。如果您只想加载一些未定义模块的纯JavaScript文件,则还有一种简化的语法:
require(["some/script.js"], function() {
//此函数在加载了/script.js之后调用。
});
选择这种语法是因为它很简洁,并且允许加载程序使用head.appendChild(script)类型的加载。
它与正常的CommonJS语法有所不同,因为它无法在浏览器中正常工作。有人建议,如果服务器进程将模块转换为具有函数包装器的传输格式,则可以将普通的CommonJS语法与head.appendChild(script)类型的加载配合使用。
我相信不要强迫使用运行时服务器进程来转换代码很重要:
- 这使得调试很奇怪,因为服务器正在注入函数包装器,所以行号相对于源文件将关闭。
- 它需要更多的装备。静态文件应该可以进行前端开发。