- PVSM.RU - https://www.pvsm.ru -
Заинтересовавшись методологией построения SPA-приложений на Ruby on Rails, я пришел к некоторым идеям, которые реализуются теперь в каждом моем приложении и впоследствии даже были выделены в отдельный гем Oxymoron [1]. На данный момент на Oxymoron написано более 20 достаточно крупных коммерческих рельсовых приложений. Хочу вынести гем на общественный суд. Поэтому дальнейшее свое повествование буду вести уже на его основе.
Пример [2] готового приложения.
Для меня этот гем на порядок сокращает количество рутинного кода и, как следствие, значительно повышает скорость разработки. Позволяет очень легко построить взаимодействие AngularJS + RoR.
Первым делом, необходимо подключить гем в Gemfile:
gem 'oxymoron'
Теперь, каждый раз при изменении routes.rb, либо при перезапуске приложения, в app/assets/javascripts будет генерироваться файл oxymoron.js, содержащий в себе весь необходимый функционал для построения приложения.
Следующим этапом необходимо произвести настройку ассетов. В простейшем случае это выглядет вот так:
Для application.js:
/*
= require oxymoron/underscore
= require oxymoron/angular
= require oxymoron/angular-resource
= require oxymoron/angular-cookies
= require oxymoron/angular-ui-router
= require oxymoron/ng-notify
= require oxymoron
= require_self
= require_tree ./controllers
*/
Для application.css:
/*
*= require oxymoron/ng-notify
*= require_self
*/
Мы используем UI Router, значит необходимо определить тег ui-view в нашем лейауте. Поскольку приложение будет использовать HTML5-роутинг, необходимо указать тег base. В нашем случае это application.html.slim. Я использую SLIM [3] в качестве препроцессора и всем категорически советую.
html ng-app="app"
head
title Блог
base href="/"
= stylesheet_link_tag 'application'
body
ui-view
= javascript_include_tag 'application'
Для всех AJAX-запросов необходимо выключить layout. Для этого в ApplicationController пропишем необходимую логику:
layout proc {
if request.xhr?
false
else
"application"
end
}
Для корректной обработки форм и простановки ng-model необходимо создать инициалайзер, переопределяющий дефолтный FormBuilder на OxymoronFormBuilder.
ActionView::Base.default_form_builder = OxymoronFormBuilder
Последним делом необходимо заинжектить модуль oxymoron в ваше приложение и сообщить UI Router, что будет использоваться автоматически сгенерированный роутинг:
var app = angular.module("app", ['ui.router', 'oxymoron']);
app.config(['$stateProvider', function ($stateProvider) {
$stateProvider.rails()
}])
Все готово для создания полноценного SPA-приложения!
Итак. Первым делом подготовим модель Post и RESTful-контроллер для управления этой моделью. Для этого в консоли выполним команды:
rails g model post title:string description:text
rake db:migrate
rails g controller posts index show
В routes.rb создадим ресурс posts:
Rails.application.routes.draw do
root to: "posts#index"
resources :posts
end
Теперь опишем методы нашего контроллера. Часто, один и тот же метод может возвращать в ответе как JSON-структуры, так и HTML-разметку, такие методы необходимо обернуть в respond_to.
class PostsController < ActiveRecord::Base
before_action :set_post, only: [:show, :edit, :update, :destroy]
def index
respond_to do |format|
format.html
format.json {
@posts = Post.all
render json: @posts
}
end
end
def show
respond_to do |format|
format.html
format.json {
render json: @post
}
end
end
def new
respond_to do |format|
format.html
format.json {
render json: Post.new
}
end
end
def edit
respond_to do |format|
format.html
format.json {
render json: @post
}
end
end
def create
@post = Post.new post_params
if @post.save
render json: {post: @post, msg: "Post successfully created", redirect_to: "posts_path"}
else
render json: {errors: @post.errors, msg: @post.errors.full_messages.join(', ')}, status: 422
end
end
def update
if @post.update(post_params)
render json: {post: @post, msg: "Post successfully updated", redirect_to: "posts_path"}
else
render json: {errors: @post.errors, msg: @post.errors.full_messages.join(', ')}, status: 422
end
end
def destroy
@post.destroy
render json: {msg: "Post successfully deleted"}
end
private
def set_post
@post = Post.find(params[:id])
end
def post_params
params.require(:post).permit(:title, :description)
end
end
Каждому Rails-контроллеру соответствует AngularJS-контроллер. Правило соответствия очень простое:
PostsController => PostsCtrl
Admin::PostsController => AdminPostsCtrl # для контроллеров внутри namespace Admin
Создадим соответствующий контроллер в app/javascripts/controllers/post_ctrl.js:
app.controller('PostsCtrl', ['Post', 'action', function (Post, action) {
var ctrl = this;
// Код отработает только для '/posts'
action('index', function(){
ctrl.posts = Post.query();
});
// Вызовется для паттерна '/posts/:id'
action('show', function (params){
ctrl.post = Post.get({id: params.id});
});
// Только для '/posts/new'
action('new', function(){
ctrl.post = Post.new();
// Присваивание каллбека создания, который будет вызван автоматически при сабмите формы. См. ниже.
ctrl.save = Post.create;
});
// Для паттерна '/posts/:id/edit'
action('edit', function (params){
ctrl.post = Post.edit({id: params.id});
// Аналогичное присваивание для каллбека обновления
ctrl.save = Post.update;
})
// Общий код. Вызовется для двух методов edit и new.
action(['edit', 'new'], function(){
//
})
action(['index', 'edit', 'show'], function () {
ctrl.destroy = function (post) {
Post.destroy({id: post.id}, function () {
ctrl.posts = _.select(ctrl.posts, function (_post) {
return _post.id != post.id
})
})
}
})
// Так же внутри ресурса routes.rb можно создать свой кастомный метод. Вызовется для: '/posts/some_method'
action('some_method', function(){
//
})
// etc
}])
Обратите внимание на фабрику action. С помощью нее очень удобно разделять код между страницами приложения. Фабрика резолвится через сгенерированный стейт в oxymoron.js и, как следствие, знает текущий рельсовый метод контроллера.
action(['edit', 'new'], function(){
// код выполнится только на страницах posts/new и posts/:id/edit
})
Далее следует обратить внимание на фабрику Post. Данная фабрика генерируется автоматически из ресурса, определенного в routes.rb. Для правильной генерации, у ресурса должен быть определен метод show. Из коробки доступны следующие методы работы с ресурсом:
Post.query() // => GET /posts.json
Post.get({id: id}) // => GET /posts/:id.json
Post.new() // => GET /posts/new.json
Post.edit({id: id}) // => GET /posts/:id/edit.json
Post.create({post: post}) // => POST /posts.json
Post.update({id: id, post: post}) // => PUT /posts/:id.json
Post.destroy({id: id}) // => DELETE /posts/:id.json
Кастомные методы ресурса (member и collection) работаю точно так же. Например:
resources :posts do
member do
get "comments", is_array: true
end
end
Создаст соответствующий метод для AngularJS-ресурса:
Post.comments({id: id}) //=> posts#comments
Устанавливайте опцию is_array: true, если ожидается, что в ответ ожидается массив. В противном случае, AngularJS выкинет исключение.
Осталось создать недостающие вьюхи.
h1 Posts
input.form-control type="text" ng-model="search" placeholder="Поиск"
br
table.table.table-bordered
thead
tr
th Date
th Title
th
tbody
tr ng-repeat="post in ctrl.posts | filter:search"
td ng-bind="post.created_at | date:'dd.MM.yyyy'"
td
a ui-sref="post_path(post)" ng-bind="post.title"
td.w1
a.btn.btn-danger ng-click="ctrl.destroy(post)" Удалить
a.btn.btn-primary ui-sref="edit_post_path(post)" Редактировать
.small ng-bind="ctrl.post.created_at | date:'dd.MM.yyyy'"
a.btn.btn-primary ui-sref="edit_post_path(ctrl.post)" Редактировать
a.btn.btn-danger ng-click="ctrl.destroy(ctrl.post)" Удалить
h1 ng-bind="ctrl.post.title"
p ng-bind="ctrl.post.description"
h1 New post
= render 'form'
h1 Edit post
= render 'form'
= form_for Post.new do |f|
div
= f.label :title
= f.text_field :title
div
= f.label :description
= f.text_area :description
= f.submit "Save"
Особое внимание стоит обратить на результат генерации хелпера form_for.
<form ng-submit="formQuery = ctrl.save({form_name: 'post', id: ctrl.post.id, post: ctrl.post}); $event.preventDefault();"></form>
Достаточно определить метод ctrl.save внутри контроллера и он будет выполнятся каждый раз при сабмите формы и передавать параметры, которые вы видите. Но поскольку эти параметры идеально подходят в качестве аргументов для методов ресурса update и create, мы можем в нашем контроллере написать всего лишь ctrl.save = Post.create. В листинге PostsCtrl этот момент помечен соответствующим комментарием.
Для тегов text_field и text_area был автоматически добавлен атрибут ng-model. Правило составления ng-model следующее:
ng-model="ctrl.название_модели.название_поля"
В листинге рельсового PostsController вы наверняка заметили поля msg, redirect_to и тд в методе render. Для этих полей работает специальный перехватчик, который производит необходимое действие до передачи результата в контроллер.
msg – содержимое будет показано в всплывашке зеленого цвета в верхней части экрана. Если передать в render статус какой либо ошибки, то цвет изменится на красный
errors – принимает объект errors.full_messages, служит для отображения ошибок непосредственно самих полей формы.
redirect_to – выполнить редирект к необходимому стейту UI Router
redirect_to_options – если стейт требует опции, например, страница show требует id, то необходимо указать их в данном поле
redirect_to_url – выполнить переход по указаному урлу
reload – полностью перезагрузить страницу пользователю
Все данные действия происходят без перезагрузки страницы пользователя. Используется HTML5-роутинг на основе UI Router.
Раньше приходилось использовать хелпер link_to, когда мы хотели определить ссылку в зависимости от названия роута. Теперь данный функционал реализует ui-sref [4] в привычной нам манере описания роута.
a ui-sref="posts_path" Все посты
a ui-sref="post_path({id: 2})" Пост №2
a ui-sref="edit_post_path({id: 2})" Редактирование поста №2
a ui-sref="new_post_path" Создание нового поста
В глобальной области видимости вы можете найти переменную Routes. Она работает практически точно так же, как и js-routes. Отличие лишь в том, что данная реализация принимает только объект и не имеет сахара в виде аргумента-числа. Возможен конфликт, поэтому рекомендую отключить js-routes.
Routes.posts_path() // => "/posts"
Routes.new_post_path() // => "/post/new"
Routes.edit_posts_path({id: 1}) // => "/post/1/edit"
// Параметры по умолчанию
Routes.defaultParams = {id: 1}
Routes.post_path({format: 'json'}) // => "/posts/1.json"
Мы написали примитивное SPA-приложение. При этом код выглядит абсолютно рельсовым, логики описано минимум, а та, что есть уже, является максимально общей. Я понимаю, что нет предела совершенству и, что Oxymoron далек от идеала, однако, надеюсь, что смог заинтересовать кого-нибудь своим подходом. Буду рад любой критике и любому позитивному участию в жизни гема.
Пример [2] готового приложения.
Автор: storuky
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ruby/120242
Ссылки в тексте:
[1] Oxymoron: https://github.com/storuky/oxymoron
[2] Пример: https://github.com/storuky/oxymoron_app
[3] SLIM: http://slim-lang.com/
[4] ui-sref: http://angular-ui.github.io/ui-router/site/#/api/ui.router.state.directive:ui-sref
[5] Источник: https://habrahabr.ru/post/283214/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.