Require.js和在DOM中创建<script>元素有什么区别?

138

使用Require.JS和直接在DOM中创建一个<script>元素的区别是什么?

我理解Require.JS可以加载依赖项,但是是否可以通过创建一个载入必要外部JS文件的<script>元素来实现同样的功能呢?

例如,假设我有一个需要使用函数needMe()的函数doStuff()。其中doStuff()在外部文件do_stuff.js中,而needMe()在外部文件need_me.js中。

按照Require.JS的方式:

define(['need_me'],function(){
    function doStuff(){
        //do some stuff
        needMe();
        //do some more stuff
    }
});

通过创建一个脚本元素来实现此目的:

function doStuff(){
    var scriptElement  = document.createElement('script');
    scriptElement.src = 'need_me.js';
    scriptElement.type = 'text/javascript';
    document.getElementsByTagName('head')[0].appendChild(scriptElement);

    //do some stuff
    needMe();
    //do some more stuff
}

这两种都可以工作。但是第二个版本不需要我加载整个Require.js库。我并没有看到任何功能上的区别...


1
浏览器缓存怎么样?RequireJS 会干扰它吗? - Muhammad Umer
我重新打开这个问题,因为它询问两个非常相似的事物之间的区别。它可以客观地回答,而且说实话,我不知道意见在哪里发挥作用。 - RamenChef
4个回答

52

相比于只是在DOM中创建元素,Require.JS提供了哪些优势?

在你的示例中,你是异步地创建脚本标签,这意味着在 need_me.js 文件完成加载之前会调用你的 needMe() 函数。这会导致未捕获的异常,其中你的函数未定义。

相反,要使你建议的方法实际起作用,你需要像这样做:

function doStuff(){
    var scriptElement  = document.createElement('script');
    scriptElement.src = 'need_me.js';
    scriptElement.type = 'text/javascript';

    scriptElement.addEventListener("load", 
        function() { 
            console.log("script loaded - now it's safe to use it!");

            // do some stuff
            needMe();
            //do some more stuff

        }, false);

    document.getElementsByTagName('head')[0].appendChild(scriptElement);

}

可以说,最好使用诸如RequireJS之类的软件包管理器,或者采用纯JavaScript策略,就像上面所示的那样。虽然您的Web应用程序加载可能更快,但调用站点上的功能和特性会更慢,因为这将涉及等待资源加载,然后才能执行该操作。

如果将Web应用程序构建为单页应用程序,则需考虑人们实际上不会经常重新加载页面。在这些情况下,预加载所有内容有助于使体验在实际使用应用程序时似乎更快。在这些情况下,您可以简单地通过在页面的头部或正文中包含脚本标签来加载所有资源。

但是,如果构建一个遵循更传统模型的网站或Web应用程序,其中一个从页面转换到另一个,导致资源重新加载,延迟加载方法可能有助于加速这些转换。


43

以下是关于为什么使用它的ajaxian.com上的不错文章:

RequireJS:异步JavaScript加载

  • 某种形式的#include/import/require
  • 能够加载嵌套依赖项
  • 对开发人员易于使用,但又由优化工具支持以帮助部署

2
我已经阅读了这些内容,但现在我更深入地思考后意识到,嵌套依赖的想法不能仅通过编写<script>标签来实现。谢谢。 - maxedison
37
“对开发人员的易用性”与事实相去甚远。这个项目肯定对你和其他任何人都有陡峭的学习曲线。 - Sahat Yalkabov
3
@TwilightPony 我认为自己不是很聪明,但是 requirejs 对我来说并不难理解。它可以帮助你处理依赖关系并加速页面加载速度。使用它可以让你的代码更贴近服务器端编程方式,声明依赖项变得简单清爽。语法简单,闭包设计让你可以轻松进行代码组合和调试,非常方便。相比之下,我曾尝试另一种方式,但发现那种方式要困难得多。 - King Friday
我很困惑,尤其是那些试图附加到全局对象上的模块(比如React模块)。。。 - geilt
1
那个页面上的评论让我感觉应该远离 require,而不是靠近它。特别是底部链接到 http://stevesouders.com/tests/require.php 的那个评论。 - Raydot
那个stevesounders的例子没有抓住重点。如果脚本2依赖于脚本1中的符号,则脚本2应该要求脚本1。这就是声明依赖关系的全部意义所在。如果您不声明它们,当然它不会做正确的事情。理想情况下,您永远不应该在模块之间神奇地传递符号。如果您正在模块之间神奇地传递符号并将内容放在window/global上,则需要手动声明您的依赖项。 - gman

