EmberJS: 如何在同一路线上加载多个模型?

虽然我对 web 开发并不陌生,但对客户端 MVC 框架却是相当陌生的。我做了一些研究,决定尝试一下 EmberJS。我浏览了 TodoMVC 指南,它对我来说是有意义的..。

我已经设置了一个非常基本的应用程序; 索引路线,两个模型和一个模板。我运行了一个服务器端 php 脚本,它返回一些 db 行。

有一件事让我非常困惑,那就是如何在同一条路线上加载多个模型。我已经阅读了一些关于使用 setupController 的信息,但是我仍然不清楚。在我的模板中,我有两个表,我试图加载与不相关的数据库行。在一个更传统的 web 应用程序中,我只需要发出 sql 语句并循环它们来填充行。我有困难翻译这个概念到 EmberJS。

如何在同一路由上加载多个不相关数据模型?

我正在使用最新的 Ember 和 Ember 数据库。

更新

虽然第一个答案给出了处理它的方法,但是第二个答案解释了什么时候合适,以及什么时候不合适的不同方法。

25228 次浏览

BEWARE:

You want to be careful about whether or not returning multiple models in your model hook is appropriate. Ask yourself this simple question:

  1. Does my route load dynamic data based on the url using a slug :id? i.e. this.resource('foo', {path: ':id'});

If you answered yes

Do not attempt to load multiple models from the model hook in that route!!! The reason lies in the way Ember handles linking to routes. If you provide a model when linking to that route (\{\{link-to 'foo' model}}, transitionTo('foo', model)) it will skip the model hook and use the supplied model. This is probably problematic since you expected multiple models, but only one model would be delivered. Here's an alternative:

Do it in setupController/afterModel

App.IndexRoute = Ember.Route.extend({
model: function(params) {
return $.getJSON('/books/' + params.id);
},
setupController: function(controller, model){
this._super(controller,model);
controller.set('model2', {bird:'is the word'});
}
});

Example: http://emberjs.jsbin.com/cibujahuju/1/edit

If you need it to block the transition (like the model hook does) return a promise from the afterModel hook. You will need to manually keep track of the results from that hook and hook them up to your controller.

App.IndexRoute = Ember.Route.extend({
model: function(params) {
return $.getJSON('/books/' + params.id);
},
afterModel: function(){
var self = this;
return $.getJSON('/authors').then(function(result){
self.set('authors', result);
});
},
setupController: function(controller, model){
this._super(controller,model);
controller.set('authors', this.get('authors'));
}
});

Example: http://emberjs.jsbin.com/diqotehomu/1/edit

If you answered no

Go ahead, let's return multiple models from the route's model hook:

App.IndexRoute = Ember.Route.extend({
model: function() {
return {
model1: ['red', 'yellow', 'blue'],
model2: ['green', 'purple', 'white']
};
}
});

Example: http://emberjs.jsbin.com/tuvozuwa/1/edit

If it's something that needs to be waited on (such as a call to the server, some sort of promise)

App.IndexRoute = Ember.Route.extend({
model: function() {
return Ember.RSVP.hash({
model1: promise1,
model2: promise2
});
}
});

Example: http://emberjs.jsbin.com/xucepamezu/1/edit

In the case of Ember Data

App.IndexRoute = Ember.Route.extend({
var store = this.store;
model: function() {
return Ember.RSVP.hash({
cats: store.find('cat'),
dogs: store.find('dog')
});
}
});

Example: http://emberjs.jsbin.com/pekohijaku/1/edit

If one is a promise, and the other isn't, it's all good, RSVP will gladly just use that value

App.IndexRoute = Ember.Route.extend({
var store = this.store;
model: function() {
return Ember.RSVP.hash({
cats: store.find('cat'),
dogs: ['pluto', 'mickey']
});
}
});

Example: http://emberjs.jsbin.com/coxexubuwi/1/edit

Mix and match and have fun!

