如何在 EmberJS / Ember Data 中使用单个路由的多个模型?

Posted

技术标签:

【中文标题】如何在 EmberJS / Ember Data 中使用单个路由的多个模型?【英文标题】:How to use multiple models with a single route in EmberJS / Ember Data? 【发布时间】:2013-05-04 01:23:14 【问题描述】:

通过阅读文档,您似乎必须(或应该)将模型分配给这样的路线:

App.PostRoute = Ember.Route.extend(
    model: function() 
        return App.Post.find();
    
);

如果我需要在某个路由中使用多个对象怎么办?即帖子、评论和用户?我如何告诉加载这些的路线?

【问题讨论】:

【参考方案1】:

永远最后更新:我不能继续更新了。所以这已被弃用,很可能会是这样。这是一个更好,更新的帖子EmberJS: How to load multiple models on the same route?

更新:在我原来的回答中,我说过在模型定义中使用embedded: true。这是不正确的。在修订版 12 中,Ember-Data 期望外键定义为带有后缀 (link) _id 用于单个记录或 _ids 用于集合。类似于以下内容:


    id: 1,
    title: 'string',
    body: 'string string string string...',
    author_id: 1,
    comment_ids: [1, 2, 3, 6],
    tag_ids: [3,4]

我已经更新了小提琴,如果有任何变化或者我发现这个答案中提供的代码有更多问题,我会再次这样做。


用相关模型回答:

对于您描述的场景,我将依赖模型之间的associations (设置embedded: true,并且只在该路由中加载Post 模型,考虑到我可以定义一个@在 CommentPost 模型中,Comment 模型的 987654334@ 关联和 UserDS.belongsTo 关联。像这样的:

App.User = DS.Model.extend(
    firstName: DS.attr('string'),
    lastName: DS.attr('string'),
    email: DS.attr('string'),
    posts: DS.hasMany('App.Post'),
    comments: DS.hasMany('App.Comment')
);

App.Post = DS.Model.extend(
    title: DS.attr('string'),
    body: DS.attr('string'),
    author: DS.belongsTo('App.User'),
    comments: DS.hasMany('App.Comment')
);

App.Comment = DS.Model.extend(
    body: DS.attr('string'),
    post: DS.belongsTo('App.Post'),
    author: DS.belongsTo('App.User')
);

这个定义会产生如下的结果:

根据这个定义,每当我find 一个帖子时,我都可以访问与该帖子相关联的 cmets 集合,以及评论的作者,以及作为帖子作者的用户,因为它们都是嵌入的。路线很简单:

App.PostsPostRoute = Em.Route.extend(
    model: function(params) 
        return App.Post.find(params.post_id);
    
);

所以在PostRoute(或者PostsPostRoute,如果你使用resource)中,我的模板将可以访问控制器的content,也就是Post模型,所以我可以参考作者,简写为author

<script type="text/x-handlebars" data-template-name="posts/post">
    <h3>title</h3>
    <div>by author.fullName</div><hr />
    <div>
        body
    </div>
    partial comments
</script>

<script type="text/x-handlebars" data-template-name="_comments">
    <h5>Comments</h5>
    #each content.comments
    <hr />
    <span>
        this.body<br />
        <small>by this.author.fullName</small>
    </span>
    /each
</script>

(见fiddle)


用不相关的模型回答:

但是,如果您的场景比您描述的要复杂一些,并且/或者 必须 为特定路线使用(或查询)不同的模型,我建议您在 @ 987654325@。例如:

App.PostsPostRoute = Em.Route.extend(
    model: function(params) 
        return App.Post.find(params.post_id);
    ,
    // in this sample, "model" is an instance of "Post"
    // coming from the model hook above
    setupController: function(controller, model) 
        controller.set('content', model);
        // the "user_id" parameter can come from a global variable for example
        // or you can implement in another way. This is generally where you
        // setup your controller properties and models, or even other models
        // that can be used in your route's template
        controller.set('user', App.User.find(window.user_id));
    
);

现在,当我在 Post 路由中时,我的模板将可以访问控制器中的 user 属性,因为它是在 setupController 钩子中设置的:

<script type="text/x-handlebars" data-template-name="posts/post">
    <h3>title</h3>
    <div>by controller.user.fullName</div><hr />
    <div>
        body
    </div>
    partial comments
</script>

<script type="text/x-handlebars" data-template-name="_comments">
    <h5>Comments</h5>
    #each content.comments
    <hr />
    <span>
        this.body<br />
        <small>by this.author.fullName</small>
    </span>
    /each
</script>

(见fiddle)

【讨论】:

感谢您这么多抽出宝贵的时间发布,我发现它真的很有用。 @MilkyWayJoe,非常好的帖子!现在我的方法看起来真的很幼稚:) 非相关模型的问题在于它不像模型钩子那样接受承诺,对吧?有什么解决方法吗? 如果我正确理解您的问题,您可以通过简单的方式进行调整。只需等待承诺实现,然后将模型设置为控制器的变量 除了迭代和显示 cmets,如果你能展示一个例子来说明某人如何向post.comments 添加新评论,那就太好了。【参考方案2】:

