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

В этом выпуске основное внимание будет уделено моделям, поддерживаемым хранилищем RESTful с использованием ember-data. Во-первых, гигантское красное предупреждение: ember-data не готово к работе. Он находится в активной разработке и часто меняется. Есть ошибки и ограничения, и вам придется подождать, пока они будут исправлены. Мы в Embedly, в своем юношеском изобилии, решили проигнорировать эти проблемы и устремиться вперед в будущее! Если вы решите подписаться на нас, будьте в курсе последних изменений на BREAKING_CHANGES.md.

Как обычно, вы можете найти полный код демо-проекта на github. Мы не будем рассматривать здесь все изменения кода, а только те, которые непосредственно связаны с моделями ember и данными ember.

Если вы помните, в нашей последней части мы создали страницу команды. Мы использовали фикстуры для предоставления данных шаблону. Давайте переключим эти приборы на некоторые данные, предоставляемые RESTful API. Мы создали простой сервер API в node.js для подключения. Это всего лишь пример, и он делает минимум, чтобы заставить нас работать. Пожалуйста, не рассматривайте возможность использования его для чего-то отдаленно серьезного.

Добавление ember-данных

Мы используем Bower для управления зависимостями JavaScript. Существует оболочка ember-data, называемая ember-data-shim, созданная специально для управления зависимостями. Если вы используете его, убедитесь, что он относительно актуален, когда вы добавляете его в свой собственный проект. Вот наш новый компонент component.json.

// bower.json

    {
      "name": "demo",
      "version": "0.0.0",
      "dependencies": {
        "modernizr": "~2.6.2",
        "jquery": "~1.9.1",
        "handlebars": "1.0.0-rc.3",
        "ember": "1.0.0-rc.3",
        "ember-data-shim": "0.0.12"
      },
      "devDependencies": {}
    }

Запуск установки Bower установит зависимость в путь нашего приложения/компонентов. Теперь нам просто нужно добавить ember-data.js в наш app/index.html.

// app/index.html

    - snip -
    <script src="components/ember/ember.js"></script>
    <script src="components/ember-data-shim/ember-data.js"></script>
    - snip -

Если все прошло по плану, мы сможем запустить приложение, открыть его в браузере и проверить, доступен ли глобальный объект DS в консоли javascript браузера.

Настройка ember-данных

Чтобы заставить ember-data работать, нужно немного настроить. Мы добавим его в app/scripts/main.js. Первая часть сообщает ember-data, какую версию мы планируем использовать. Это важно, так как API имеет критические изменения между версиями. Мы не хотим случайно обновиться до того, как будем готовы. Второй раздел сообщает RESTAdapter базовый URL-адрес для нашего RESTful API.

// app/scripts/main.js

    - snip -
    App.Store = DS.Store.extend({
      revision: 12
    });

    DS.RESTAdapter.reopen({
      url: '/rest/1'
    });

    - snip -

Вы можете спросить себя, что такое RESTAdapter и какие еще существуют адаптеры? Ember-data поставляется вместе с RESTadapter и FixtureAdapter. FixtureAdapter предоставляет хранилище браузера в памяти и в основном полезен для тестирования и разработки.

FixtureAdapter был вариантом при написании этого поста, но я также хотел показать ожидаемые ответы и результаты вызовов API, чтобы, когда вы приступите к реализации собственной службы RESTful, вы знали, чего ожидать.

Определение нашей модели

Определить нашу модель очень просто. Сначала мы удалим все наши «фикстуры» из app/scripts/modules/about.js и заменим их определениями нашей модели в app/scripts/modules/models.js.

// app/scripts/modules/models.js

    App.Org = DS.Model.extend({
      name: DS.attr('string'),
      description: DS.attr('string'),
      members: DS.hasMany('App.Member')
    });

    App.Member = DS.Model.extend({
      name: DS.attr('string'),
      org: DS.belongsTo('App.Org')
    });

    // app/index.html

    - snip -
    <script src="scripts/modules/models.js"></script>
    - snip -

Здесь мы используем только тип атрибута string, но также доступны строка, число, логическое значение и дата. Также можно определить свои собственные типы атрибутов, но мы не будем здесь это рассматривать.