App.IndexRoute = Ember.Route.extend({
var store = this.store;
model: function() {
return Ember.RSVP.hash({
cats: store.find('cat'),
dogs: Ember.RSVP.Promise.cast(['pluto', 'mickey']),
weather: $.getJSON('weather')
});
},
setupController: function(controller, model){
this._super(controller, model);
controller.set('favoritePuppy', model.dogs[0]);
}
});

Example: http://emberjs.jsbin.com/joraruxuca/1/edit

NOTE: for Ember 3.16+ apps, here is the same code, but with updated syntax / patterns: https://stackoverflow.com/a/62500918/356849

The below is for Ember < 3.16, even though the code would work as 3.16+ as fully backwards compatible, but it's not always fun to write older code.


You can use the Ember.RSVP.hash to load several models:

app/routes/index.js

import Ember from 'ember';


export default Ember.Route.extend({
model() {
return Ember.RSVP.hash({
people: this.store.findAll('person'),
companies: this.store.findAll('company')
});
},


setupController(controller, model) {
this._super(...arguments);
Ember.set(controller, 'people', model.people);
Ember.set(controller, 'companies', model.companies);
}
});

And in your template you can refer to people and companies to get the loaded data:

app/templates/index.js

<h2>People:</h2>
<ul>
\{\{#each people as |person|}}
<li>\{\{person.name}}</li>
\{\{/each}}
</ul>
<h2>Companies:</h2>
<ul>
\{\{#each companies as |company|}}
<li>\{\{company.name}}</li>
\{\{/each}}
</ul>

This is a Twiddle with this sample: https://ember-twiddle.com/c88ce3440ab6201b8d58

I use something like the answer that Marcio provided but it looks something like this:

    var products = Ember.$.ajax({
url: api + 'companies/' +  id +'/products',
dataType: 'jsonp',
type: 'POST'
}).then(function(data) {
return data;
});


var clients = Ember.$.ajax({
url: api + 'clients',
dataType: 'jsonp',
type: 'POST'
}).then(function(data) {
return data;
});


var updates = Ember.$.ajax({
url: api + 'companies/' +  id + '/updates',
dataType: 'jsonp',
type: 'POST'
}).then(function(data) {
return data;
});


var promises = {
products: products,
clients: clients,
updates: updates
};


return Ember.RSVP.hash(promises).then(function(data) {
return data;
});

If you use Ember Data, it gets even simpler for unrelated models:

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

If you only want to retrieve an object's property for model2, use DS.PromiseObject instead of 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'))
});
}
});

The latest version of JSON-API as implemented in Ember Data v1.13 supports bundling of different resources in the same request very well, if you don't mind modifying your API endpoints.

In my case, I have a session endpoint. The session relates to a user record, and the user record relates to various models that I always want loaded at all times. It's pretty nice for it all to come in with the one request.

One caveat per the spec is that all of the entities you return should be linked somehow to the primary entity being received. I believe that ember-data will only traverse the explicit relationships when normalizing the JSON.

For other cases, I'm now electing to defer loading of additional models until the page is already loaded, i.e. for separate panels of data or whatever, so at least the page is rendered as quickly as possible. Doing this there's some loss/change with the "automatic" error loading state to be considered.

Taking the accepted answer, and updating it for Ember 3.16+

app/routes/index.js

import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';


export default class IndexRoute extends Route {
@service store;


async model() {
let [people, companies] = await Promise.all([
this.store.findAll('person'),
this.store.findAll('company'),
]);




return { people, companies };
}


}

Note, it's recommended to not use setupController to setup aliases, as it obfuscates where data is coming from and how it flows from route to template.

So in your template, you can do:

<h2>People:</h2>


<ul>
\{\{#each @model.people as |person|}}
<li>\{\{person.name}}</li>
\{\{/each}}
</ul>


<h2>Companies:</h2>


<ul>
\{\{#each @model.companies as |company|}}
<li>\{\{company.name}}</li>
\{\{/each}}
</ul>