9
一些使用RequireJS的非常明显的原因:
  1. 对于大型项目,自己管理依赖会很快崩溃。
  2. 你可以拥有任意数量的小文件,而不必担心跟踪依赖关系或加载顺序。
  3. RequireJS使得编写整个模块化应用程序成为可能,而无需接触window对象。
摘自rmurphey在这个Gist中的评论
抽象层可以是学习和适应的噩梦,但当它有用并且做得好时,它就是有意义的。

9
你仍需要处理所有那些 require 和 define 语句、配置文件、与没有实现 AMD 规范的其他系统和库的冲突等等。我尝试在 node-webkit 项目中使用 Require.JS,但 Require.JS 在每一步都与我作对...相比之下,只需以特定方式排序脚本即可...当然,你可以通过 Require.JS 获得懒加载,这就是我尝试让它工作的原因 :) - jamesmortensen
我完全同意@jmort253的观点,一开始确实有些困难,但现在我非常喜欢它。所有三个观点都是正确的!而修改库应该不会那么困难...或者使用shim。 - Legends

0

这里有一个更具体的例子。

我正在进行一个包含60个文件的项目。我们有两种不同的运行模式。

  1. 加载一个连接的版本,即一个大文件。(生产环境)

  2. 加载全部60个文件。(开发环境)

我们使用一个加载器,所以网页中只有一个脚本。

<script src="loader.js"></script>

默认情况下,它会使用模式#1(加载一个大的连接文件)。要在模式#2(分离文件)中运行,我们需要设置一些标志。它可以是任何东西。查询字符串中的一个键。在这个例子中,我们只是这样做

<script>useDebugVersion = true;</script>
<script src="loader.js"></script>

loader.js 大致长这样

if (useDebugVersion) {
   injectScript("app.js");
   injectScript("somelib.js");
   injectScript("someotherlib.js");
   injectScript("anotherlib.js");
   ... repeat for 60 files ...
} else {
   injectScript("large-concatinated.js");
}

构建脚本只是一个 .sh 文件,看起来像这样。
cat > large-concantinated.js app.js somelib.js someotherlib.js anotherlib.js

如果添加新文件,我们可能会使用mode#2,因为我们正在开发,我们必须将injectScript(“somenewfile.js”)行添加到loader.js中。

然后,以后在生产中,我们还必须将somenewfile.js添加到我们的构建脚本中。这是我们经常忘记的一步,然后出现错误信息。

通过切换到AMD,我们不必编辑两个文件。保持loader.js和构建脚本同步的问题消失了。使用r.jswebpack,它可以只读取代码以构建large-concantinated.js

它还可以处理依赖关系,例如我们加载了2个文件lib1.js和lib2.js。

injectScript("lib1.js");
injectScript("lib2.js");

lib2需要lib1。它内部有类似以下代码的功能

lib1Api.installPlugin(...);

但是由于注入的脚本是异步加载的,所以无法保证它们会按正确顺序加载。这两个脚本不是 AMD 脚本,但是使用 require.js 我们可以告诉它它们的依赖关系。

require.config({
    paths: {
        lib1: './path/to/lib1',
        lib2: './path/to/lib2',
    },
    shim: {
        lib1: {
            "exports": 'lib1Api',
        },
        lib2: {
            "deps": ["lib1"],
        },
    }
});

在我们使用lib1的模块中,我们会这样做

define(['lib1'], function(lib1Api) {
   lib1Api.doSomething(...);
});

现在 require.js 将会为我们注入脚本,而且它不会注入 lib2 直到 lib1 被加载,因为我们告诉它 lib2 依赖于 lib1。它也不会启动使用 lib1 的模块直到 lib2 和 lib1 都被加载。

这使得开发过程变得很好(没有构建步骤,不用担心加载顺序),并且使得生产环境变得很好(不需要为每个添加的脚本更新构建脚本)。

额外的好处是,我们可以使用 webpack 的 babel 插件在旧版浏览器上运行 babel,并且我们也不必维护该构建脚本。

请注意,如果 Chrome(我们的选择浏览器)真正开始支持 import,我们可能会转换到该语法进行开发,但这并不会改变任何事情。我们仍然可以使用 webpack 创建一个合并的文件,并且我们可以使用它在所有浏览器上运行 babel。

通过不使用 script 标签和使用 AMD,所有这些都实现了。


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