Статьи

Ember и TodoMVC на рельсах

embertodo

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

Я уверен, что многие из вас играли с этими структурами, наблюдая, как другие делают то же самое . Эти короткие заметки мало что говорят о том, что значит создавать что-то полезное. В этой части я буду исследовать, что значит создавать что-то действительно ценное.

Выбор правильной платформы MVC может показаться сложным. Есть только Backbone.js, Angular.js, Ember.js. По моим оценкам, прагматичный разработчик Rails решил, что Ember.js — самая дружественная среда Rails. Он хорошо интегрируется с Rails и хорошо подходит для переключения с бэкенда на внешний интерфейс.

Чтобы построить что-то интересное, а также не изобретать велосипед, мы будем строить поверх приложения TodoMVC . Это то же самое приложение, которое используется в качестве примера в официальном руководстве по ember . Наша версия будет сфокусирована на том, как создать и расширить ее следующими способами:

  • перейти к проекту Rails
  • использовать Rails в качестве внутреннего хранилища
  • добавить аутентификацию
  • добавить личные списки для аутентифицированных пользователей
  • добавить защищенный обмен списками

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

TodoMVC в Эмбер

Приложение TodoMVC используется в качестве универсального примера для сравнения оболочек javascript. Он обладает достаточной функциональностью для демонстрации фреймворка и в то же время мгновенно знаком с любым потенциальным разработчиком. Давайте кратко рассмотрим особенности.

Приложение отображает список элементов Todo с текстовым полем вверху. Вы можете добавлять новые элементы в список, используя текстовое поле. Отдельные элементы также можно редактировать, дважды щелкнув по ним, и удалить с помощью значка удаления, который отображается при наведении курсора. Все задачи могут быть помечены как выполненные с помощью флажка nex для ввода.

Ниже списка есть счетчик незавершенных предметов и фильтр для отображения всех / активных / выполненных задач. Наконец, вы можете удалить все завершенные элементы из списка, используя кнопку «Очистить завершено» внизу.

TodoMVC

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

