以同步方式使用requirejs(AMD)是错误的吗?

时间:2013-06-08 08:55:59

标签: javascript requirejs amd

我正在开发一个JavaScript重型Web应用程序;如果没有JavaScript,整个应用程序都是无用的。我目前正在使用requirejs作为我的模块加载器,并使用r.js工具将我的JS优化为生产中的单个文件。

目前,在制作中我的标记看起来像这样;

<script src="/js/require.js"></script>
<script>
    require.config({
       // blah blah blah
    });

    require(['editor']); // Bootstrap the JavaScript code.
</script>

但是,这会异步加载JavaScript,这会使页面呈现尽管在加载JavaScript之前无法使用;我没有看到这一点。相反,我想像这样加载JavaScript 同步;

<script src="/js/bundle.js"></script><!-- combine require.js, config and editor.js -->

这样,当呈现页面时,它是可用的。我已经读过所有现代浏览器都支持parallel loading,这让我相信互联网上的大多数建议都建议避免这种方法,因为阻止并行下载已经过时了。

然而;

  1. AMD(异步模块定义)暗示这不是必须使用requirejs的方式。
  2. 在开发中,我想将未组合的文件作为多个脚本标签插入,而不是单个缩小的文件;

    <script src="/js/require.js"></script>
    <script>/* require.config(...); */</script>
    <script src="/js/editor-dep-1.js"></script>
    <script src="/js/editor-dep-2.js"></script>
    <script src="/js/editor.js"></script>
    

    ...然而这在requirejs中显得非常繁琐(使用r.js生成虚假构建,获取editor.js的依赖关系列表),感觉错误

  3. 我的问题如下:

    1. 我是否正确避免同步<script />的建议过时?
    2. 以这种方式使用requirejs / AMD是错误的吗?
    3. 我错过了替代技术/方法/工具/模式吗?

4 个答案:

答案 0 :(得分:3)

简短回答:是的,这是错误的。您使用require.js首先加载所有依赖项,然后一旦加载所有依赖项,您运行的代码依赖于您加载的所有内容。