Мы также определяем отношение «многие к одному» между Member и Org. У нас действительно только одна организация, но кто знает, как наш сайт будет расти в будущем. Можно объявить отношения «многие ко многим», но давайте не будем усложнять.

Подключение нашего маршрута и контроллера

Давайте обновим страницу about, чтобы использовать нашу новую модель, заменив наш старый код прибора.

// app/scripts/main.js

    App = Em.Application.create({
      rootElement: $('#app'),
      orgId: 0
    });
    - snip -

    // app/scripts/modules/about.js

    - snip -
    App.AboutRoute = Em.Route.extend({
      model: function() {
        return App.Org.find(App.get('orgId'));
      }
    });

    // app/templates/about.handlebars

    - snip -
    {{render "team" controller.members}}

Мы настроили наш orgId в объекте приложения. Это хорошее место для размещения конфигурации всего сайта. Затем мы заменили нашу фикстуру вызовом App.Org.find() в нашей модели Route#. ember-data вернет пустой объект, а затем переключится в Org, когда он будет готов. Поскольку Ember.js такой замечательный, он будет повторно отображать соответствующие части представления при обновлении. Наконец, мы заменяем фикстуру участников в about.handlebars полем участников объекта Org, которое представляет собой массив членов организации.

Теперь наше приложение ведет себя точно так же, как и до внедрения ember-data, за исключением того, что оно поддерживается RESTful API. Довольно просто, а? Но это не очень интересно, нам придется добавить несколько CRUD-операций, чтобы по-настоящему продемонстрировать мощь ember-data. Давайте посмотрим, что происходит между сервером и клиентом, прежде чем мы перейдем к изменению данных.

Получение записей с помощью ember-data чрезвычайно просто. Следует иметь в виду, что это асинхронно. Обычно вы можете смело игнорировать этот факт из-за того, как работает система событий Ember, но иногда это может вас укусить, так что имейте это в виду. Мы получаем нашу организацию с помощью строки App.Org.find(App.get(‘orgId’)). Давайте посмотрим, что возвращает наш сервер.

// Request

  GET /rest/1/orgs/0

  // Response

  {
    "org": {
      "name": "jubarian.org",
      "description": "A non-profit organization of people...",
      "member_ids": [ 0, 1, 2, 3, 4, 5, 6 ]
    },
    "members": [{
      "org_id": 0,
      "name": "Nina",
      "id": 0
    }, {
      "org_id": 0,
      "name": "Kawan",
      "id": 1
  - snip -
    }]
  }

Есть несколько вещей, о которых нужно знать. Во-первых, мы видим, что наш API помещает полезную нагрузку объекта org в поле с именем org (единственное число). Здесь ember-data будет искать данные организации. Если бы мы сделали более общий запрос, возвращающий список, это были бы организации во множественном числе. Во-вторых, вместе с ответом мы также возвращаем данные всех участников. Это называется боковой загрузкой и является необязательным. В нашем случае, поскольку у нас есть роскошь очень маленького приложения с четко определенными требованиями, мы знаем, что это самый эффективный способ сделать что-то. Если бы мы не выполняли боковую загрузку, ember-data выполнит еще один вызов для извлечения данных, когда это необходимо.

Обратите внимание, что поле Members имеет множественное число, это список, а идентификаторы включены в объекты. Также обратите внимание на сериализацию полей данных. Ember ожидает, что все сериализованные имена полей будут разделены символом подчеркивания, даже если он ожидает верблюжьего регистра в моделях javascript. Кроме того, ожидается множественное число полей member_ids для отношения «один ко многим» и единственное число org_id для отношения «многие к одному».

Все эти поведения можно изменить. Ember-data легко настраивается, но если вы не имеете дело с устаревшим API, всегда лучше соответствовать ожиданиям фреймворков.

Протокол Ember Data

Давайте дадим краткий обзор того, что ember-data ожидает отправить и получить во время различных транзакций. Понимание этого протокола не только поможет вам реализовать собственный постоянный бэкенд, но и бесценно для отладки ошибок Ember-данных, которые часто являются результатом каких-то неожиданных данных. Многие из этих деталей можно изменить, настроив RESTAdapter, но опять же, если возможно, лучше придерживаться стандарта. Имейте в виду, что если у вас есть существующий API, который сильно отличается, возможно, стоит написать собственный адаптер. В этом посте мы не будем рассматривать пользовательские адаптеры.