Базовый шаблон — это место, где можно начать знакомство с приложением Ember. В этом шаблоне все собрано вместе: вы получаете обзор (из тегов script Следующая выдержка из приложения TodoMVC:

 <!doctype html>
<html lang="en" data-framework="emberjs">
  <head>
    <meta charset="utf-8">
    <title>ember.js • TodoMVC</title>
    <link rel="stylesheet" href="bower_components/todomvc-common/base.css">
  </head>
  <body>
    <script type="text/x-handlebars" data-template-name="todos">
    <!--handlebars template content omitted-->
    </script>

    <!--library files-->
    <script src="bower_components/todomvc-common/base.js"></script>
    <script src="bower_components/jquery/jquery.js"></script>
    <script src="bower_components/handlebars/handlebars.js"></script>
    <script src="bower_components/ember/ember.js"></script>
    <script src="bower_components/ember-data/ember-data.js"></script>
    <script src="bower_components/ember-localstorage-adapter/localstorage_adapter.js"></script>

    <!--application files-->
    <script src="js/app.js"></script>
    <script src="js/router.js"></script>
    <script src="js/models/todo.js"></script>
    <script src="js/controllers/todos_controller.js"></script>
    <script src="js/controllers/todo_controller.js"></script>
    <script src="js/views/edit_todo_view.js"></script>
    <script src="js/views/todos_view.js"></script>
    <script src="js/helpers/pluralize.js"></script>
  </body>
</html>

По большей части это выглядит как стандартный документ HTML5 с большим количеством javascript. Единственная нестандартная часть — это шаблон x-handlebars . Код здесь опущен, но обсуждается в официальном руководстве ember . Наличие такого кода внутри HTML хорошо для небольших приложений, но мы извлечем его как часть перехода на Rails.

Импорт JavaScript состоит из двух частей: первая часть — это импорт файлов библиотеки, необходимых для запуска приложения Ember, а другая — это само приложение Ember. Оба из них обсуждаются более подробно в руководстве, поэтому обратитесь к нему для получения дополнительной информации.

Настройка Rails

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

 gem 'ember-rails'
gem 'ember-data-source', '>= 1.0.0.beta7'

rails g ember:bootstrap

Генератор создает структуру папок ember в app / assets / javascripts . Текущая версия не идеальна, и для завершения настройки требуются небольшие изменения.

Сначала удалите исходный файл app / assets / javascripts / application.js . Затем добавьте следующие две строки в самый верх app / assets / javascripts / application.js.coffee, чтобы загрузить jQuery перед загрузкой Ember.

 #= require jquery
#= require jquery_ujs

Чтобы открыть корневую страницу, добавьте следующее в config / rout.rb

 Rails.application.routes.draw do
  root to: 'application#index'
end

Также добавьте пустой app / views / application / index.html.erb . Это хорошая отправная точка, использующая стандартный ApplicationControllerindex Запустите приложение Rails ( rails shttp: // localhost: 3000, чтобы убедиться, что все подключено.

Перемещение TodoMVC в Rails

Пришло время скопировать приложение TodoMVC в наше приложение Rails. Полученный код находится на github , если вы хотите перейти к концу.

Начните с копирования шаблона руля, который обсуждался ранее, в app / views / application / index.html.haml . Отредактируйте файл app / views / layouts / application.html.erb , удалив ссылки на turbolinksjavascript_include_tagyieldbody Для дополнительного кредита мы можем удалить турболинки из Gemfile, потому что мы не будем их использовать.

Завершите миграцию, скопировав следующие файлы и преобразовав их в CoffeeScript.

js / rout.js => приложение / assets / javascripts / rout.js.coffee

 TadaEmber.Router.map ->
  @resource 'todos', path: '/', ->
    @route 'active'
    @route 'completed'

TadaEmber.TodosRoute = Ember.Route.extend
  model: -> @store.find('todo')

TadaEmber.TodosIndexRoute = Ember.Route.extend
  setupController: -> @controllerFor('todos').set('filteredTodos', this.modelFor('todos'))

TadaEmber.TodosActiveRoute = Ember.Route.extend
  setupController: ->
    todos = @store.filter 'todo', (todo) ->
      !todo.get('isCompleted')

    @controllerFor('todos').set('filteredTodos', todos)

TadaEmber.TodosCompletedRoute = Ember.Route.extend
  setupController: ->
    todos = @store.filter 'todo', (todo) ->
      todo.get('isCompleted')

    @controllerFor('todos').set('filteredTodos', todos)

js / models / todo.js => приложение / assets / javascripts / models / todo.js

 TadaEmber.Todo = DS.Model.extend
  title: DS.attr('string')
  isCompleted: DS.attr('boolean')

js / controllers / todos controller.js => приложение / assets / javascripts / controllers / todos controller.js.cofee

 TadaEmber.TodosController = Ember.ArrayController.extend
  actions:
    createTodo: ->
      title = @get('newTitle').trim()
      return if !title

      todo = @store.createRecord 'todo',
        title: title
        isCompleted: false
      todo.save()

      @set('newTitle', '')

    clearCompleted: ->
      completed = @get('completed')
      completed.invoke('deleteRecord')
      completed.invoke('save')

  remaining: Ember.computed.filterBy('content', 'isCompleted', false)
  completed: Ember.computed.filterBy('content', 'isCompleted', true)

  allAreDone: ((key, value) ->
    if value != undefined
      @setEach('isCompleted', value)
      return value;
    else
      length = @get('length')
      completedLength = @get('completed.length')

      return length > 0 && length == completedLength
  ).property('length', 'completed.length')

js / controllers / todo controller.js => приложение / assets / javascripts / controllers / todo controller.js.coffee

 TadaEmber.TodoController = Ember.ObjectController.extend
  isEditing: false

  bufferedTitle: Ember.computed.oneWay('title')

  actions:
    editTodo: -> @set('isEditing', true)
    doneEditing: ->
      bufferedTitle = @get('bufferedTitle').trim()
      if Ember.isEmpty(bufferedTitle)
        Ember.run.debounce(@, 'removeTodo', 0)
      else
        todo = @get('model')
        todo.set('title', bufferedTitle)
        todo.save()

      @set('bufferedTitle', bufferedTitle)
      @set('isEditing', false)

    cancelEditing: ->
      @set('bufferedTitle', @get('title'))
      @set('isEditing', false)

    removeTodo: -> @removeTodo()

  removeTodo: ->
    todo = @get('model')
    todo.deleteRecord()
    todo.save()

  saveWhenCompleted: (->
    @get('model').save()
  ).observes('isCompleted')

js / views / edit todo view.js => приложение / assets / javascripts / views / edit todo view.js.coffee

 TadaEmber.EditTodoView = Ember.TextField.extend
  focusOnInsert: (->
    @.$().val(@.$().val())
    @.$().focus
  ).on('disInsertElement')

Ember.Handlebars.helper('edit-todo', TadaEmber.EditTodoView)

js / views / todos view.js => приложение / assets / javascripts / views / todos view.js.coffee

 TadaEmber.TodosView = Ember.View.extend
  focusInput: (-> @.$('#new-todo').focus() ).on('disInsertElement')

js / helpers / множественное число.js => приложение / активы / javascripts / помощники / множественное число.js

 Ember.Handlebars.helper 'pluralize', (singular, count) ->
  inflector = Ember.Inflector.inflector;

  count == 1 ? singular : inflector.pluralize(singular)

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

 TadaEmber.Store = DS.Store.extend()
  # Override the default adapter with the `DS.ActiveModelAdapter` which
  # is built to work nicely with the ActiveModel::Serializers gem.
  #adapter: '_ams'

TadaEmber.ApplicationAdapter = DS.LSAdapter.extend
  namespace: 'tada-emberjs'

Почти сделано. Скопируйте компоненты bower / ember-localstorage-adapter / localstorage adapter.js в app / assets / javascript / localstorage adapter.js и добавьте следующую строку в начало файла app / assets / javascript / tadaember.js.coffee

 #= require ./localstorage_adapter

Завершите преобразование, скопировав содержимое тега scriptapp / views / application / index.html.erb в app / javascripts / templates / todos.hbs . Наконец, копирование CSS и изображений из исходного кода в наш каталог ресурсов добавит некоторые стили.

Добавление Rails в бэкэнд

Список хранит свои данные внутри localstorage Открытие приложения в другом браузере приведет к сбросу приложения в чистое состояние без каких-либо задач. Мы исправим это, используя приложение Rails в качестве поставщика хранилища.

Сначала создайте модель и перенесите

 rails g model Todo title is_completed:boolean
rake db:migrate

Добавьте контроллер, который будет действовать как API для приложения Ember. Не забудьте добавить ресурсный вызов в роутер.

приложение / контроллеры / todos_controller.rb

 class TodosController < ApplicationController
  respond_to :json

  def index
    respond_with Todo.all
  end

  def show
    respond_with Todo.find(params[:id])
  end

  def create
    respond_with Todo.create(todo_params)
  end

  def update
    respond_with Todo.update(params[:id], todo_params)
  end

  def destroy
    respond_with Todo.destroy(params[:id])
  end

  private
    # Never trust parameters from the scary internet, only allow the white list through.
    def todo_params
      params.require(:todo).permit(:title, :is_completed)
    end
end

конфиг / routes.rb

 Rails.application.routes.draw do
  resources :todos
  root to: 'application#index'
end

Наконец, добавьте сериализатор для Rails, чтобы правильно сериализовать модель. Ember ожидает идентификатор строки для каждой модели. ActiveModelAdapteris_completedisCompleted

Приложение / сериализаторов / todo_serializer.rb

 class TodoSerializer < ActiveModel::Serializer
  # fix for ember-data deserializer not being able to handle non-string ids
  def id
    object.id.to_s
  end

  attributes :id, :title, :is_completed
end

Чтобы снять учебные колеса и использовать новый бэкэнд Rails, обновите хранилище Ember, чтобы использовать хранилище activemodel, предоставляемое самоцветом ember-rails. (см. это для деталей.)

 TadaEmber.Store = DS.Store.extend
  # Override the default adapter with the `DS.ActiveModelAdapter` which
  # is built to work nicely with the ActiveModel::Serializers gem.
  adapter: '-active-model'

Конечный продукт доступен на GitHub

Вывод

Мы успешно перенесли приложение TodoMVC из автономного приложения в приложение Rails. Мы также отошли от локального хранилища и храним данные в Rails. В следующих статьях мы рассмотрим добавление аутентификации и возможность делиться списком через URL.