Статьи

Изучение MVC на стороне клиента с Backbone.js

Backbone.js продолжает набирать популярность в сообществе JavaScript MVC. Я решил попробовать, создав простое одностраничное приложение для CRUD-модели с одним доменом.

Хотя это было не так тривиально, как традиционная реализация на стороне сервера в Rails, оно оказалось относительно чистым. Это длинное сообщение, и если вы пройдете через него, дайте мне знать, что вы думаете.

Первоначально Автор Джаред Кэрролл

Настройка на стороне сервера

Наша серверная часть — это стандартное приложение Rails 3.1. Мы пойдем с классическим блог-приложением, состоящим из одной модели Post в CRUD.

$ gem list rails
*** LOCAL GEMS *** 
rails (3.1.3)
$ rails new blog
$ cd blog
$ rails g scaffold post title:string body:text
$ rake db:migrate
$ : > app/views/posts/index.html.erb  # remove the default markup from posts/index.html.erb (this will be our app's homepage)

Мы будем использовать Ruby gem для backbone-on-rails, чтобы добавить backbone.js и underscore.js (единственная зависимость backbone.js) в наше приложение Rails.

$ echo "gem 'backbone-on-rails'" >> Gemfile
$ bundle install
$ rails g backbone:install
      insert  app/assets/javascripts/application.js
      create  app/assets/javascripts/collections
      create  app/assets/javascripts/models
      create  app/assets/javascripts/routers
      create  app/assets/javascripts/views
      create  app/assets/templates
      create  app/assets/javascripts/blog.js.coffee

В Backbone.js и underscore.js файлы не копируются в каталог вашего Rails приложения. Они оба автоматически загружаются гемом backbone-on-rails (Ruby jquery-rails включает в себя jquery.js и jquery_ujs.js одинаково).

Давайте используем генератор скаффолдов из backbone-on-rails для генерации наших объектов Backbone.js на стороне клиента.

$ rails g backbone:scaffold post
      create  app/assets/javascripts/models/post.js.coffee
      create  app/assets/javascripts/collections/posts.js.coffee
      create  app/assets/javascripts/routers/posts_router.js.coffee
      create  app/assets/javascripts/views/posts
      create  app/assets/javascripts/views/posts/posts_index.js.coffee
      create  app/assets/templates/posts
      create  app/assets/templates/posts/index.jst.eco

Мы будем использовать CoffeeScript, язык Rails по умолчанию на стороне клиента, в нашем приложении Backbone. Более чистый Ruby-подобный синтаксис CoffeeScript поможет сделать код более читабельным и понятным.

Список сообщений

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

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

Приложение / активы / JavaScripts / маршрутизаторы / posts_router.js.coffee

class Blog.Routers.Posts extends Backbone.Router
  routes:
    '' : 'index'
 index: ->
    posts = new Blog.Collections.Posts
    new Blog.Views.PostsIndex collection: posts
    posts.fetch()

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

Коллекция Posts сконфигурирована так, чтобы отправлять запросы на сервер к нашему приложению Rails по адресу «/ posts» и содержать модели Post.

приложение / активы / JavaScripts / коллекции / posts.js.coffee

class Blog.Collections.Posts extends Backbone.Collection
  url: '/posts'
  model: Blog.Models.Post

Наша модель Post настолько проста, насколько это возможно. Это просто подкласс Backbone.Model и не содержит никакой собственной логики.

приложение / активы / JavaScripts / модель / post.js.coffee

class Blog.Models.Post extends Backbone.Model

Прежде чем мы взглянем на представление PostsIndex, давайте сначала быстро рассмотрим макет Rails, используемый на стороне сервера.

приложение / просмотров / макеты / application.html.erb

<!DOCTYPE html>
<html>
  <head>
    <title>Blog</title>
    <%= stylesheet_link_tag    "application" %>
    <%= javascript_include_tag "application" %>
    <%= csrf_meta_tags %>
  </head>
  <body>
    <div id="app">
      <%= yield %>
    </div>
  </body>
</html>

Единственное изменение, которое мы внесли в этот макет Rails по умолчанию, это добавление одного «#app» <div>. Этот <div> будет действовать как контейнер для нашего приложения. Каждый просмотр Backbone заменит его содержимое.

Теперь мы можем взглянуть на вид Backbone.

приложение / активы / JavaScripts / просмотров / сообщений / posts_index.js.coffee

class Blog.Views.PostsIndex extends Backbone.View
  el: '#app'
  template: JST['posts/index']
  initialize: ->
    @collection.bind 'reset', @render, @
  render: ->
    $(@el).html(@template())
    @collection.each (post) =>
      view = new Blog.Views.PostsItem model: post
      @$('#posts').append(view.render().el)
    @