使用Em.Object封装多个模型是获取model钩子中所有数据的好方法。但不能保证视图渲染后所有数据都准备好。

另一种选择是使用Em.RSVP.hash。它将几个 Promise 组合在一起并返回一个新的 Promise。新的 Promise 如果在所有 Promise 都解决后解决。而setupController 在promise 被解决或被拒绝之前不会被调用。

App.PostRoute = Em.Route.extend(
  model: function(params) 
    return Em.RSVP.hash(
      post:     // promise to get post
      comments: // promise to get comments,
      user:     // promise to get user
    );
  ,

  setupController: function(controller, model) 
    // You can use model.post to get post, etc
    // Since the model is a plain object you can just use setProperties
    controller.setProperties(model);
  
);

通过这种方式,您可以在视图渲染之前获得所有模型。而使用Em.Object则没有这个优势。

另一个优点是您可以结合承诺和非承诺。像这样:

Em.RSVP.hash(
  post: // non-promise object
  user: // promise object
);

点击此处了解更多关于Em.RSVP的信息:https://github.com/tildeio/rsvp.js


但如果您的路线有动态路段,请不要使用Em.ObjectEm.RSVP 解决方案

主要问题是link-to。如果您通过单击由link-to 生成的带有模型的链接来更改 url,则模型将直接传递到该路由。 在这种情况下,model 钩子不会被调用,在setupController 中你会得到模型link-to 给你。

一个例子是这样的:

路线代码:

App.Router.map(function() 
  this.route('/post/:post_id');
);

App.PostRoute = Em.Route.extend(
  model: function(params) 
    return Em.RSVP.hash(
      post: App.Post.find(params.post_id),
      user: // use whatever to get user object
    );
  ,

  setupController: function(controller, model) 
    // Guess what the model is in this case?
    console.log(model);
  
);

还有link-to代码,帖子是模特:

#link-to "post" postSome post/link-to

这里的事情变得有趣了。当你使用 url /post/1 访问页面时,model 钩子被调用,setupController 在 Promise 解决时获取普通对象。

但是如果你通过点击link-to链接访问页面,它会将post模型传递给PostRoute,并且路由会忽略model钩子。在这种情况下setupController 会得到post 模型,当然你不能得到用户。

所以请确保不要在具有动态路段的路线中使用它们。

【讨论】:

我的回答适用于 Ember 和 Ember-Data 的早期版本。这是一个非常好的方法+1 其实有。如果您想将模型 ID 而不是模型本身传递给链接到帮助器,则始终会触发模型挂钩。 这应该记录在 Ember 指南的某个地方(最佳实践或其他内容)。这是一个重要的用例,我相信很多人都会遇到。 如果您使用controller.setProperties(model);,请不要忘记将这些具有默认值的属性添加到控制器。否则会抛出异常Cannot delegate set...【参考方案3】:

有一段时间我在使用Em.RSVP.hash,但是我遇到的问题是我不希望我的视图等到所有模型都加载完毕后再进行渲染。然而,感谢Novelys 的人们,我找到了一个很棒的(但相对未知的)解决方案,其中涉及使用Ember.PromiseProxyMixin:

假设您有一个包含三个不同视觉部分的视图。这些部分中的每一个都应由其自己的模型支持。支持视图顶部“splash”内容的模型很小,加载速度很快,因此您可以正常加载:

创建路由main-page.js

import Ember from 'ember';

export default Ember.Route.extend(
    model: function() 
        return this.store.find('main-stuff');
    
);

然后就可以创建对应的Handlebars模板main-page.hbs

<h1>My awesome page!</h1>
<ul>
#each thing in model
    <li>thing.name is really cool.</li>
/each
</ul>
<section>
    <h1>Reasons I Love Cheese</h1>
