En En

Модели


Модели — это объекты. Они представляют базовые данные, которые ваше приложение показывает пользователю. Разные приложения имеют различные модели в зависимости от того, какие задачи они решают.

Например, приложение для обмена фотографиями должно иметь модель Photo, чтобы показывать определенное фото, и PhotoAlbum, чтобы предоставлять группу фотографий. В то же время приложение для оналайн-покупок будет иметь другие модели, например hoppingCart, Invoice или LineItem.

Обычно модели неизменные. То есть пользователь ожидает, что данные модели не пропадут после того, как он закроет окно браузера. Чтобы данные не исчезали, когда пользователь вносит изменения в модель, нужно хранить их в надежном месте. Большинство моделей загружаются с сервера и хранятся на нем. Для хранения информации сервер использует базу данных.

Обычно вы обмениваетесь JSON-представлениями моделей с HTTP-сервером, который прописали. Но в Ember можно с легкостью использовать и другие долговременные хранилища, например жесткий диск с IndexedDB или удаленные хранилища, которые позволяют не прописывать и не запускать собственные серверы.

После загрузки моделей из хранилища компоненты понимают, как перевести данные модели в интерфейс, с которым пользователь может взаимодействовать. Чтобы подробнее узнать о том, как компоненты получают данные модели, смотрите главу «Спецификация модели маршрута».

Ember Data подключается по умолчанию при создании нового приложения. Это библиотека, которая тесно интегрирована с Ember и упрощает возвращение моделей с сервера в формате JSON, сохранение обновлений на сервере и создание новых моделей в браузере.

В Ember Data используются адаптеры. Благодаря этому ее можно настраивать для работы с разными видами backend'ов. Существует целая экосистема адаптеров, которая позволяет вашему приложению Ember общаться с разными типами серверов без написания какого-либо сетевого кода.

Если вам нужно интегрировать приложение Ember.js с сервером, для которого нет доступного адаптера (например, вы запустили сервер приложений, который не придерживается каких-либо спецификаций JSON), Ember Data можно настроить для работы с любыми данными, которые возвращает сервер.

Ember Data также позволяет работать с потоковыми серверами, например WebSocket. Вы можете открыть двунаправленный канал для своего сервера и вносить изменения в Ember Data при их возникновении. Так вы обеспечиваете обновление пользовательского интерфейса приложения в реальном времени.

Сначала может показаться, что использование Ember Data отличается от привычного написания JavaScript-приложений. Многие разработчики используют AJAX для извлечения исходных данных в формате JSON из конечной точки, что сначала дается легко. Но спустя время код приложения усложняется, и его становится трудно поддерживать.

Ember Data упрощает управление моделями по мере разработки приложения.

Когда вы поймете, как использовать Ember Data, вы легко сможете управляться с загрузкой данных в своем приложении. Это позволит развивать код и не запутаться в нем.

Хранилище и единственный источник истины

Распространенный способ создания веб-приложений заключается в тесном связывании элементов пользовательского интерфейса с извлечением данных. Например, вы пишите раздел администратора в приложении для ведения блогов. В нем есть функция, которая выводит список черновиков только что вошедшего в систему пользователя.

Скорее всего, вы захотите сделать компонент, отвечающий за извлечение и хранение этих данных:

app/components/list-of-drafts.js

import Ember from 'ember';

export default Ember.Component.extend({
  willRender() {
    $.getJSON('/drafts').then(data => {
      this.set('drafts', data);
    });
  }
});

Тогда вы сможете показывать список черновиков в шаблоне компонента таким образом:

app/templates/components/list-of-drafts.hbs