При инициализации представление связывает свой метод рендеринга как обработчик с событием сброса своей коллекции (это коллекция Posts, которую мы создали в действии Blog.Routers.Posts # index). Коллекция сообщений инициирует событие сброса после успешной загрузки своих сообщений с сервера. Когда событие вызывается, представление будет отображать свой шаблон и создавать и отображать другое представление для каждой публикации в своей коллекции.

Вот шаблон, используемый представлением PostsIndex:

приложение / активы / шаблоны / сообщений / index.jst.eco

<h1>Posts</h1>

<table id="posts">
  <tr>
    <th>Title</th>
    <th>Body</th>
    <th></th>
    <th></th>
    <th></th>
  </tr>
</table>

<br />

<a href="#posts/new">new post</a>

Эта таблица будет заполнена представлением, используемым для каждого сообщения в коллекции:

app / assets / javascripts / views / posts / posts_item.js.coffee

class Blog.Views.PostsItem extends Backbone.View
  tagName: 'tr'
  template: JST['posts/item']
  render: ->
    $(@el).html(@template(post: @model))
    @

И его шаблон:

app / assets / templates / posts / item.jst.eco

<td><%= @post.get 'title' %></td>
<td><%= @post.get 'body' %></td>
<td><a href="#posts/<%= @post.id %>">show</a></td>

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

приложение / активы / JavaScripts / blog.js.coffee

window.Blog =
  Models: {}
  Collections: {}
  Views: {}
  Routers: {}
  init: ->
    new Blog.Routers.Posts
    Backbone.history.start()

$(document).ready ->
  Blog.init()

Обработчик события onDOMReady вызывает функцию init, которая создает PostsRouter и сообщает Backbone начать мониторинг всех событий hashchange. Все наши клиентские страницы будут использовать хеш-фрагменты в качестве URL. Backbone будет прослушивать изменения фрагментов хеша и направлять их, используя маршруты, указанные в нашем PostsRouter.

Наконец, теперь мы можем запустить наш сервер Rails и открыть наше приложение по адресу http: // localhost: 3000 / posts . К сожалению, у нас пока нет сообщений. Итак, давайте перейдем к созданию поста.

Создание сообщения

Внизу нашего существующего домашнего шаблона находится ссылка для создания нового сообщения:

приложение / активы / шаблоны / сообщений / index.jst.eco

<a href="#posts/new">new post</a>

Это означает, что нам нужно добавить другой маршрут и действие к нашему PostsRouter.

Приложение / активы / JavaScripts / маршрутизаторы / posts_router.js.coffee

class Blog.Routers.Posts extends Backbone.Router
  routes:
    '' : 'index'
    'posts/new' : 'new'
  index: ->
    # same code from above...
  new: ->
    post = new Blog.Models.Post
    posts = new Blog.Collections.Posts
    posts.bind 'add', =>
      triggerRouter = true
      @navigate '', triggerRouter
    view = new Blog.Views.PostsNew
      collection: posts
      model: post
    view.render()

В нашем новом действии мы создаем модель Post, затем создаем коллекцию Posts и привязываем анонимный обработчик к его событию add. Коллекция будет запускать событие добавления всякий раз, когда к нему добавляется новая модель. Наш обработчик события добавления эквивалентен перенаправлению на стороне сервера, переместив маршрутизатор на «пустой» маршрутизатор (домашнюю страницу). Наконец мы создаем и визуализируем представление для отправки нового сообщения.
приложение / активы / JavaScripts / просмотров / сообщений / posts_new.js.coffee

class Blog.Views.PostsNew extends Backbone.View
  el: '#app'
  template: JST['posts/new']
  events:
    'submit form' : 'create'
  render: ->
    $(@el).html(@template(post: @model))
    @
  create: (event) ->
    event.preventDefault()
    @collection.create
      title: @$('#title').val()
      body: @$('#body').val()

Элемент (PostsNew # el), используемый в нашем новом представлении сообщений, является глобальным «#app» <div>. Новое представление сообщения заменит содержимое этого <div> своим собственным шаблоном, содержащим форму для отправки нового сообщения.

Представление также регистрирует обработчик для события submit из <form> в своем шаблоне. Этот обработчик останавливает поведение отправки формы по умолчанию, а затем просит свою коллекцию сообщений создать новое сообщение. Это приведет к HTTP-запросу POST на сервер, и в случае успеха коллекция вызовет событие add. Это то же событие, к которому наш PostsRouter в своем новом действии привязал анонимный обработчик «перенаправления».

А вот и новый шаблон представления поста:

приложение / активы / шаблоны / сообщений / new.jst.eco

<h1>New post</h1>

<form>
  <div class="field">
    <label for="title">Title</label><br />
    <input type="text" name="title" id="title" />
  </div>
  <div class="field">
    <label for="body">Body</label><br />
    <textarea name="body" id="body"></textarea>
  </div>
  <div class="actions">
    <input type="submit" value="create" />
  </div>
</form>

<a href="#">back</a>

Отлично, теперь мы можем создавать сообщения. Однако мы не можем позволить людям создавать посты без заголовка и тела. Давайте посмотрим, что мы можем сделать, чтобы проверить наши новые сообщения.

Проверка сообщения

Adding some client-side validation will help improve the responsiveness of our UI. We’ll still validate posts on the server-side, but for the user’s sake, I feel this duplication is worth the tradeoff.

Backbone models include a validate method that can be used to perform custom validations. However, as Rails developers we’re used to ActiveRecord‘s high-level, declarative approach to validation. And thanks to the jQuery validation plugin, we can have declarative validation on the client-side too.

First, download the plugin (after downloading and unzipping it, remember to restart your Rails server).

$ wget http://jquery.bassistance.de/validate/jquery-validation-1.9.0.zip --directory-prefix vendor/assets/javascripts
$ unzip vendor/assets/javascripts/jquery-validation-1.9.0.zip -d vendor/assets/javascripts

Затем добавьте плагин в файл манифеста JavaScript нашего приложения.

приложение / активы / JavaScripts / application.js

//= require jquery
//= require jquery_ujs
//= require underscore
//= require backbone
//= require jquery-validation-1.9.0/jquery.validate
// other default initialization

Теперь мы можем добавить некоторые проверки в наши новые сообщения.

приложение / активы / JavaScripts / просмотров / сообщений / posts_new.js.coffee

class Blog.Views.PostsNew extends Backbone.View
  # same code as above...
  render: ->
    $(@el).html(@template(post: @model))
    @$('form').validate
      rules:
        title: 'required'
        body: 'required'
    @
  create: (event) ->
    # same code as above...

Плагин jQuery validate позволяет вам задавать правила проверки для каждого атрибута. Здесь нам требуется заголовок и тело. Метод validate остановит отправку формы при сбое проверки. Это также предотвратит выполнение обработчика отправки формы представления нового поста, #create.

Идите и попробуйте в браузере. Отправка сообщения без заголовка и тела теперь не удастся и отобразит сообщение об ошибке.

Просмотр сообщений

Хорошо, пока мы можем отобразить список сообщений, создать сообщение и проверить сообщение. Далее идет просмотр отдельного поста.

На нашей домашней странице каждая строка сообщения содержала следующую ссылку для просмотра сообщения:

приложение / активы / шаблоны / сообщения / item.jst.eco

<td><a href="#posts/<%= @post.id %>">show</a></td>

Давайте добавим маршрут и действие в наш маршрутизатор для этой страницы поста (т.е. #show).

Приложение / активы / JavaScripts / маршрутизаторы / posts_router.js.coffee

class Blog.Routers.Posts extends Backbone.Router
  routes:
    '' : 'index'
    'posts/new' : 'new'
    'posts/:id' : 'show'
  index: ->
    # same code from above...
  new: ->
    # same code from above...
  show: (id) ->
    post = new Blog.Models.Post id: id
    view = new Blog.Views.PostsShow model: post
    collection = new Blog.Collections.Posts [post]
    post.fetch()

Этот новейший маршрут включает в себя именованный параметр: id. Backbone автоматически передаст значение именованного параметра в качестве аргумента нашему действию #show.

В настоящее время в Backbone выбор отдельной модели не очень элегантен. Выше один из самых чистых способов. Сначала вы создаете модель с запрошенным идентификатором, затем добавляете модель в коллекцию и, наконец, говорите модели, чтобы она сама выбирала.

Наше действие #show также создает экземпляр следующего представления PostsShow для отдельной записи.

приложение / активы / JavaScripts / просмотров / сообщений / posts_show.js.coffee

class Blog.Views.PostsShow extends Backbone.View
  el: '#app'
  template: JST['posts/show']
  initialize: ->
    @model.bind 'change', @render, @
  render: ->
    $(@el).html(@template(post: @model))
    @

Во время инициализации представление добавляет обработчик к событию изменения своего сообщения. Почта запустит событие изменения после того, как оно было успешно получено.

Элемент (PostsShow # el) для этого представления — это тот же глобальный контейнер «#app» <div>, который использовался списком сообщений и новыми представлениями сообщений. Как и эти два представления, отдельное представление поста также заменит содержимое этого <div>.

А вот посты показывают шаблон представления представления:

приложение / активы / шаблоны / сообщений / show.jst.eco

<p>
  <b>Title:</b>
  <%= @post.get 'title' %>
</p>

<p>
  <b>Body:</b>
  <%= @post.get 'body' %>
</p>

<a href="#">back</a>

Ссылка «назад» возвращается на нашу домашнюю страницу.

Клиентская MVC

Backbone.js brings the power and maintainability of MVC to the client-side. Routers can be used like Rails server-side controllers to create responsive, single-page apps. Views are also like controllers, but instead of responding to url changes, they respond to DOM level events, such as link clicks and form submissions. Templates contain the app’s markup, they’re what Rails calls views. Models and collections round out Backbone.js, providing traditional class-based, client-side domain modeling.

We’ve only covered the first two letters in CRUD, but it was a lot! I hope this article can give you some direction in implementing the rest of them. The client-side JavaScript MVC space is growing rapidly. Backbone’s quick and easy Rails integration makes it an easy sell but there are other options. Be sure to research other frameworks before evaluating what works best for you.

Source: http://blog.carbonfive.com/2011/12/19/exploring-client-side-mvc-with-backbonejs/