Ember 2, отфильтруйте модели отношений (hasMany, ownTo) и вычислите вычисленное свойство на основе отношений
Это мои файлы:
модели
приложение / модели /basket.js:
export default DS.Model.extend({
name: DS.attr('string'),
house: DS.belongsTo('house', { async: true }),
boxes: DS.hasMany('box', { async: true })
});
приложение / модели /box.js:
export default DS.Model.extend({
qty: DS.attr('number'),
basket: DS.belongsTo('basket'),
cartLines: DS.hasMany('cart-line', { async: true })
});
приложение / модель / корзина-line.js:
export default DS.Model.extend({
qty: DS.attr('number'),
box: DS.belongsTo('box'),
product: DS.belongsTo('product')
});
приложение / модели /product.js:
export default DS.Model.extend({
name: DS.attr('string'),
price: DS.attr('number')
});
Маршруты
приложение / маршруты /basket.js:
export default Ember.Route.extend({
model(params) {
return Ember.RSVP.hash({
basket: this.store.findRecord('basket', params.basket_id),
boxes: this.store.findAll('box'),
products: this.store.findAll('product')
});
},
setupController(controller, models) {
controller.setProperties(models);
}
});
Контроллеры
приложение / контроллеры /basket.js:
export default Ember.Controller.extend({
subTotal: Ember.computed('boxes.@each.cartLines', function () {
return this.products.reduce((price, product) => {
var total = price + product.get('price');
return total;
}, 0);
})
});
Вопросы:
Я новичок, поэтому я учусь и делаю ошибки. Сожалею.
1) Какой лучший способ фильтрации отношений Ember при первом входе в маршрут? Например, теперь я загружаю каждую коробку в своем приложении с boxes: this.store.findAll('box')
, Мне нужен способ не загружать всю коробку в моем веб-приложении, только одну в корзине. Мне нужен "запрос с фильтром" прямо из бэкэнда?
ОБНОВЛЕННЫЙ ВОПРОС2) Какой метод Ember является лучшим для расчета подытога? Теперь, с кодом ниже, Ember дает мне итоговую сумму, но только в console.log(tot)
и после обещаний! Почему это? Как я могу ждать обещания? Я не понимаю, что делать:
subTotal: Ember.computed('basket.boxes.@each.cartLines', function () {
let count = 0;
console.log('subTotal called: ', count);
// It should be 0 ever
count = count + 1;
return this.get('basket.boxes').then(boxes => {
boxes.forEach(box => {
box.get('cartLines').then(cartLines => {
cartLines.reduce(function (tot, value) {
console.log('tot:', tot + value.get('product.price'));
return tot + value.get('product.price');
}, 0);
});
});
});
});
Это дает мне в шаблоне [объект объекта], потому что я также использую в HBS {{log subTotal}}
и в консоли это дает мне это:
subTotal called: 0
ember.debug.js:10095 Class {__ember1476746185015: "ember802", __ember_meta__: Meta}
subTotal called: 0
ember.debug.js:10095 Class {__ember1476746185015: "ember934", __ember_meta__: Meta}
ember.debug.js:10095 Class {isFulfilled: true, __ember1476746185015: "ember934", __ember_meta__: Meta}
subTotal called: 0
ember.debug.js:10095 Class {__ember1476746185015: "ember1011", __ember_meta__: Meta}
ember.debug.js:10095 Class {isFulfilled: true, __ember1476746185015: "ember1011", __ember_meta__: Meta}
tot: 3.5
tot: 6
tot: 13.5
tot: 21
tot: 24.5
tot: 27
tot: 3.5
tot: 6
tot: 13.5
tot: 21
tot: 24.5
tot: 27
tot: 3.5
tot: 6
tot: 13.5
tot: 21
tot: 24.5
tot: 27
Почему это показывает три раза subTotal called: 0
независимо от того, есть ли ноль или один или тысяча продуктов. Он всегда звонит три раза subTotal called: 0
зачем?
Хорошо ли использовать вычисляемые свойства с обещаниями?
3) Прав ли я с инкапсуляцией этих отношений?
ОБНОВЛЕНИЕ ВОПРОСА 2:
Сейчас я использую этот код, но безуспешно:
import Ember from 'ember';
import DS from 'ember-data';
export default Ember.Controller.extend({
totalCount: Ember.computed('basket.boxes.@each.cartLines', function () {
let total = 0;
const promise = this.get('basket.boxes').then(boxes => {
boxes.map(box => {
// const trypromise = boxes.map(box => {
console.log('box:', box);
box.get('cartLines').then(cartLines => {
console.log('cartLines:', cartLines);
const cartLinesPromise = cartLines.map(cartLine => {
console.log('cartLine:', cartLine);
// return cartLine.get('qty');
// return cartLine;
// });
return {
qty: cartLine.get('qty'),
price: cartLine.get('product.price')
};
// return cartLines.map(cartLine => {
// console.log('cartLine:', cartLine);
// return cartLine.get('qty');
// // return {
// // qty: cartLine.get('qty'),
// // price: cartLine.get('product.price')
// // };
// });
})
// });
return Ember.RSVP
.all(cartLinesPromise)
.then(cartLinesPromise => {
console.log('cartLinesPromise:', cartLinesPromise);
// cartLinesPromise.reduce((tot, price) => {
// console.log('tot:', tot);
// console.log('price:', price);
// console.log('tot+price:', tot + price);
// return tot + price, 0;
// });
return total = 10;
// return total;
})
});
});
// return total;
});
return DS.PromiseObject.create({ promise });
})
})
Комментарии для многих пытаются.
В шаблоне я использую:
{{log 'HBS totalCount:' totalCount}}
{{log 'HBS totalCount.content:' totalCount.content}}
Total: {{totalCount.content}}
Но promise
иметь null
содержание.
Где я не прав?
Любой неверный return
?
Является ли этот код "многообещающим" правильным?
2 ответа
Нет ничего плохого в том, чтобы быть новичком в технологии, особенно когда ваш вопрос хорошо отформатирован и продуман.
1) Какой лучший способ Ember-Data фильтровать отношения?
Это сложный вопрос с множеством возможных окончаний.
Самое простое, что нужно сделать, это просто спросить об этой модели.
Спросить на корзине
Учитывая вашу модель вы можете сделать:
model(params) {
// we will return basket but make boxes ready
return this.get('store').find('basket', params.basket_id).then(basket => {
return basket.get('boxes').then(() => basket);
});
}
Но это имеет несколько ограничений и преимуществ
- вам нужно отправить идентификаторы с корзиной
- Вы должны включить coalesceFindRequests, чтобы сделать его нормальным
- он будет загружать только ящики, которых нет в магазине
Редактировать:you need to send ids with basket
Это означает, что basket
в вашей полезной нагрузке придется предоставить идентификацию для своих ящиков. В случае отдыха api: {basket: {id: 1, boxes: [1,2,3], ...}
, Затем он проверит, какие идентификаторы еще не загружены в хранилище, и спросит здесь api (при условии, что поле с идентификатором 2 уже загружено): /boxes?ids[]=1&ids[]=3
,
Спроси себя
model(params) {
const store = this.get('store');
const basket = params.basket_id;
return RSVP.hash({
model: store.find('basket', basket),
boxes: store.query('box', {basket}),
});
},
- С другой стороны, этот подход будет отправлять запрос на корзину только в том случае, если корзина еще не в магазине (такая же, как раньше), но всегда запрашивать ящики (если вам это не нравится, вам придется использовать peekAll и фильтр, чтобы проверить, есть ли у вас все они или что-то в этом роде).
- Хорошо, что запросы будут параллельными, а не последовательными, поэтому это может ускорить процесс.
- Корзина также не должна отправлять идентификаторы своих ящиков.
- Вы можете выполнить фильтрацию на стороне сервера, изменив
query
пары
Редактировать:if you don't like it you would have to use peekAll and filter to check if you have all of them
Вы можете проверить это с помощью hasMany.
Загружать их
Вместо того, чтобы отправлять два запроса на сервер, вы можете создать свой API, чтобы он добавлял поля в полезную нагрузку.
Загрузи только корзину и дай отдохнуть загрузке из шаблона
Вы можете загрузить только минимальный объем (например, загрузить только корзину), позволить ember продолжить и отобразить страницу. Он увидит, что вы получаете доступ basket.boxes
собственности и получить их. Это не будет хорошо выглядеть само по себе и потребует некоторой дополнительной работы, такой как счетчики и так далее. Но это один из способов, как ускорить загрузку и начальное время рендеринга.
2) Какой самый лучший способ Ember для расчета подытога
Вы хотите вычислить сумму чего-то, что на трех уровнях глубоко в асинхронных отношениях, это будет непросто. Прежде всего я бы предложил поместить вычисляемое свойство totalPrice в саму модель корзины. Вычисленные свойства лениво оцениваются, поэтому нет снижения производительности, и эта модель должна быть в состоянии обеспечить.
Вот небольшой фрагмент:
// basket.js
const {RSVP, computed} = Ember;
price: computed('boxes.@each.price', function() {
const promise = this.get('boxes').then(boxes => {
// Assuming box.get('price') is computed property like this
// and returns promise because box must wait for cart lines to resolve.
const prices = boxes.map(box => box.get('price'));
return RSVP
.all(prices)
.then(prices => prices.reduce((carry, price) => carry + price, 0));
});
return PromiseObject.create({promise});
}),
Вам нужно будет написать что-то подобное для каждого уровня или отказаться от некоторых асинхронных отношений. Проблема с вашим вычисленным свойством заключается в том, что boxes.@each.cartLines
не будет слушать все, что может изменить общую цену (например, изменение цены самого продукта). Так что он не будет отражать и обновлять все возможные изменения.
Я хотел бы отказаться от некоторых асинхронных отношений. Например запрос на /baskets/2
может загрузить все свои коробки, cartLines и, возможно, даже продукты. Если ваш API-интерфейс не поддерживает боковую загрузку, вы можете подделать его, загрузив все в маршруте (вам придется использовать второй пример - вам не разрешено получать доступ к ящикам, прежде чем они будут в магазине в случае async: false
). Это привело бы к гораздо более простым вычисляемым свойствам для расчета общей цены, а в случае дополнительной загрузки также уменьшило бы нагрузку на серверные и клиентские устройства.
// basket.js
const {computed} = Ember;
boxes: DS.hasMany('box', {async: false}),
price: computed('boxes.@each.price', function() {
return this.get('boxes').reduce(box => box.get('price'));
}),
Обновление и общее после мыслей
Я не думаю, что выполнение всех сумм в одной функции является жизнеспособным, выполнимым или вменяемым. Вы окажетесь в аду обратного вызова или в каком-то другом аду. Более того, это не будет узким местом в производительности.
Я сделал jsfiddle в основном более детализированную версию сниппета выше. Обратите внимание, что он будет правильно ждать и распространять цену, которая составляет два обещания, и также должна обновляться, когда что-то меняется (также я не проверял это).
Решение вашего вопроса хорошо объясняется в разделе Как вернуть обещание, состоящее из вложенных моделей в EmberJS, с EmberData? @Kingpin2k.
Вам нужно просто загрузить корзину и связанные с ней модели (box, cat-line и product), а не загружать все коробки, cartLines и продукты. Также для вычисления подытога нам нужно было бы разрешить все эти обещания зависимости заранее. Следуя решению, приведенному в посте, упомянутом ранее, ваше решение будет выглядеть следующим образом:
МОДЕЛЬ: app/models/cart-line.js
export default DS.Model.extend({
qty: DS.attr('number'),
box: DS.belongsTo('box'),
product: DS.belongsTo('product', { async: true })//assuming you are not side-loading these
});
МАРШРУТ: app /ways/basket.js
export default Ember.Route.extend({
model(params) {
return this.store.findRecord('basket', params.basket_id).then((basket)=> {
return basket.get('boxes').then((boxes)=> {
let cartLinesPromises = boxes.map(function (box) {
return box.get('cartLines');
});
return Ember.RSVP.allSettled(cartLinesPromises).then((array)=> {
let productPromises = array.map(function (item) {
return (item.value).get('product');
});
return Ember.RSVP.allSettled(productPromises);
});
});
});
}
});
КОНТРОЛЛЕР: app/controllers/basket.js
subTotal: computed('model.boxes.@each.cartLines', function () {
//you dont need to use DS.PromiseArray because the promises all have been already resolved in the route's model hook
let total = 0;
this.get('model.boxes').forEach((box)=> {
box.get('cartLines').forEach((cartLine)=> {
total += cartLine.get('product.price');
});
});
return total;
})
Наконец, по проблеме, которую вы имели здесь:
subTotal: computed('boxes.@each.cartLines', function() {
return DS.PromiseArray.create({
//"this" here is DS.PromiseArray object and not your controller instance
promise: this.get('boxes').then(boxes => {
return boxes.filter(i => i.get('cart-line'));
})
});
})
Вы не будете использовать вычисленную конструкцию, если будете следовать вышеупомянутому решению, а просто хотели бы указать решение в аналогичных условиях.
subTotal: computed('boxes.@each.cartLines', function() {
let controllerInstance = this;
return DS.PromiseArray.create({
promise: controllerInstance.get('boxes').then(boxes => {
return boxes.filter(i => i.get('cart-line'));
})
});
})