<ul>
  {{#each drafts key="id" as |draft|}}
    <li>{{draft.title}}</li>
  {{/each}}
</ul>

Компонент list-of-drafts будет работать отлично. Но ваше приложение, вероятно, состоит из множества разных компонентов. На другой странице вы, возможно, захотите разместить компонент для отображения количества черновиков. Вы захотите скопировать и вставить существующий код willRender в новый компонент.

app/components/drafts-button.js

import Ember from 'ember';

export default Ember.Component.extend({
  willRender() {
    $.getJSON('/drafts').then(data => {
      this.set('drafts', data);
    });
  }
});

app/templates/components/drafts-button.hbs

{{#link-to 'drafts' tagName="button"}}
Drafts ({{drafts.length}})
{{/link-to}}

К сожалению, теперь приложение будет выполнять два отдельных запроса для одной и той же информации. Излишнее извлечение данных не только затратно в плане напрасно потерянной пропускной способности и влияния на скорость работы приложения; два значения могут быть просто не синхронизированы. Вы сами, скорее всего, использовали веб-приложение, где список элементов не синхронизирован со счетчиком на панели инструментов. И это негативно влияет на опыт использования.

Существует также тесная связь между пользовательским интерфейсом вашего приложения и сетевым кодом. Если url или полезная нагрузка в JSON меняется, это нарушает работу всех компонентов интерфейса таким образом, что проблему сложно отследить.

Принципы хорошего дизайна SOLID указывают нам, что объекты должны иметь одну обязанность. Обязанность компонента заключается в представлении данных модели пользователю, а не в извлечении модели.

В хороших приложениях Ember используется другой подход. Ember Data предоставляет вам одно хранилище, которое служит основным репозиторием моделей в вашем приложении. Компоненты и маршруты могут запрашивать у хранилища модели, а хранилище уже должно знать, как их извлекать.

Хранилище также может определить, что два разных компонента запрашивают одну и ту же модель. Это позволяет вашему приложению извлекать данные с сервера только один раз. Вы можете рассматривать хранилище как кэш со сквозным чтением для моделей приложения. Ваши компоненты и маршруты имеют доступ к этому общему хранилищу; когда им нужно отобразить или изменить модель, они сначала запрашивают ее у хранилища.

Программирование по согласованию с JSON API

Вы можете значительно сократить количество кода, который необходимо писать и поддерживать, с помощью соглашений Ember. Если разработчики в вашей команде придерживаются этих соглашений, код будет проще понять и поддерживать.

Вместо создания произвольного набора соглашений в Ember Data изначально предусмотрена поддержка JSON API. JSON API — формальная спецификация для создания общепринятых, надежных и эффективных API, которые позволяют клиентам и серверам обмениваться данными модели.

JSON API стандартизирует общение приложений JavaScript с серверами, поэтому вы ослабляете связь между frontend'ом и backend'ом и получаете больше свободы в плане изменения частей вашего стека.

Можно провести аналогию: JSON API для приложений JavaScript и серверов приложений представляет собой то же, что SQL для фреймворков на стороне сервера и баз данных. Такие популярные фреймворки, как Ruby on Rails, Laravel, Django, Spring и пр., изначально поддерживают многие базы данных, например PostgreSQL, SQL Server и т. д.

Для фреймворков (или приложений, созданных с их помощью) не нужно писать много отдельного кода, чтобы добавить поддержку новой базы данных; если база данных поддерживает SQL, довольно просто добавить ее поддержку.

Это справедливо и для JSON API. При использовании JSON API для взаимодействия между приложением Ember и сервером вы можете полностью изменить стек backend'а без нарушения работы frontend'а. И если вы добавляете приложения для других платформ, например iOS и Android, вы сможете применить библиотеки JSON API для них, чтобы с легкостью использовать тот же API, что и ваше приложение Ember.

Модели

В Ember Data каждую модель представляет подкласс Model. Он определяет атрибуты, связи и поведение данных, которые вы предоставляете пользователю.

Модели определяют тип данных, которые предоставит ваш сервер. Например, модель Person может иметь атрибут firstName с типом string и атрибут birthday с типом date:

app/models/person.js

import DS from 'ember-data';

export default DS.Model.extend({
  firstName: DS.attr('string'),
  birthday:  DS.attr('date')
});

Модель также описывает свои связи с другими объектами. Например, order может иметь много line-items, а line-item может принадлежать конкретному order.

app/models/order.js

import DS from 'ember-data';
export default DS.Model.extend({
  lineItems: DS.hasMany('line-item')
});

app/models/line-item.js

import DS from 'ember-data';

export default DS.Model.extend({
  order: DS.belongsTo('order')
});

Сами модели не имеют данных. Они определяют атрибуты, связи и поведение особых экземпляров, которые называют записями.

Записи

Запись — это экземпляр модели, который содержит данные, загруженные с сервера. Ваше приложение может также создавать новые записи и сохранять их на сервере.

Тип модели и идентификатор однозначно определяют запись.

Например, если вы писали приложение для управления контактами, у вас могла быть модель Person. Индивидуальная запись в вашем приложении могла иметь тип person и идентификатор 1 или steve-buscemi.

this.get('store').findRecord('person', 1); // => { id: 1, name: 'steve-buscemi' }

Обычно сервер назначает записи идентификатор, когда вы сохраняете ее в первый раз. Но вы также можете генерировать идентификаторы на стороне клиента.

Адаптер

Адаптер — это объект, который переводит запросы от Ember (например, «найти пользователя с идентификатором 1») в запросы для сервера.

Например, если ваше приложение запрашивает Person с идентификатором 1, как Ember должен загрузить ее? Через HTTP или WebSocket? Если через HTTP, URL будет /person/1 или /resources/people/1?

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

Адаптеры позволяют вам полностью менять реализацию API без воздействия на код приложения Ember.

Кэширование

Хранилище будет автоматически кэшировать записи. Если запись уже была загружена, при ее запросе во второй раз всегда будет возвращен тот же экземпляр объекта. Это минимизирует количество обходов сервера и позволяет вашему приложению отображать интерфейс для пользователя с максимальной скоростью.

Например, в первый раз ваше приложение запрашивает у хранилища запись person с идентификатором 1. Оно извлечет эту информацию с сервера.

Но в следующий раз, когда ваше приложение запросит запись person с идентификатором 1, хранилище учтет, что оно уже возвращало эту информацию с сервера и поместило ее в кэш. Вместо отправки еще одного запроса для той же информации, оно выдаст вашему приложению ту же запись, которую предоставило в первый раз. Эту особенность — всегда возвращать тот же объект записи, независимо от количества его запросов — иногда называют коллекцией объектов.

Использовать коллекцию объектов важно, так как это гарантирует, что изменения, внесенные вами в одной части интерфейса распространятся в других его частях. Это также значит, что вам не придется вручную синхронизировать записи. Вы можете запросить запись по идентификатору и не беспокоиться о том, что другие части вашего приложения уже запрашивали ее и загрузили.

Недостаток возвращения кэшированной записи заключается в том, что состояние данных могло измениться с того момента, когда вы в первый раз загрузили запись в коллекцию объектов хранилища. Чтобы устаревшие данные не создавали проблем, Ember Data будет автоматически выполнять запрос в фоновом режиме каждый раз, когда кэшированная запись будет возвращаться из хранилища. При появлении новых данных запись обновляется; и если запись претерпела изменения, шаблон отображается заново с новой информацией.

Обзор архитектуры

Когда ваше приложение в первый раз запрашивает у хранилища запись, оно ищет локальную копию, и если не находит ее, то запрашивает запись у адаптера. Ваш адаптер вернет запись со слоя хранения данных; обычно это JSON-представление записи, поступающее с HTTP-сервера.

finding unloaded record step1 diagram

На диаграмме выше проиллюстрировано, что адаптер не может всегда возвращать запрашиваемую запись сразу. В этом случае адаптер должен выполнить асинхронный запрос к серверу, и только когда запрос окончит загрузку, запись можно будет создать с его резервными данными.

Из-за этой асинхронности хранилище сразу возвращает обещание из метода find(). Похожим образом любые запросы, которые хранилище выполняет к адаптеру также возвращают обещания.

Когда запрос к серверу возвращается с полезной нагрузкой в формате JSON для запрошенной записи, адаптер разрешает обещание, которое вернул хранилищу, с JSON.

Затем хранилище берет этот JSON, инициализирует запись с данными JSON и разрешает обещание, возвращенное приложению, с только что загруженной записью.

finding unloaded record step2

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

finding loaded record diagram

В этом случае, так как хранилище уже знает о записи, оно возвращает обещание, которое сразу разрешает с записью. Нет необходимости запрашивать у адаптера (и следовательно у сервера) копию, так как она уже сохранена локально.

Модели, записи, адаптеры и хранилище — это ключевые концепции, которые следует понимать, чтобы получить максимальную пользу от Ember Data. В следующих главах подробнее рассматривается каждая концепция и их совместное использование.


Комментарии (0)

    Выделите опечатку и нажмите Ctrl + Enter, чтобы отправить сообщение об ошибке.