如何在使用AMD(require.js)时在Backbone.js中加载引导模型

53

Backbone.js的文档建议使用以下方式加载引导模型:

<script>
var Accounts = new Backbone.Collection;
Accounts.reset(<%= @accounts.to_json %>);
var Projects = new Backbone.Collection;
Projects.reset(<%= @projects.to_json(:collaborators => true) %>);
</script>

但是这种模式无法在AMD方式(使用require.js)中使用。

唯一可能的解决方案是声明存储JSON数据的全局变量,并在相关的初始化方法中后续使用该变量。

是否有一种更好的方法可以做到这一点(不使用全局变量)?


意译:我使用 require.js 和 bootstrap。我基本上所做的正是你不想做的。第一个获取页面具有 JSON 数据的全局变量,我进行了引导。我复制/使用这些值填充我的初始模型和集合。我不确定这是否是一种不好的方式。如果你担心污染全局空间,你不能在初始复制结果后删除这些变量吗? - jmk2142
3
"@"符号是用来引用用户或实体的标识符,在网络文化中被称为at符号。"@accounts"和"@projects"代表不同的实体,可能是用户账户或者项目名称等。 - Alexander Mills
9个回答

71

这是我们如何启动数据的方式,以避免污染全局命名空间。相反,它完全使用require.js。它还可以根据模板内的变量提供初始应用程序配置。

在渲染页面中

<script src="require.js"></script>
<script>
define('config', function() {
  return {
    bootstrappedAccounts: <%= @accounts.to_json %>,
    bootstrappedProjects: <%= @projects.to_json(:collaborators => true) %>
  };
});
</script>
<script src="app.js"></script>

globals.js

该文件检查配置并使用返回的任何数据来扩展自身。

define([
  'config',
  'underscore'
], function(config) {

  var globals = {
  };
  _.extend(globals, config);
  return globals;

});

config.js

如果您想无论页面中是否定义了config,都能够加载应用程序,则需要该文件。

define(function() {
  // empty array for cases where `config` is not defined in-page
  return {};
});

app.js

require([
  'globals',
  'underscore',
  'backbone'
], function(globals) {

  if (globals.bootstrappedAccounts) {
    var accounts = new Backbone.Collection(globals.bootstrappedAccounts);
  }
  if (globals.bootstrappedProjects) {
    var projects = new Backbone.Collection(globals.bootstrappedProjects);
  }

});

1
我还注意到,require.js优化器需要config.js以避免出错。请解释一下为什么建议使用globals.js来添加另一层。 - opengrid
1
我发现这个方法可能可行,但由于模块是异步加载的,有时候虚拟选项会在真实选项之前加载。我还发现,在优化时,虚拟选项会被优先选择而不是真实选项。这是一个有趣的想法,但根据我的经验,这并不起作用,我认为其他答案中有更好的方法来实现这一点。 - Ollie Edwards
3
这是对我有效的设置,为了使它与优化器一起正常工作,我所做的是在我的config.js中进行excludeShallow配置,这样它就总是使用在html中定义的模块。 - Asgaroth
@dlrust,请根据Asgaroth的建议更新您的回答。 - opengrid
这个可以运行,但是需要注意一点。请确保您的数据中没有 '</script>' 这个字符串,否则它会被处理并且会以惊人的方式破坏您的标签。 - goozbox
有人有描述 @ 符号/语法的链接吗?我猜这是 RequireJS 的东西? - Alexander Mills

31

看起来你可以使用 require.config() 函数或 "require" 全局对象,并在其中使用 "config" 选项来通过特殊依赖项 "module" 向模块传递数据。请参见 http://requirejs.org/docs/api.html#config-moduleconfig

常见的需求是向模块传递配置信息。这些配置信息通常作为应用程序的一部分而已知,需要有一种方法将其传递给模块。在 RequireJS 中,可以使用 requirejs.config() 的 config 选项来实现该功能。然后,模块可以通过请求特殊依赖项 "module" 并调用 module.config() 来读取该信息。

因此,对于我们拥有的引导模型,我们在顶层 HTML 页面中编写如下代码:

<script>
var require = {
    config: {
        'app': {
            bootstrappedAccounts: <%= @accounts.to_json %>
            bootstrappedProjects: <%= @projects.to_json(:collaborators => true) %>
        }
    }
};
</script>
<script src="scripts/require.js"></script>

然后在应用程序模块(app.js)中,我们有:

define(['module'], function (module) {
    var accounts = new Backbone.Collection( module.config().bootstrappedAccounts );
    var bootstrappedProjects = new Backbone.Collection( module.config().bootstrappedProjects );
});

这里的“module”是为此类情况提供的特殊依赖项。

这个代码没有经过测试,但从文档上看似乎很确定。


5
不错的解决方案,对我很有效,应该也适用于优化器(但我还没有那么远)。我注意到 'app' 对应着加载对象的模块,因此如果你想在另一个模块(例如 otherModule.js)中使用引导数据,它需要在 config: {'otherModule': otherModuleBootstrapData} 中。 - d4kris
真希望早点发现你的解决方案!刚刚花了三个小时得出同样的结论。打算发布问答,却发现了你的回复。 :-) - Dan Abramov
很棒的答案。请注意,为了让module.config()起作用,必须使用define()(如答案中所示),而不是require() - Willie

