为什么要使用WEB模块

该页面讨论了为什么Web上的模块有用的原因以及今天可以在Web上使用以启用它们的机制。在单独的页面上讨论了RequireJS使用的特定函数包装格式的设计力。

问题 § 1

  • 网站正在变成Web应用程序
  • 随着站点的扩大,代码的复杂性也随之增加
  • 组装变得更加困难
  • 开发人员想要离散的JS文件/模块
  • 部署只需要一个或几个HTTP调用即可获得优化的代码

解决方案§ 2

前端开发人员需要以下解决方案:

  • 某种#include/import/require
  • 加载嵌套依赖项的能力
  • 易于开发人员使用,但随后得到有助于部署的优化工具的支持

脚本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)类型的加载配合使用。

我相信不要强迫使用运行时服务器进程来转换代码很重要:

  • 这使得调试很奇怪,因为服务器正在注入函数包装器,所以行号相对于源文件将关闭。
  • 它需要更多的装备。静态文件应该可以进行前端开发。