如果您的页面在需要包装的代码运行之后无法使用,则问题不是必需的,而是您的页面:相反,创建一个最小的页面并指示它仍在加载,其他任何内容(可见)都没有(在JS完成之前不应该使用的元素上使用css display:none,并且只在需要完成后启用/显示实际的功能页面元素,并且您的代码已经设置了所有必需的UI / UX。

答案 1 :(得分:2)

花一点时间考虑一下你为什么要使用requirejs。它有助于管理您的依赖项,避免一长串的脚本标记必须按照正确的顺序排列。你可以说,当涉及大量脚本时,这只会变得无法管理。

其次,它异步加载脚本。同样,使用大量脚本可以大大减少加载时间,但是当使用少量脚本时,优势会更小。

如果您的应用程序仅使用一些javascript文件,您可能会认为正确设置requirejs的开销并不值得。 requirejs的好处只有在涉及大量脚本时才会变得明显。如果您发现自己想要以一种感觉“错误”的方式使用框架,那么退回并询问您是否需要使用该框架会有所帮助。

编辑:

要解决RequireJS的问题,最初将主要内容区域设置为display: none,或者更好地显示加载微调器动画。然后在主要RequireJS文件的末尾,只需淡入内容区域。

答案 2 :(得分:2)

我决定采用ljfranklin's advice,完全取消RequireJS。我个人认为AMD做错了,CommonJS(带有它的同步行为)是要走的路;但这是另一个讨论。

我看到的一件事是转移到Browserify,但在开发过程中,每次编辑(因为它扫描所有文件并追捕require()次调用)花了太长时间才能让我认为可以接受。

最后,我推出了自己的定制解决方案。它基本上是 Browserify,但它要求你指定所有依赖项,而不是让Browserify自己解决它。这意味着编译只需几秒而不是30秒。

这是TL; DR 。下面,我详细介绍了我是如何做到的。抱歉这个长度。希望这可以帮助某人......或者至少给某人一些灵感!


首先,我有我的JavaScript文件。它们是CommonJS编写的,exports不能作为“全局”变量使用(您必须使用module.exports)。 e.g:

var anotherModule = require('./another-module');

module.exports.foo = function () {
    console.log(anotherModule.saySomething());
};

然后,我在配置文件中指定依赖项的有序列表(注意js/support.js,它会在以后保存一天):

{
  "js": [
    "js/support.js",
    "js/jquery.js",
    "js/jquery-ui.js",
    "js/handlebars.js",
    // ...
    "js/editor/manager.js",
    "js/editor.js"
  ]
}

然后,在编译过程中,我将所有JavaScript文件(在js/目录中)映射到表单;

define('/path/to/js_file.js', function (require, module) {
    // The contents of the JavaScript file
});

这对原始JavaScript文件完全透明;下面我们提供对definerequiremodule等的所有支持,这样,对于原始的JavaScript文件,它才能正常工作

我使用grunt进行映射;首先将文件复制到build目录(这样我就不会弄乱原件),然后重写文件。

// files were previous in public/js/*, move to build/js/*
grunt.initConfig({
    copy: {
      dist: {
        files: [{
          expand: true,
          cwd: 'public',
          src: '**/*',
          dest: 'build/'
        }]
      }
    }
});

grunt.loadNpmTasks('grunt-contrib-copy');

grunt.registerTask('buildjs', function () {
    var path = require('path');

    grunt.file.expand('build/**/*.js').forEach(function (file) {
      grunt.file.copy(file, file, {
        process: function (contents, folder) {
          return 'define(\'' + folder + '\', function (require, module) {\n' + contents + '\n});'
        },
        noProcess: 'build/js/support.js'
      });
    });
});

我有一个文件/js/support.js,它定义了我用每个文件包装的define()函数;这是魔术发生的地方,因为它增加了对不到40行的module.exportsrequire()的支持!

(function () {
    var cache = {};

    this.define = function (path, func) {
        func(function (module) {
            var other = module.split('/');
            var curr = path.split('/');
            var target;

            other.push(other.pop() + '.js');
            curr.pop();

            while (other.length) {
                var next = other.shift();

                switch (next) {
                case '.':
                break;
                case '..':
                    curr.pop();
                break;
                default:
                    curr.push(next);
                }
            }

            target = curr.join('/');

            if (!cache[target]) {
                throw new Error(target + ' required by ' + path + ' before it is defined.');
            } else {
                return cache[target].exports;
            }
        }, cache[path] = {
            exports: {}
        });
    };
}.call(this));

然后,在开发中,我逐字地遍历配置文件中的每个文件,并将其作为单独的<script />标记输出;一切都是同步的,没有什么是缩小的,一切都很快。

{{#iter scripts}}<script src="{{this}}"></script>
{{/iter}}

这给了我;

<script src="js/support.js"></script>
<script src="js/jquery.js"></script>
<script src="js/jquery-ui.js"></script>
<script src="js/handlebars.js"></script>
<!-- ... -->
<script src="js/editor/manager.js"></script>
<script src="js/editor.js"></script>

在制作中,我使用UglifyJs缩小并合并JS文件。好吧,从技术上讲,我在UglifyJs周围使用了一个包装器; mini-fier

grunt.registerTask('compilejs', function () {
    var minifier = require('mini-fier').create();

    if (config.production) {
      var async = this.async();
      var files = bundles.js || [];

      minifier.js({
        srcPath: __dirname + '/build/',
        filesIn: files,
        destination: __dirname + '/build/js/all.js'
      }).on('error', function () {
        console.log(arguments);
        async(false);
      }).on('complete', function () {
        async();
      });
    }
});

...然后在应用程序代码中,我更改scripts(我用来容纳脚本以在视图中输出的变量),只是['/build/js/all.js'],而不是实际的数组文件。这给了我一个

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

...输出。同步,缩小,合理快速。

答案 3 :(得分:2)

比赛有点晚了,但这是我对这个主题的看法:

是的,这是错的。 AMD增加了#34;语法噪音&#34;在不增加任何好处的情况下进入您的项目。

它被设计为仅在需要时逐步加载模块。虽然这是出于善意,但它在大型项目中成为一个问题。我已经看过几个应用程序,只需要2秒或更长时间来引导应用程序。这是因为在客户端上解析模块之后,requirejs只能请求其他依赖项。因此,您将在开发人员工具的网络选项卡中获得类似瀑布的图片。

更好的方法是使用同步模块样式(例如CommonJS或即将推出的ES6模块)并将应用程序划分为。然后这些块只能按需加载。在webpackcode splitting)时browserify can be configured to support it too做得很好。

通常您会按照正常要求执行,例如:

var a = require("a");
var b = require("b");
var c = require("c");

然后,当您确定在某些情况下只需要模块时,请写下:

// Creates a new chunk
require.ensure(["d"], function () { // will be called after d has been requested
    var d = require("d");
});

如果d需要e模块eabc不需要require.ensure,那么它只会是包含在第二个块中。 webpack将所有块导出到输出文件夹中,并在运行时自动加载它们。你不必处理这些事情。只要您想异步加载代码,就需要使用{{1}}(或bundle- / promise - loader)。

这种方法可以在保持入口包较小的同时实现快速自举。


我在requirejs中看到的唯一优势是,开发设置非常简单。您只需添加requirejs作为脚本标记,创建一个小配置,然后您就可以开始了。

但imho有点短视,因为你需要一种策略来将你的代码分成生产中的块。这就是为什么我不认为在将代码发送到客户端之前在服务器上预处理代码的原因就会消失。