11
在RequireJS中,这可以通过requirejs.config()的配置选项来完成。然后,模块可以通过请求特殊依赖项 "module" 并调用 module.config() 来读取该信息。示例: index.html
<script>
  var require = {
    config: {
      'app': {
        'api_key': '0123456789-abc'
      }
    }
  };
</script>
<script src="js/libs/require.js" data-main="js/main"></script>

main.js

require( ['app'], function(App) {
  new App();
});

应用程序脚本(app.js)

define( ['module'], function(module) {
  var App = function() {
    console.log( 'API Key:', module.config().api_key );
  };

  return App;
});

请注意,配置对象的名称必须匹配模块的名称。在我的示例中,模块的名称是app,因此配置对象的名称也需要命名为app。在模块中,您需要将['module']作为依赖项包含,并调用module.config()[property name]来检索配置数据。

阅读有关此内容的文档:http://requirejs.org/docs/api.html#config-moduleconfig


你不能在main.js中使用define的原因是什么?除非main需要使用require而不是define,否则main.js文件似乎有点多余。换句话说,你可以将app.js中的所有代码复制到main.js中,然后将配置更改为config.main而不是config.app吗?然后你可以完全删除app.js。在测试中,这似乎可行,但由于我对requirejs还不熟悉,所以可能是不好的做法。 - Bart
只要它是一个模块,它就能够通过module.config()[property/module name]来检索分配给它的数据。然而,通常情况下,main.js被保留用于通过require语句将核心模块作为依赖项引入应用程序中。 - Tom Doe
1
上面的示例中,我可能没有使用最佳的模块名称。假设是 "apis/GoogleAnalytics.js",而不是 app.js,因为它正在检索的数据是 "api_key"。这将是一个自己的模块,需要从 main.js 中调用,而不是整个应用程序本身。在这种情况下,配置对象看起来像: require = { config: { 'apis/GoogleAnalytics' : { 'api_key': '0123456789-abc' } } }; - Tom Doe

6
一些答案让我接近了我的类似问题,但没有一个解决了它。特别是排名第一和被接受的答案似乎给了我一个讨厌的竞争条件,有时候虚拟对象会先加载。当与优化器一起使用时,这也会100%发生。它还使用显式字符串名称作为模块,而require文档明确建议不要这样做。
这是我如何工作的。与勇敢的戴夫类似,我使用配置对象来捕获参数(在我的情况下是从jsp页面),如下所示:
<script type="text/javascript">
    var require = {
        config: {
            options : { 
                bootstrappedModels : ${models}
            }
        }
    }
</script>

特别要注意,我的参数在一个叫做options的对象中。 这个名称不是可选的! 虽然文档没有提到这一点,但以下是require如何加载您的配置(在requirejs 2.1.1中的第564行):

config: function () {
    return (config.config && config.config[mod.map.id]) || {};
},

重点是 config 对象上必须有一个属性,其键为 mod.map.id 并且值为 'options'。
现在你可以像这样访问模型。
define(['module'], function(module){
    console.log(module.config().bootstrappedModels);
    //...
});

@BraveDave的答案对我有用,但这个不行,虽然我希望它能行。 - Chris Salzberg
@shioyama,有趣。你使用的requirejs版本是什么?当你尝试这种方式时,实际发生了什么?如果我们都得到不同的结果,似乎存在一些相当大的不一致性。 - Ollie Edwards
我一直无法让@BraveDave的解决方案或这个解决方案运行。目前我正在使用不规范的全局变量方法。在我的模块中,“module.config().myVar”始终为空。 - Colin Young
关于竞争条件的观点很好。我也注意到了同样的问题。对我有效的解决方案是使用@OllieEdwards的方法 - excludeShallow: ["config"]。 - opengrid

3
您可以在AMD模块的末尾添加一个循环函数,以检查init方法何时被定义(这样它就可以在body之后被填充或从包含文件中加载)。这样,模块就可以保证可用,初始化可以在准备就绪时发生。
require(...,function (...) {
   //define models collections, etc..

   var initme = function () {
     if(document.initThisModule) 
       document.initThisModule();
     else
       setTimeout(initme, 10);
   }();
});

3
我不太熟悉AMD的方法,但是不要使用全局变量,为什么不将JSON添加到DOM中呢。
例如:
var json = ...,
$jsonContainer = $(json).wrap("<script id='json-container' type='text/javascript'>").appendTo($("body"));

然后,在文档准备就绪后,不要像backbone文档建议的那样使用嵌入式脚本标签:
$(function(){
    MyCollection.reset($("#json-container").html());
    ...
});

2
虽然这样做是可行的(与原始问题一样,全局也可以),但这与AMD哲学相悖。 AMD模块应声明其所有依赖项,并理想情况下不依赖于全局变量,而“document”显然是一个全局变量(由$用于查找HTML)。 - Peter Davis

2

如上所述,'data module'(或config,或您想要称呼的任何名称)可以包含在已经生成的文件中(例如index.html),但我认为这样做相当丑陋。

