Введение
В этом разделе руководства описаны основные возможности Ember Data, мощного набора инструментов для форматирования запросов, нормализации ответов и эффективного управления локальным кешем данных.
Сам Ember.js работает с любым типом бэкенда: REST, JSON:API, GraphQL или чем-либо еще. Чтобы узнать о других способах обработки данных и найти расширения, ознакомьтесь с руководством по созданию запросов к API, найдите плагины на Ember Observer и найдите руководства сообщества.
Что такое модели Ember Data?
В Ember Data модели — это объекты, представляющие базовые данные, которые ваше приложение отображает пользователю. Обратите внимание, что модели Ember Data — это другой концептуальный объект, чем метод model в маршрутах, хотя у них одинаковое название.
Разные приложения могут иметь очень разные модели, в зависимости от решаемых задач. Например, приложение для обмена фотографиями может иметь модель Photo для представления конкретной фотографии и модель PhotoAlbum для представления группы фотографий. В отличие от этого, приложение для онлайн-шопинга, вероятно, будет иметь разные модели, такие как ShoppingCart, Invoice или LineItem.
Модели, как правило, являются постоянными. Это означает, что пользователь не ожидает потери данных модели при закрытии окна браузера. Чтобы убедиться, что данные не потеряны, если пользователь внес изменения в модель, необходимо сохранить данные модели в месте, где они не будут потеряны.
Обычно большинство моделей загружаются и сохраняются на сервер, который использует базу данных для хранения данных. Обычно вы будете отправлять JSON-представления моделей туда и обратно на HTTP-сервер, который вы написали. Однако Ember упрощает использование других долговременных хранилищ, таких как сохранение на жесткий диск пользователя с помощью IndexedDB или хранилищ размещения, которые позволяют избежать написания и размещения собственных серверов.
После загрузки моделей из хранилища компоненты знают, как преобразовать данные модели в пользовательский интерфейс, с которым пользователь может взаимодействовать. Дополнительную информацию о том, как компоненты получают данные модели, см. в руководстве Определение модели маршрута.
Использование Ember Data с самого начала может отличаться от того, к чему вы привыкли при написании JavaScript-приложений. Многие разработчики знакомы с использованием Ajax для получения необработанных JSON-данных с конечной точки, что может показаться простым на первый взгляд. Однако со временем сложность просачивается в код приложения, что затрудняет его поддержку.
С Ember Data управление моделями по мере роста приложения становится проще и легче.
После понимания Ember Data у вас появится гораздо лучший способ управления сложностью загрузки данных в вашем приложении. Это позволит вашему коду эволюционировать и расти с лучшей поддерживаемостью.
Гибкость Ember Data
Благодаря использованию паттерна адаптера, Ember Data может быть настроен для работы со многими различными бэкендами. Существует целая экосистема адаптеров и несколько встроенных адаптеров, которые позволяют вашему приложению Ember общаться с различными серверами.
По умолчанию Ember Data предназначен для работы «из коробки» с JSON:API. JSON:API — это формальное описание для создания стандартных, надежных и производительных API, которые позволяют клиентам и серверам обмениваться данными модели.
JSON:API стандартизирует, как JavaScript-приложения общаются с серверами, что снижает зависимость между front-end и back-end и предоставляет больше свободы для изменения отдельных частей вашего стека.
Если вам нужно интегрировать ваше приложение Ember.js с сервером, для которого недоступен адаптер (например, вы создали API-сервер, который не соответствует какому-либо JSON-спецификации), Ember Data предназначен для настройки, чтобы работать с любыми данными, которые возвращает ваш сервер.
Ember Data также предназначен для работы с потоковыми серверами, такими как серверы на основе WebSocket. Вы можете открыть сокет на вашем сервере и передавать изменения в Ember Data всякий раз, когда они происходят, обеспечивая вашему приложению интерфейс пользователя в реальном времени, который всегда является актуальным.
Хранилище и единый источник истины
Один из распространенных способов создания веб-приложений — это тесная связь элементов пользовательского интерфейса с загрузкой данных. Например, представьте, что вы пишете админскую часть приложения блоггинга, которое имеет функцию, отображающую черновики для текущего пользователя.
Вы можете захотеть сделать компонент ответственным за получение этих данных и их хранение:
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import fetch from 'fetch';
export default class ListOfDraftsComponent extends Component {
@tracked drafts;
constructor() {
super(...arguments);
fetch('/drafts').then(data => {
this.drafts = data;
});
}
}
Затем вы можете отобразить список черновиков в шаблоне вашего компонента следующим образом:
<ul>
{{#each this.drafts key="id" as |draft|}}
<li>{{draft.title}}</li>
{{/each}}
</ul>
Это отлично работает для компонента list-of-drafts. Однако ваше приложение, вероятно, состоит из многих разных компонентов. На другой странице вы, возможно, захотите компонент для отображения количества черновиков. Вы можете захотеть скопировать и вставить существующий код willRender в новый компонент.
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import fetch from 'fetch';
export default class DraftsButtonComponent extends Component {
@tracked drafts;
constructor() {
super(...arguments);
fetch('/drafts').then(data => {
this.drafts = data;
});
}
}
<LinkTo @route="drafts">
Drafts ({{this.drafts.length}})
</LinkTo>
К сожалению, приложение теперь сделает два отдельных запроса за одну и ту же информацию. Не только избыточная загрузка данных дорого обходится с точки зрения затрат на пропускную способность и влияет на воспринимаемую скорость вашего приложения, но также легко, чтобы два значения разошлись. Вы, вероятно, сами пользовались веб-приложением, в котором список элементов расходится со счетчиком в строке меню, что приводит к раздражающему и несогласованному опыту.
Существует также тесная связь между пользовательским интерфейсом вашего приложения и кодом сети. Если URL или формат JSON-загрузки изменятся, это, вероятно, приведет к сбою всех компонентов пользовательского интерфейса способами, которые трудно отследить.
Принципы SOLID хорошего дизайна говорят нам о том, что объекты должны иметь одну ответственность. Ответственность компонента — представление данных модели пользователю, а не загрузка модели.
Хорошие приложения Ember используют другой подход. Ember Data предоставляет вам единое хранилище, которое является центральным хранилищем моделей в вашем приложении. Маршруты и соответствующие контроллеры могут запрашивать модели в хранилище, и хранилище отвечает за знание того, как их получить.
Это также означает, что хранилище может обнаруживать, что два разных компонента запрашивают одну и ту же модель, позволяя вашему приложению загружать данные с сервера только один раз. Можно представить себе хранилище как кэш чтения для моделей вашего приложения. И маршруты, и соответствующие контроллеры имеют доступ к этому общему хранилищу; когда им нужно отобразить или изменить модель, они сначала запрашивают ее у хранилища.
Модели
В Ember Data каждая модель представлена подклассом Model, который определяет атрибуты, отношения и поведение данных, которые вы представляете пользователю.
Модели определяют тип данных, которые будут предоставлены вашим сервером. Например, модель Person может иметь атрибут name, который является строкой, и атрибут birthday, который является датой:
import Model, { attr } from '@ember-data/model';
export default class PersonModel extends Model {
@attr('string') name;
@attr('date') birthday;
}
Модель также описывает ее отношения с другими объектами. Например, order может иметь много line-items, а line-item может принадлежать конкретному order.
import Model, { hasMany } from '@ember-data/model';
export default class OrderModel extends Model {
@hasMany('line-item') lineItems;
}
import Model, { belongsTo } from '@ember-data/model';
export default class LineItemModel extends Model {
@belongsTo('order') order;
}
Модели сами по себе не содержат данных; они определяют атрибуты, отношения и поведение конкретных экземпляров, которые называются записями.
Записи
Запись — это экземпляр модели, содержащий данные, загруженные с сервера. Ваше приложение также может создавать новые записи и сохранять их обратно на сервер.
Запись уникально идентифицируется типом и ID модели.
Например, если вы пишете приложение для управления контактами, у вас может быть модель Person. Отдельная запись в вашем приложении может иметь тип person и ID 1 или steve-buscemi.
this.store.findRecord('person', 1); // => { id: 1, name: 'steve-buscemi' }
ID обычно назначается записи сервером при первом сохранении, но вы также можете генерировать ID на стороне клиента.
Адаптер
Адаптер — это объект, который преобразует запросы от Ember (например, «найдите пользователя с ID 1») в запросы к серверу.
Например, если ваше приложение запрашивает Person с ID 1, как Ember должен загрузить его? Через HTTP или WebSocket? Если это HTTP, URL /person/1 или /resources/people/1?
Адаптер отвечает на все эти вопросы. Всякий раз, когда ваше приложение запрашивает у хранилища запись, которой нет в кэше, оно обращается к адаптеру за ней. Если вы изменяете запись и сохраняете ее, хранилище передаст запись адаптеру, чтобы он отправил соответствующие данные на ваш сервер и подтвердил успешность сохранения.
Адаптеры позволяют полностью изменить реализацию вашего API без влияния на код вашего приложения Ember.
Кэширование
Хранилище автоматически кэширует записи для вас. Если запись уже была загружена, повторный запрос всегда вернёт тот же экземпляр объекта. Это минимизирует количество обращений к серверу и позволяет вашему приложению отображать пользовательский интерфейс как можно быстрее.
Например, в первый раз, когда ваше приложение запрашивает у хранилища запись person с идентификатором 1, оно получит эту информацию с вашего сервера.
Однако, при следующем запросе приложения к хранилищу записи person с идентификатором 1, хранилище обнаружит, что уже загрузило и кэшировало эту информацию с сервера. Вместо отправки нового запроса за той же информацией, оно предоставит вашему приложению ту же запись, что и в первый раз. Эта функция — всегда возвращать один и тот же объект записи, независимо от того, сколько раз вы её запрашиваете — иногда называется картой идентичности.
Использование карты идентичности важно, потому что это гарантирует, что изменения, внесённые в одной части пользовательского интерфейса, распространяются на другие части. Это также означает, что вам не нужно вручную синхронизировать записи — вы можете запросить запись по идентификатору и не беспокоиться о том, запросили ли другие части приложения и загрузили ли её.
Один из недостатков возвращения кэшированной записи заключается в том, что состояние данных может измениться с момента её первоначальной загрузки в карту идентичности хранилища. Для того, чтобы предотвратить проблему с устаревшими данными, Ember Data автоматически выполняет запрос в фоновом режиме каждый раз, когда кэшированная запись возвращается из хранилища. Когда новые данные поступают, запись обновляется, и если с момента первоначального отображения произошли изменения в записи, шаблон перерисовывается с новой информацией.
Обзор архитектуры
В первый раз, когда ваше приложение запрашивает запись у хранилища, хранилище видит, что у него нет локальной копии, и запрашивает её у адаптера. Ваш адаптер пойдёт и получит запись из вашего слоя персистентности; как правило, это будет JSON-представление записи, предоставленное HTTP-сервером.

Как показано на диаграмме выше, адаптер не всегда может вернуть запрошенную запись сразу. В этом случае адаптер должен выполнить асинхронный запрос к серверу, и только после завершения этого запроса запись может быть создана со своими данными поддержки.
Из-за этой асинхронности хранилище сразу возвращает обещание из метода findRecord(). Аналогично, любые запросы, выполняемые хранилищем к адаптеру, также возвращают обещания.
После того, как запрос к серверу возвращает JSON-данные для запрошенной записи, адаптер выполняет разрешение обещания, возвращенного хранилищу, с JSON.
Затем хранилище использует этот JSON, инициализирует запись данными JSON и выполняет разрешение обещания, возвращённого вашему приложению, с загруженной записью.

Давайте посмотрим, что произойдёт, если вы запросите запись, которая уже есть в кэше хранилища.

В этом случае, поскольку хранилище уже знало о записи, оно возвращает обещание, которое разрешается с записью немедленно. Ему не нужно просить адаптер (и, следовательно, сервер) о копии, так как он уже имеет её в локальном хранилище.
Модели, записи, адаптеры и хранилище — это основные понятия, которые вы должны понять, чтобы максимально эффективно использовать Ember Data. В следующих разделах более подробно рассматриваются каждое из этих понятий и то, как их использовать вместе.
© 2022 Yehuda Katz, Tom Dale and Ember.js contributors
Licensed under the MIT License.
https://guides.emberjs.com/v3.28.0/models