为什么连接的RequireJS AMD模块需要一个加载器?

37

我们在开发过程中非常喜欢使用RequireJS和AMD。我们可以编辑一个模块,点击浏览器的刷新按钮,立即看到结果。但是当我们需要将模块连接成一个单独的文件进行生产部署时,显然仍然需要存在一个AMD加载器,无论是RequireJS本身还是它的较小伙伴“almond”,如此解释:

http://requirejs.org/docs/faq-optimization.html#wrap

我的困惑是:为什么加载器是必要的呢?除非你有非常特殊的情况需要在你的模块内部调用require(),否则一系列的AMD模块似乎可以在没有加载器的情况下连接起来。最简单的例子就是像下面这样的一对模块。

ModA.js:

define([], function() {
    return {a: 1};
});

ModB.js:

define(['ModA'], function(A) {
    return {b : 2};
});

鉴于这两个模块,似乎可以通过一个连接器生成以下文本,而不会给生产服务器或浏览器增加额外的带宽或计算负担,RequireJS或Almond都需要它们:

我想象了一个连接器,它将生成以下内容(我使用尖括号引用符号«,»来显示上述两个模块的片段已被插入的位置):

(function() {
    var ModA = «function() {
        return {a: 1};
    }»();
    var ModB = «function(A) {
        return {b : 2};
    }»(ModA);
    return ModB;
})();

就我所知,这样做可以正确地重现AMD的语义,同时最大限度地减少不必要的胶水JavaScript。是否有这样的连接器可用?如果没有,那么我认为写一个是否太蠢了——真的很少有代码库仅由使用define()编写的简单和清晰的模块组成,并且永远不需要在内部进行require()调用,以启动后续异步获取代码的过程吗?


1
你是怎么解决这个问题的?我发现使用杏仁,最小文件比连接文件大3k(9K vs 6K)。 - Naor
5个回答

14

AMD优化器的作用不仅可以优化要下载的文件数,还可以优化在内存中加载的模块数。

例如,如果你有10个模块并且可以将它们优化为1个文件,那么你就可以节省9次下载。

如果Page1使用了所有的10个模块,那就很好。但是如果Page2只使用了1个模块呢?AMD加载器可以延迟“工厂函数”的执行直到一个模块被require。因此,Page2只会触发一次“工厂函数”的执行。

如果每个模块在被require时占用100kb的内存,则具有运行时优化的AMD框架也将在Page2上为我们节省900kb的内存。

这种情况的一个示例可以是一个“关于”对话框。其执行被延迟到最后一秒,因为在99%的情况下都不会访问它。例如(使用松散的jQuery语法):

aboutBoxBtn.click(function () {
    require(['aboutBox'], function (aboutBox) {
        aboutBox.show();
    }
});

只有在确认需要时,才创建与“关于对话框”相关的JS对象和DOM,以节省开销。

更多信息,请参见Delay executing defines until first require,了解requirejs对此的看法。


1
有趣!所以,如果我理解你的话,你正在提出创建对象和修改 DOM 的代码“仅仅是因为需要而产生的副作用”,对吗?我没有想到过这点,如果这种做法很常见,那么你可能已经找到了 JS 程序员喜欢在生产环境中使用 AMD 加载器的原因。可能因为我的 Python 背景,我的代码在导入时永远不会执行——它只是返回一个充满函数的对象,当它们的副作用最终需要被调用时才会执行。 - Brandon Rhodes
你关于内存使用的例子经常被I/O开销所混淆 - 对于像盒子这样的东西,似乎提前加载但延迟初始化会更好,这样可以节省内存并在没有大量交互延迟的情况下进行设置。 - Chris Adams
@Chris,我同意你的观点,但我想为运行时存在AMD加载器提出一个案例。我可能会担心启动时执行太多代码,而你可能会担心延迟。除了琐碎的情况外,我认为在性能方面没有一种适合所有情况的解决方案。在我的例子中不会有延迟问题,因为所有JS都已加载,只是没有“执行”。 - Paul Grime
1
@Brandon,是的,那就是要点。显然,模块的“工厂函数”中所做的工作量会因人而异。如果没有太多的工作量,那么好处就会大大降低。但如果它是一个特别复杂的模块,那么将该模块的“工厂函数”的执行延迟到实际需要时可能会提高性能。此外,我链接的文章提到,define的执行顺序不应总是与优化后的JS文件中遇到它们的顺序相同。 - Paul Grime
1
@PaulGrime:完全同意不可能有一种适用于所有情况的解决方案。必须对应用程序进行分析,实际测试不同的方法才能取得最佳效果。 - Chris Adams

1
我有同样的需求,因此我创建了一个简单的AMD“编译器”来实现这个目的。你可以在https://github.com/amitayh/amd-compiler获取它。
请注意,它缺少许多功能,但它完成了工作(至少对我来说)。欢迎为代码库做出贡献。

1
唯一的真正好处是,如果您在不同部分使用模块,则可以独立缓存模块,从而获得好处。

0

没有理由不能有一个像你提议的构建工具。

我上一次查看优化器的输出时,它将模块转换为显式命名的模块,然后将它们连接在一起。它依靠 require 本身来确保工厂函数按正确顺序调用,并传递正确的模块对象。要构建一个像您想要的工具,您必须明确地线性化模块——这并非不可能,但需要更多的工作。这可能就是为什么没有做到的原因。

我相信优化器有一个功能可以自动包括 require 本身(或杏仁)到构建文件中,以便您只需下载一个文件。虽然与您所要求的构建工具的输出相同,但文件会更大。

如果有一个构建工具可以生成您所要求的输出,那么它必须更加小心,以防同步 require、exports 的使用而不是 return,以及任何其他 CommonJS 兼容性特性。

* 那是几年前了。2010 年,我想。

** 但现在好像找不到它了。


0

如果你使用require.js将代码编译成一个单独的大文件用于生产环境,你可以使用almond.js来完全替换require。

Almond只处理模块引用管理而不是加载本身,因此加载已不再需要。

请注意almond实施的限制以确保其正常工作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接