</section>
<section>
    <h1>Reasons I Hate Cheese</h1>
</section>

因此,假设在您的模板中,您希望有关于您对奶酪的爱/恨关系的单独部分,每个部分(出于某种原因)都有自己的模型支持。您在每个模型中都有许多记录,其中包含与每个原因相关的大量详细信息,但是您希望顶部的内容能够快速呈现。这就是 render 助手的用武之地。您可以这样更新您的模板:

<h1>My awesome page!</h1>
<ul>
#each thing in model
    <li>thing.name is really cool.</li>
/each
</ul>
<section>
    <h1>Reasons I Love Cheese</h1>
    render 'love-cheese'
</section>
<section>
    <h1>Reasons I Hate Cheese</h1>
    render 'hate-cheese'
</section>

现在您需要为每个创建控制器和模板。因为在这个例子中它们实际上是相同的,所以我只使用一个。

创建一个名为love-cheese.js的控制器:

import Ember from 'ember';

export default Ember.ObjectController.extend(Ember.PromiseProxyMixin, 
    init: function() 
        this._super();
        var promise = this.store.find('love-cheese');
        if (promise) 
            return this.set('promise', promise);
        
    
);

您会注意到我们在这里使用了PromiseProxyMixin,这使控制器具有承诺意识。当控制器初始化时,我们指出承诺应该通过 Ember Data 加载 love-cheese 模型。您需要在控制器的 promise 属性上设置此属性。

现在,创建一个名为 love-cheese.hbs 的模板:

#if isPending
  <p>Loading...</p>
else
  #each item in promise._result 
    <p>item.reason</p>
  /each
/if

在您的模板中,您将能够根据承诺的状态呈现不同的内容。当您的页面最初加载时,您的“我喜欢奶酪的原因”部分将显示 Loading...。当 Promise 被加载时,它会为你的模型的每条记录渲染所有相关的原因。

每个部分都将独立加载,不会阻止主要内容立即呈现。

这是一个简单的例子,但我希望其他人都发现它和我一样有用。

如果您希望对多行内容执行类似的操作,您可能会发现上面的 Novellys 示例更加相关。如果没有,上述内容应该适合您。

【讨论】:

【参考方案4】:

这可能不是最佳实践,也不是一种幼稚的方法,但它从概念上展示了您将如何在一条中心路线上拥有多个可用模型:

App.PostRoute = Ember.Route.extend(
  model: function() 
    var multimodel = Ember.Object.create(
      
        posts: App.Post.find(),
        comments: App.Comments.find(),
        whatever: App.WhatEver.find()
      );
    return multiModel;
  ,
  setupController: function(controller, model) 
    // now you have here model.posts, model.comments, etc.
    // as promises, so you can do stuff like
    controller.set('contentA', model.posts);
    controller.set('contentB', model.comments);
    // or ...
    this.controllerFor('whatEver').set('content', model.whatever);
  
);

希望对你有帮助

【讨论】:

这种方法很好,只是没有过多地利用 Ember Data。对于模型不相关的某些场景,我会得到类似的东西。【参考方案5】:

感谢所有其他出色的答案,我创建了一个 mixin,它将这里的最佳解决方案组合成一个简单且可重用的界面。它为您指定的模型在afterModel 中执行Ember.RSVP.hash,然后将属性注入setupController 中的控制器。它不会干扰标准的 model 钩子,因此您仍然可以将其定义为正常。

使用示例:

App.PostRoute = Ember.Route.extend(App.AdditionalRouteModelsMixin, 

  // define your model hook normally
  model: function(params) 
    return this.store.find('post', params.post_id);
  ,

  // now define your other models as a hash of property names to inject onto the controller
  additionalModels: function() 
    return 
      users: this.store.find('user'), 
      comments: this.store.find('comment')
    
  
);

这里是混合:

App.AdditionalRouteModelsMixin = Ember.Mixin.create(

  // the main hook: override to return a hash of models to set on the controller
  additionalModels: function(model, transition, queryParams) ,

  // returns a promise that will resolve once all additional models have resolved
  initializeAdditionalModels: function(model, transition, queryParams) 
    var models, promise;
    models = this.additionalModels(model, transition, queryParams);
    if (models) 
      promise = Ember.RSVP.hash(models);
      this.set('_additionalModelsPromise', promise);
      return promise;
    
  ,

  // copies the resolved properties onto the controller
  setupControllerAdditionalModels: function(controller) 
    var modelsPromise;
    modelsPromise = this.get('_additionalModelsPromise');
    if (modelsPromise) 
      modelsPromise.then(function(hash) 
        controller.setProperties(hash);
      );
    
  ,

  // hook to resolve the additional models -- blocks until resolved
  afterModel: function(model, transition, queryParams) 
    return this.initializeAdditionalModels(model, transition, queryParams);
  ,

  // hook to copy the models onto the controller
  setupController: function(controller, model) 
    this._super(controller, model);
    this.setupControllerAdditionalModels(controller);
  
);