另一种方法是将其声明在其自己的模块文件中,但这将需要在生产环境中向服务器进行额外的往返。一旦您想要构建和优化您的requirejs依赖关系,数据模块就不能被包含,因为它是在页面加载时动态生成的。

第三个选项可能是将其附加到您提供的文件之一(例如,优化后的requirejs文件),但我不知道如何/是否可以完成此操作。


2
像这样做怎么样:
<script>
define('Models', ['backbone'], function(Backbone) {
    var Models = {
        Accounts: new Backbone.Collection,
        Projects: new Backbone.Collection
    };

    Models.Accounts.reset(<%= @accounts.to_json %>);
    Models.Projects.reset(<%= @projects.to_json(:collaborators => true) %>);

    return Models;
});
</script>

那么您就可以像这样在其他模块中使用模型:
var models = require(['Models']);
models.Accounts.doWhatYouNeed();

或者这个:
define(['any', 'dependencies', 'and', 'Models'], function(a, b, c, Models) {
   // Models will be available here
});

@opengrid - 这就是为什么它在<script>标签中的原因。 - Peter Davis
我喜欢这种方式,作为对@dlrust答案的一种倒置。尽管如此,它依赖于您的视图和集合能够处理在它们已经被渲染之后“重置”的任何数据,虽然通常是一个好主意,但可能涉及大量额外的簿记工作。 - Peter Davis
每次调用 require(['Models']); 都会实例化 Collection 吗? - andho

1

@dlrust的答案是可行的,但不能扩展参数并从代码中的多个位置传递。如果您尝试在渲染模板中执行以下操作:

<script>
    define('config', function() {
        return {
            bootstrappedAccounts: <%= @accounts.to_json %>,
            bootstrappedProjects: <%= @projects.to_json(:collaborators => true) %>
        };
    });
</script>

在另一个文件中添加一些数据。
<script>
    define('config', function() {
        return {
            goods: <%= some data %>,
            showcaseList: <%= some json or array %>
        };
    });
</script>

这里是覆盖操作(不是扩展!!!),在配置中只会保留最后声明的数据。

我的解决方案:使用Backbone模型进行数据的设置/获取。

app.js

define("App", [], function() {
    window.App = {
        // модели
        Model: {},
        // коллекции
        Collection: {},
        // виды
        View: {},
        // роутеры
        Router: {},
        // модальные окна
        Modal: {},
        // UI компоненты
        UI: {}
    };
    return window.App;
});

global.js

define(["App", "underscore", "backbone"], function(App, _, Backbone) {
    "use strict";

    // модель глобальных данных
    App.Model.Global = Backbone.Model.extend({
        defaults: {}
    });

    return new App.Model.Global;    
});

index.php

<!DOCTYPE html>
<html>
    <head>
        <!--HEAD_START-->
        <script type="text/javascript" data-main="/app/init" src="/app/require/require.js"></script>
        <!--HEAD_END-->
    </head>

    <body>          
        <div id="tm-inner-wrap">
            <div id="loader"><i class="uk-icon-refresh uk-icon-spin"></i></div>
            <!--HEADER_START-->
            <?= $this->includeTpl('header_view'); ?>
            <!--HEADER_END-->

            <!--CONTENT_START-->
            <div>your html content data</div>
            <!--CONTENT_END-->

            <!--FOOTER_START-->
            <?= $this->includeTpl('footer_view');?>
            <!--FOOTER_END-->

            <script>
                require(["global"], function(Global) {
                    Global.set("notifyList", <?=json_encode($this->notifyList);?>);
                });
            </script>
        </div>
    </body>
</html>

另一个模板

someTemplate.php

<div class="tm-inner-body">
    <div class="uk-container uk-container-center">
        // content data
    </div>
</div>

<script>    
    require(["global", "module/index"], function(Global) {
        Global.set("goodList", <?=json_encode($this->goodList);?>);
    });
</script>

index.js

require(["App", "core", "jquery", "uikit!uikit-addons-min", "underscore", "backbone", "global", "module/good/goodView"], function(App, Core, $, UIkit, _, Backbone, Global, goodView) {
    "use strict";

    // Global.get("notifyList"); its too able

    App.Collection.Good = new Backbone.Collection(Global.get("showcaseList")["items"]);

    // вид списка товаров
    App.View.GoodList = Backbone.View.extend({
        // елемент
        el: ".tm-good-list",
        // init
        initialize: function() {
            this.collection = App.Collection.Good;
            // список товаров
            this.drawList();
        },
        // отрисовка списка
        drawList: function() {
            this.$el.empty();
            this.collection.each(function(item, index) {
                this.$el.append(this.drawItem(item));
            }, this);
        },
        // отрисовка елемента
        drawItem: function(data) {
            var good = new goodView({model: data});
            return good.render().el;
        }
    });

    App.View.Index = Backbone.View.extend({
        el: "body",
        // пользовательские события
        events: {
            //
        },
        // init
        initialize: function() {
            var $this = this;
            if(Global.get("showcaseList")) new App.View.GoodList();
        }
    });

    new App.View.Index();
});

文件结构:

file structure


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