Сколько раз в жизни вам нужно было сделать одностраничное приложение, которое хранит данные
в локал сторадже наподобие TodoMVC из официального гайда Эмбера
?
Мне ни разу. Поэтому, как только я начинал делать что-то полезное,
то сразу же возникало сотни практических вопросов, на которые гайды не отвечали.
Если подумать, то что представляют из себя 99% современных сайтов? Это CRUD-работа с базой.
Предположим людям
не
нужен дизайн и всякая анимация, что лучше всего подходит для создания CRUD-интерфейсов?
Конечно
же Рейлс приложение на бутстрапе!
Предлагаю делать бутстрап-CRUD на Эмбере,
это будет почти также круто, как Рейлс
.
Сервер
Я решил, если клиент на javascript, то и сервер должен быть на javascript, а база должна быть Монго.
С меня сошло семь потов пока я написал простенькое приложение на ноде, обрабатывающее запросы
от REST-адаптера Эмбера. Если кратко, то запросы принимаются в виде {record: {field1: "value1"}, ...}
и
возвращаются в том же виде. Более подробно можно прочитать в официальной документации :
// INDEX
app.get("/api/posts", function(req, res) {
db.collection("posts").find().toArray(function(err, result) {
// ...
});
});
// CREATE
app.post("/api/posts", function(req, res) {
//..
});
// SHOW
app.get("/api/posts/:id", function(req, res) {
//..
});
// ...
Вообщем такой, обычный REST. Код сервера здесь
.
Я не добавил обработку ошибок и, уверен, нарушил кучу принципов построения ноде-приложений,
так как понятия не имею как это правильно делать, но для демонстрации работы Эмбера
нам будет достаточно и такого сервера.
Томстер, зажигай
В принципе Эмбер действительно похож на Рейлс. Каждой странице соответствует свой уникальный урл.
Определением кто будет рисовать страницу заведует раутер:
// app/router.js
Router.map(function() {
this.route('posts/index', {path: '/'});
this.route('posts/new', {path: '/posts/new'});
this.route('posts/show', {path: '/posts/:id'});
this.route('posts/edit', {path: '/posts/:id/edit'});
});
Рауты я использовал немного не канонические, но таким образом все файлы очень здорово расскладываются по
папочкам.
Когда мы заходим на главную страницу, то вызывается раут posts/index
. В Рейлс мы можем добавить вьюху,
а экшен в контроллере сгенерируется автоматически (хотя я так обычно не люблю делать и всегда
добавляю пустой метод), в Эмбере принцип автоматической генерации доведен до абсолюта.
Любой компонент в цепочке вызова после раута можно пропустить и он сгенерируется автоматически.
Для posts/index
мы определим раут явно:
// app/routes/posts/index.js
import Ember from 'ember';
export default Ember.Route.extend({
model: function(params) {
return this.store.find('post');
}
});
Обратите внимания используется магия модулей ES6, запоминать синтаксис не нужно,
заготовки файлов удобно создаются с помощью ember-cli
генераторов (в консоли ember g route posts/index
). Задача раута в большинстве случаев сохранить модель
или коллекцию моделей. На первом этапе нужно придерживаться правила один урл = один раут = одна модель/коллекция моделей.
Такое соглашение обедняет возможный функционал, но в дальнейшем мы научимся обходить
это ограничение несколькими способами.
Метод model
должен вернуть модель или промис (мы же находимся в асинхронном енвайроменте).
store
- это "база данных" нашего клиентского приложения. Работой с моделями занимается специальный
модуль Ember Data, который никак не может выйти из статуса беты. Стор частично зеркалирует данные,
содержащиеся на сервере. То есть, если мы запросим все посты с сервера, они будут лежат
в сторе, пока мы не запросим их заново. Мы можем добавить в стор запись и позднее ее сохранить на сервере.
Таким образом если сильно постараться
можно добиться приятной отзывчивости своего приложения.
store.find
возвращает промис
на коллекцию моделей post
, которые Эмбер запрашивает с сервера, используя REST-адаптер. Чтобы система
заработала необходимо определить собственно сам адаптер:
// app/adapters/application.js
import DS from 'ember-data';
import config from '../config/environment';
export default DS.RESTAdapter.extend({
host: config.baseHost,
namespace: 'api'
});
Хост указан, чтобы при разработке мы запускали ember-cli
и ноде-сервер на разных портах, нейспейс
помогает разделить запросы к API, с доставкой приложения (Эмбер хоть и одностраничное приложение,
при использовании history api он занимает все урлы).
И также описываем схему самой модели:
// app/models/post.js
import DS from 'ember-data';
export default DS.Model.extend({
title: DS.attr('string'),
body: DS.attr('string')
});
Многообразием типов Эмбер в отличие от Рейлс не балует (да и в принципе он не стремиться
нас в чем-то особо баловать), однако строки, числа, даты и даже связи между моделями есть.
Следующим компонентом в иерархии обработки запроса идет контроллер. Задача контроллера - хранить стейт
страницы и пробрасывать модель в темплейт. В данном случае мы оставим автосгенеренный контроллер.
Контроллеры нам понадобятся при реализации других раутов.
И наконец темплейт - это реактивный хенлебар-шаблон с расширениями и разными наворотами (в прошлой записе
я говорил, что все есть Ember.Object
с свойствами через set/get
и computed-свойствами, через
магию Эмбера все что вы поменяется в модели, контроллере и тд автоматически отобразится во вьюхе
наподобие React.js):
<ol class="breadcrumb">
<li class="active">Posts</li>
</ol>
<p>{{#link-to "posts/new" class="btn btn-default"}}New Post{{/link-to}}
{{#each post in model}}
<h2>{{#link-to "posts/show" post}}{{post.title}}{{/link-to}}</h2>
{{post.body}}
{{/each}}
Тут все просто: бежим по model
(который на самом деле
в данном случае коллекция постов) и отрисовываем. link-to
-
это хэлпер, которые делает известно что. Кстати особенностью создания ссылки в таком виде является то, что
при нажатии на эту ссылку, хук model
в рауте post/show
вызван не будет,
то есть не будет запроса к серверу и с главной
страницы вы мгновенно попадете на страницу поста. Это хорошо с одной стороны, но с другой стороны
вызывает такие нежданные баги, которым позавидует любая другая система.
Аналогично создаем раут для posts/show
.
// app/routes/posts/show.js
import Ember from 'ember';
export default Ember.Route.extend({
model: function(params) {
return this.store.find('post', params.id);
}
});
Здесь мы уже используем динамический раут с айдишником записи, прямо как в рейлс. Темплейт тоже
стандартный:
<ol class="breadcrumb">
<li>{{#link-to 'posts/index'}}Posts{{/link-to}}</li>
<li class="active">{{title}}</li>
</ol>
<h2>{{title}}</h2>
{{body}}
Хотя здесь есть один нюанс, мы используем {{title}}
вместо {{model.title}}
. Так можно делать. Здесь включается очередная магия Эмбера, которую пока можно принять просто как факт.
Пару мелочей
Бутстрап подключаем в Brocfile.js
(это штука для сборщика ассетов в ember-cli
):
// Brocfile.js
var EmberApp = require('ember-cli/lib/broccoli/ember-app');
var app = new EmberApp();
// Bootstrap
app.import("vendor/bootstrap-3.3.1/css/bootstrap.css");
app.import("vendor/bootstrap-3.3.1/js/bootstrap.js");
var fonts = [
"vendor/bootstrap-3.3.1/fonts/glyphicons-halflings-regular.eot",
"vendor/bootstrap-3.3.1/fonts/glyphicons-halflings-regular.svg",
"vendor/bootstrap-3.3.1/fonts/glyphicons-halflings-regular.ttf",
"vendor/bootstrap-3.3.1/fonts/glyphicons-halflings-regular.woff"
];
fonts.forEach(function(font) {
app.import(font, {destDir: "fonts"});
});
module.exports = app.toTree();
Создаем в базе тестовые записи:
rs-ds053668:PRIMARY> db.posts.find()
{ "_id" : ObjectId("54c9702fce1c75040021bb37"), "title" : "First Post", "body" : "Lorem
ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." }
{ "_id" : ObjectId("54c97fc5ce1c75040021bb39"), "title" : "Second Post", "body" : "Lorem
ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." }
ember build
- компилирует код клиентского приложения, это можно повесить в post-install
, но
для простоты я скомпилировал ассеты и положил их в репозитарий:
cd client && ember build --environment=production --output-path=../server/public
Ахалай Махалай! Киргуду Бургуду!
Результат


Ура!
Фух. Спасибо, что домотали до конца.
Я выкатил приложение на хероку, исходники на гитхабе. Сегодня нам получилось
реализовать только буковку R. Буковки С, U и D реализуем в следующий раз.
Если ничего не понятно - это нормально.
Эмбер и не должен быть понятным. Это же Эмбер! Спасибо за внимание и до новых встреч!