Получить один

Вызов

find(:id)

HTTP-метод

ПОЛУЧИТЬ

URL-адрес

/‹множественное число›/:id

Пример запроса

ПОЛУЧИТЬ /орг/0

Пример ответа

{
  "org": {
    "name": "jubarian.org",
    "description": "A non-profit organization of people...",
    "member_ids": [ 0, 1, 2, 3, 4, 5, 6 ]
  }
}

ПОЛУЧИТЕ много

Вызов

find({:attr: :value}

HTTP-метод

ПОЛУЧИТЬ

URL-адрес

/‹множественное число›?:attr=:value

Пример запроса

ПОЛУЧИТЬ /orgs?name=jubarian.org

Пример ответа

{
  "orgs": [
    {
      "id": 0,
      "name": "jubarian.org",
      "description": "A non-profit organization of people...",
      "member_ids": [ 0, 1, 2, 3, 4, 5, 6 ]
    }
  ]
}

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

ПОЧТА

Вызов

var record = Model.createRecord({...}); record.get('store').commit();

HTTP-метод

ПОЧТА

URL-адрес

/‹множественное число›

Пример запроса

ПОСТ /члены

Пример тела

{"member":{"name":"Billy","org_id":0}}

Пример ответа

{"member":{"name":"Billy","org_id":0, "id": 7}}

СТАВИТЬ

Вызов

var record = Model.find(:id); record.set(:name, :value); record.get('store').commit();

HTTP-метод

СТАВИТЬ

URL-адрес

/‹множественное число›/:id

Пример запроса

ПУТ /члены/7

Пример тела

{"member":{"name":"William","org_id":0}}

Пример ответа

{"member":{"name":"William","org_id":0}}

Следует отметить, что DELETE ДОЛЖЕН возвращать действительный json, иначе вы получите сообщение об ошибке. Я считаю это ошибкой, так что, надеюсь, это изменится в будущем.

УДАЛИТЬ

Вызов

var record = Model.find(:id); record.deleteRecord(); record.get('store').commit();

HTTP-метод

УДАЛИТЬ

URL-адрес

/‹множественное число›/:id

Пример запроса

УДАЛИТЬ /члены/7

Пример ответа

{}

Создание страницы администратора команды

Далее, давайте соберем все вместе и создадим раздел управления участниками для нашего сайта. Сначала настроим маршруты. У нас будет три страницы; индекс, форма нового члена и форма редактирования члена.

// app/scripts/main.js

    - snip -
    App.Router.map(function(){
      this.route('about');
      this.resource('members', function() {
        this.route('new');
        this.route('edit', {path: '/edit/:id'});
      });
    });
    - snip -

Теперь давайте создадим маршруты и контроллеры.

// app/scripts/modules/members.js

    App.MembersIndexRoute = Em.Route.extend({
      setupController: function(controller, model) {
        App.Org.find(App.get('orgId')).then(function(org) {
          controller.set('content', org.get('members'))
        });
        controller.set('content', Em.A());
      }
    })

    App.MembersEditRoute = Em.Route.extend({
      serialize: function(obj) {
        return {id: obj.get('id')};
      },
      model: function(params) {
        return App.Member.find(params.id);
      }
    })

    App.MembersNewRoute = Em.Route.extend({
      model: function(params) {
        return Em.Object.create({org: App.Org.find(App.get('orgId'))});
      }
    })

    /*
     * Wrap member model objects in simple interface callable
     * by the view.
     */
    App.MemberController = Em.ObjectController.extend({
      "delete": function() {
        this.get('content').deleteRecord();
        this.get('store').commit();
      },
      edit: function() {
        this.transitionToRoute("members.edit", this);
      },
      save: function() {
        this.get('store').commit();
        this.transitionToRoute("members.index");
      },
      cancel: function() {
        this.get('store.defaultTransaction').rollback();
        this.transitionToRoute("members.index");
      },
      create: function() {
        this.set('content', App.Member.createRecord(this.get('content')));
        this.save();
      },
      cancelNew: function() {
        this.transitionToRoute("members.index");
      }
    })

    App.MembersIndexController = Em.ArrayController.extend({
      itemController: "member"
    })

    App.MembersNewController = App.MemberController.extend({});
    App.MembersEditController = App.MemberController.extend({});

    // app/index.html

    - snip -
    <script src="scripts/modules/members.js"></script>
    - snip -

Если вы заметили, в MembersIndexController мы отказываемся от обратного вызова модели в пользу обратного вызова setupController. Это потому, что мы передаем в поле результат асинхронной операции. MembersIndexController сразу же нуждается в массиве, чтобы настроить себя. Если бы мы вызвали что-то вроде Members.find({org_id: 0}), то ember-data сразу же вернул бы пустой массив, и все было бы в порядке, но я хотел продемонстрировать ситуацию, когда асинхронный характер ember-data может просачиваться, поэтому важно знать об этом. Как только вызов вернется, мы можем безопасно подкачать результат в поле содержимого контроллера, и все будет обновлено, как и ожидалось.

Мы также представили новую сериализацию обратного вызова контроллера в файле MembersEditController. Если у вашего маршрута есть параметры, важно предоставить способ сопоставления объекта, который он представляет, с параметрами URL. Таким образом, когда мы переходим к маршруту и ​​передаем ему объект напрямую, ember может сформировать из него URL-адрес, на который можно ссылаться. Наш метод сериализации сопоставляет атрибут id модели с параметром id в URL-адресе редактирования.

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

Мы использовали наследование, чтобы передать всю функциональность MemberController в MembersEditController и MembersNewController. Поскольку мы не добавили никакой логики представления, мы используем созданные по умолчанию представления. Давайте закончим, добавив шаблоны для наших страниц управления участниками и ссылку со страницы о нас.

// app/templates/about.handlebars

    - snip -
    <p>
      <a href="#" {{action toggleTeam}}>{{toggleTeamLabel}}</a>
      {{#linkTo members.index}}manage team{{/linkTo}}
    </p>
    - snip -

    // app/templates/members.handlebars

    <p>{{#linkTo "about"}}about{{/linkTo}}</p>
    <p>{{outlet}}</p>

    // app/templates/members/index.handlebars

    <p>List Members</p>
    <ul>
    {{#each controller}}
      <li>
        {{name}} <a href="#" {{action delete}}>delete</a> <a href="#" {{action edit}}>edit</a>
      </li>
    {{/each}}
    </ul>
    {{#linkTo "members.new"}}add member{{/linkTo}}

    // app/templates/members/edit.handlebars

    <p>Edit Member</p>
    <p><label>
      Name
      {{view Em.TextField valueBinding="name"}}
    </label></p>
    <p>
    <a href="#" {{action save}}>save</a>
    <a href="#" {{action cancel}}>cancel</a>
    </p>

    // app/templates/members/new.handlebars

    <p>Create Member</p>
    <p><label>
      Name
      {{view Em.TextField valueBinding="name"}}
    </label></p>
    <p>
    <a href="#" {{action create}}>save</a>
    <a href="#" {{action cancelNew}}>cancel</a>
    </p>

Безопасность

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

Обработка ошибок

Обработка ошибок в ember-data оставляет желать лучшего, и именно здесь тот факт, что это бета-проект, действительно очевиден. Можно создать некоторую обработку ошибок с помощью обратных вызовов и состояний объектов модели, но это сложно и постоянно меняется. Трудно даже найти документацию по соответствующим обратным вызовам и состояниям. Обычно это связано с чтением базы кода ember-data. Мы решили пропустить это в этой статье. Это не имело бы большой длительной ценности, поскольку мы ожидаем, что оно созреет довольно быстро.

Вывод

Я надеюсь, что эта статья и эта серия смогли дать вам хорошее представление об ember и ember-data на высоком уровне и быстро направить вас на правильный путь. Даже если вы решите не использовать ember-data и использовать свой собственный, я надеюсь, что эта статья дала вам некоторые идеи о том, как его создать. На этом мы завершаем серию статей об Ember в Embedly. А теперь иди и создай что-нибудь потрясающее!