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

Posted

技术标签:

【中文标题】以同步方式使用requirejs(AMD)是错误的吗?【英文标题】:Is it wrong to use requirejs (AMD) in a synchronous way? 【发布时间】:2013-06-08 08:55:59 【问题描述】:

我正在开发一个 javascript 繁重的网络应用程序;重如,没有 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,这让我相信互联网上大多数建议避免这种方法,因为它阻止并行下载已经过时了。

还是;

    AMD(异步模块定义)暗示这不是 requirejs 的使用方式。

    在开发中,我想将未合并的文件作为多个脚本标签插入,而不是单个缩小文件;

    <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 的依赖项列表),感觉错误。 p>

因此我的问题如下:

    关于避免同步 &lt;script /&gt; 的建议过时,我是否正确? 以这种方式使用 requirejs/AMD 是否像感觉上的错误? 是否有我错过的替代技术/方法/工具/模式?

【问题讨论】:

您可以考虑将您的声音添加到我在邮件列表中开始的这个线程中。 groups.google.com/forum/?fromgroups#!topic/requirejs/… 【参考方案1】:

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

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

【讨论】:

【参考方案2】:

先花点时间想想为什么要使用 requirejs。它有助于管理您的依赖关系,避免一长串必须按正确顺序排列的脚本标签。您可能会争辩说,这只有在涉及大量脚本时才会变得难以管理。

其次,它异步加载脚本。同样,使用大量脚本可以大大减少加载时间,但使用少量脚本时好处较小。

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

编辑:

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

【讨论】:

【参考方案3】:

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

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

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

这就是 TL;DR。下面,我将详细介绍我是如何做到的。对不起,长度。希望这可以帮助某人……或者至少给某人一些灵感!


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

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));

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

#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> 

... 输出。同步、缩小、相当快。

【讨论】:

【参考方案4】:

虽然有点晚了,但这是我对这个话题的看法:

是的,这是错误的。 AMD 为您的项目添加了“语法噪音”,但没有增加任何好处。

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

更好的方法是使用同步模块样式(例如 CommonJS 或即将推出的 ES6 模块)并将应用程序划分为 。然后这些块只能按需加载。 webpack 在code 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 并且e 不需要abc,那么它只会包含在第二个块中。 webpack 将所有块导出到输出文件夹并在运行时自动加载它们。你不必处理这些事情。只要您想异步加载代码,只需使用require.ensure(或bundle-/promise-loader)即可。

这种方法可以实现快速引导,同时保持条目包较小。


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

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

【讨论】:

以上是关于以同步方式使用requirejs(AMD)是错误的吗?的主要内容,如果未能解决你的问题,请参考以下文章

前端amd和cmd的区别

使用插件将RequireJS / AMD迁移到Webpack

Amd,Cmd, Commonjs, ES6 import/export的异同点

SeaJS与RequireJS最大的区别

RequireJS入门

RequireJS 订单插件和 Dojo 1.7.1