Разработка приложения на Ember.js - Model-View-Controller

Продолжим писать приложение на Ember.js. В прошлый раз мы сгенерировали каркас приложения. Сегодня рассмотрим основы типичного ember-приложения.

Сначала немного теории. Ember придерживается принципа MVC - это значит что в приложении есть модели, контроллеры и слой представления (в ember это handlebars шаблоны). Это достаточно распространненый сегодня паттерн. Но в ember есть важные отличия, которые нужно сразу понять.

В приложении есть объект Router, который сопоставляет урлу соответствующий ему шаблон или набор вложенных шаблонов. Как в прошлый раз мы просто добавили роут /hello и шаблон hello.hbs. В результате при переходе на этот урл мы видели то, что написали в шаблоне. А где же контроллер, спросите вы? Контроллеры в приложении отвечают за состояния и действия. Если нужно выполнить какое-нибудь действие при нажатии на кнопку, то этот код должен быть в контроллере. Так как наше приложение состояний не меняет, то и контроллеров мы не делали. Хотя ember неявно создал его за нас, это видно по логам в консоли браузера:

generated -> controller:hello  

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

Теперь попрактикуемся. Давайте сделаем так, чтобы в нашем приложении можно было просматривать список постов и каждый пост в отдельности.

Начнем с модели. Создадим модель Post. Для этого, находясь в папке приложения, выполните команду:

ember generate model post  

Как видите ember-cli сгенерировал для нас два файла - post.js и post-test.js. Тестов пока касаться не будем, откроем файл post.js. Там мы увидим следующее:

import DS from 'ember-data';

export default DS.Model.extend({

});

Здесь мы видим довольно необычный синтаксис с импортом и экспортом. Если ES6 modules вам ни о чем не говорит, то почитайте статью http://frontender.info/es6-modules/, там все подробно рассказано. Благодаря ember-cli, который использует ES6 module transpiler для того чтобы преобразовать ES6 модули в require.js мы можем писать в стиле ES6 уже сейчас, даже когда браузеры еще их не поддерживают.

В качестве базового класса для наших моделей мы будем использовать DS.Model из пакета Ember Data. Давайте опишем поля:

export default DS.Model.extend({  
  title: DS.attr(),
  body: DS.attr()  
});

Давайте теперь добавим в приложение урл /posts по которому будем показывать список постов.

Для этого в файле router.js добавим строчку:

this.route('posts');  

И выполним такую команду:

ember g route posts  

Здесь мы воспользовались псевдонимом команды generate и сгенерировали файлы для роута и шаблона.

По этому урлу уже можно зайти, но там пока ничего нет. Давайте обновим шаблон posts.hbs:

<h3>List of posts</h3>

{{#each}}
    <h5>{{title}}</h5>
    <p>{{body}}</p>
{{/each}}

Чтобы этот шаблон заработал в него нужно передать список постов. Как упоминалось выше, это нужно делать в роуте. Давайте изменим роут в файле posts.js в папке routes:

export default Ember.Route.extend({  
  model: function() {
    return this.store.find('post');
  }
});

Здесь мы обращаемся к хранилищу данных с запросом найти все записи для модели post.

Теперь перед нами встала проблема, по дефолту Ember Data использует REST-адаптер для загрузки данных, но мы еще не делали backend с которого можно было бы загружать данные. Эту проблему мы можем обойти изменив текущий адаптер на FixtureAdapter. Этот адаптер предназначен для использования в тестах, но и нам сейчас пригодится. Для этого используем ember cli:

ember g adapter application  

Он сгенерирует файл app/adapters/application.js в котором мы и укажем нужный нам адаптер:

export default DS.Fixturedapter.extend({});  

И немного изменим файл с моделью Post, чтобы адаптер мог найти данные:

import DS from 'ember-data';

var Post = DS.Model.extend({  
  title: DS.attr(),
  body: DS.attr()  
});

Post.reopenClass({  
  FIXTURES: [{
    id: 1,
    title: 'Post 1',
    body: 'Lectus in pellentesque lorem...'
  }, {
    id: 2,
    title: 'Post 2',
    body: 'Porttitor facilisis ac...'
  }]
});

export default Post;  

Теперь если открыть страницу /posts, то мы увидим список постов. Дальше нам нужна возможность открывать страницу конкретного поста. Для этого изменим код в router.js:

Router.map(function() {  
  this.resource('posts', function () {
      this.route('post', { path: '/:post_id' });
  });
});

Роут posts превратился в ресурс, а в ресурс мы добавили роут post. Ресурс это просто набор роутов. Роуты объявленные внутри ресурса являются вложенными. Таким образом урл конкретного поста будет таким /posts/:post_id.

Теперь сгенерируем шаблон для поста:

ember g route posts/post  

Как видите ember cli создал папку posts и в нее добавил роут post.js. Теперь перенесем роут posts.js в папку posts и переименуем его в index.js. То же самое сделаем с шаблоном и сделаем заголовок ссылкой:

{{#each}}
    <h5>{{#link-to 'posts.post' this}}{{title}}{{/link-to}}</h5>
    <p>{{body}}</p>
{{/each}}

Для этого мы использовали хэлепер link-to, которому передали название роута и модель, из которой он сам достанет id.

Ну и добавим в шаблон post.hbs вывод поста:

<h5>{{title}}</h5>  
<p>{{body}}</p>  

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

Кстати, как вы могли заметить, на не пришлось редактировать роут post, чтобы указать какую модель нужно погрузить, так как Ember сам понял это по параметру :post_id. И заметьте, как мало кода нам пришлось написать чтобы все это получить.

Исходный код приложения можно посмотреть на гитхабе.

На этом пока все.