【讨论】:

【参考方案6】:

https://***.com/a/16466427/2637573 适用于相关模型。但是,对于最新版本的 Ember CLI 和 Ember Data,对于不相关的模型有一种更简单的方法:

import Ember from 'ember';
import DS from 'ember-data';

export default Ember.Route.extend(
  setupController: function(controller, model) 
    this._super(controller,model);
    var model2 = DS.PromiseArray.create(
      promise: this.store.find('model2')
    );
    model2.then(function() 
      controller.set('model2', model2)
    );
  
);

如果您只想检索model2 的对象属性,请使用DS.PromiseObject 而不是DS.PromiseArray:

import Ember from 'ember';
import DS from 'ember-data';

export default Ember.Route.extend(
  setupController: function(controller, model) 
    this._super(controller,model);
    var model2 = DS.PromiseObject.create(
      promise: this.store.find('model2')
    );
    model2.then(function() 
      controller.set('model2', model2.get('value'))
    );
  
);

【讨论】:

我有一个post 编辑路线/视图,我想在其中呈现数据库中的所有现有标签,以便用户可以单击将它们添加到正在编辑的帖子中。我想定义一个代表这些标签的数组/集合的变量。你上面使用的方法可以解决这个问题吗? 当然,您会创建一个PromiseArray(例如“标签”)。然后,在您的模板中,您会将其传递给相应表单的选择元素。【参考方案7】:

添加到 MilkyWayJoe 的答案,顺便说一句:

this.store.find('post',1) 

返回


    id: 1,
    title: 'string',
    body: 'string string string string...',
    author_id: 1,
    comment_ids: [1, 2, 3, 6],
    tag_ids: [3,4]
;

作者是


    id: 1,
    firstName: 'Joe',
    lastName: 'Way',
    email: 'MilkyWayJoe@example.com',
    points: 6181,
    post_ids: [1,2,3,...,n],
    comment_ids: [1,2,3,...,n],

cmets


    id:1,
    author_id:1,
    body:'some words and stuff...',
    post_id:1,

... 我相信反向链接很重要,这样才能建立完整的关系。希望对某人有所帮助。

【讨论】:

【参考方案8】:

您可以使用beforeModelafterModel 钩子,因为它们总是被调用,即使model 因为您使用动态段而未被调用。

根据asynchronous routing 文档:

模型挂钩涵盖了暂停承诺转换的许多用例,但有时您需要相关挂钩 beforeModel 和 afterModel 的帮助。最常见的原因是,如果您通过 link-to 或 transitionTo 转换到具有动态 URL 段的路由(而不是由 URL 更改引起的转换),您的路由模型'正在过渡到将已经被指定(例如 #link-to 'article' article 或 this.transitionTo('article', article)),在这种情况下模型挂钩不会被调用。在这些情况下,您需要使用 beforeModel 或 afterModel 挂钩来容纳任何逻辑,同时路由器仍在收集所有路由模型以执行转换。

假设你的SiteController 上有一个themes 属性,你可以有这样的东西:

themes: null,
afterModel: function(site, transition) 
  return this.store.find('themes').then(function(result) 
    this.set('themes', result.content);
  .bind(this));
,
setupController: function(controller, model) 
  controller.set('model', model);
  controller.set('themes', this.get('themes'));

【讨论】:

我认为在 Promise 中使用 this 会给出错误消息。您可以在返回之前设置var _this = this,然后在then( 方法中执行_this.set( 以获得所需的结果

以上是关于如何在 EmberJS / Ember Data 中使用单个路由的多个模型?的主要内容,如果未能解决你的问题,请参考以下文章

将 emberJS 应用程序更新为 ember-cli 和 data 3.28 后出现阻塞错误

EmberJS / Ember-data:尽管存在所有ID,但hasMany集合不完整

如何在 Ember JS 中获取模型数据

如何在 EmberJS 应用程序中启用 CORS?

如何构建 Ember.js 应用程序

Ember-data 嵌入记录当前状态?