Главная » JavaScript » AngularJS и Ruby on Rails для одностраничного веб приложения
Rails+Angular

26.02.2016 by Devjournal

AngularJS и Ruby on Rails для одностраничного веб приложения

Простое приложение

Идея приложения довольно проста. Приложение для офисных работников, которые могут рассказать, где они будут проводить свой перерыв и возможно собраться вместе в это время.

Установка Rails проекта

Вместо обычного Rails проекта, мы будем использовать Rails::API. Я уже пробовал реализовать проект на AnguarJS вместе с полномасштабным Rails, но все кончилось большим количеством не используемых вьюх(view).

Сначала установим Rails::API.

$ gem install rails-api

Создание нового проекта Rails::API работает почти так же как и с обычным Rails проектом.

$ rails-api new fake_lunch_hub -T -d postgresql

Перейдем в наш проект

$ cd fake_lunch_hub

Создадим нашего пользователя PostgreSQL

$ createuser -P -s -e fake_lunch_hub

Создадим базу данных

rake db:create

Теперь создадим ресурс, чтобы увидеть его через наше приложение AngularJS

Создание первого ресурса

Добавим гем ‘rspec-rails’ в наш Gemfile и запустим:

$ bundle install
$ rails g rspec:install

Когда произойдет генерация приложения, RSpec захочет создать автоматически файлы тестов, включающий так же тесты наших представлений. Но этого там быть не должно. Мы может сказать RSpec не создавать эти файлы:

require File.expand_path('../boot', __FILE__)
 
# Pick the frameworks you want:
require "active_model/railtie"
require "active_record/railtie"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_view/railtie"
require "sprockets/railtie"
# require "rails/test_unit/railtie"
 
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
 
module FakeLunchHub
  class Application < Rails::Application
    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration should go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded.
 
    # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
    # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
    # config.time_zone = 'Central Time (US & Canada)'
 
    # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
    # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
    # config.i18n.default_locale = :de
 
    config.generators do |g|
      g.test_framework :rspec,
        fixtures: false,
        view_specs: false,
        helper_specs: false,
        routing_specs: false,
        request_specs: false,
        controller_specs: true
    end
  end
end

В моем приложении, я хочу чтобы каждый мог уведомить только тех, кого он хочет, а не весь мир:) И это хороший способ для работника пообщаться не только со своим окружением, но и совсем произвольной компанией друзей. Итак я решил создать модель группы для приложения. Создадим ресурс Group с единственный атрибутом :name

$ rails g scaffold group name:string

Т.к. группы должны иметь имена, установим параметр null: false в миграции. Так же добавить параметр уникальности

class CreateGroups < ActiveRecord::Migration
  def change
    create_table :groups do |t|
      t.string :name, null: false
 
      t.timestamps
    end
 
    add_index :groups, :name, unique: true
  end
end
$ rake db:migrate

Теперь, если запустим наш rails сервер и перейдем по ссылке http://localhost:3000/groups , увидим пустые скобки []. На самом деле мы хотим получить доступ по ссылке http://localhost:3000/api/groups

Rails.application.routes.draw do
  scope '/api' do
    resources :groups, except: [:new, :edit]
  end
end

Добавим реальных тестов в это руководство хотя бы на стороне сервера.

require 'rails_helper'
 
RSpec.describe Group, :type => :model do
  before do
    @group = Group.new(name: "Ben Franklin Labs")
  end
 
  subject { @group }
 
  describe "when name is not present" do
    before { @group.name = " " }
    it { should_not be_valid }
  end
end

Для того, чтобы этот тест прошел нужно добавить валидацию

class Group < ActiveRecord::Base
  validates :name, presence: true
end

Мы так же должны регулировать тесты RSpec для нас, потому генератор RSpec не совсем совместим с Rails::API. Сгенерированые тесты содержат примеры для экшена new, даже если у нас его нет. Вы можете удалить пример самостоятельно или просто скопируйте код приведенный ниже.

require 'rails_helper'
 
# This spec was generated by rspec-rails when you ran the scaffold generator.
# It demonstrates how one might use RSpec to specify the controller code that
# was generated by Rails when you ran the scaffold generator.
#
# It assumes that the implementation code is generated by the rails scaffold
# generator.  If you are using any extension libraries to generate different
# controller code, this generated spec may or may not pass.
#
# It only uses APIs available in rails and/or rspec-rails.  There are a number
# of tools you can use to make these specs even more expressive, but we're
# sticking to rails and rspec-rails APIs to keep things simple and stable.
#
# Compared to earlier versions of this generator, there is very limited use of
# stubs and message expectations in this spec.  Stubs are only used when there
# is no simpler way to get a handle on the object needed for the example.
# Message expectations are only used when there is no simpler way to specify
# that an instance is receiving a specific message.
 
RSpec.describe GroupsController, :type => :controller do
 
  # This should return the minimal set of attributes required to create a valid
  # Group. As you add validations to Group, be sure to
  # adjust the attributes here as well.
  let(:valid_attributes) {
    skip("Add a hash of attributes valid for your model")
  }
 
  let(:invalid_attributes) {
    skip("Add a hash of attributes invalid for your model")
  }
 
  # This should return the minimal set of values that should be in the session
  # in order to pass any filters (e.g. authentication) defined in
  # GroupsController. Be sure to keep this updated too.
  let(:valid_session) { {} }
 
  describe "GET index" do
    it "assigns all groups as @groups" do
      group = Group.create! valid_attributes
      get :index, {}, valid_session
      expect(assigns(:groups)).to eq([group])
    end
  end
 
  describe "GET show" do
    it "assigns the requested group as @group" do
      group = Group.create! valid_attributes
      get :show, {:id => group.to_param}, valid_session
      expect(assigns(:group)).to eq(group)
    end
  end
 
  describe "GET edit" do
    it "assigns the requested group as @group" do
      group = Group.create! valid_attributes
      get :edit, {:id => group.to_param}, valid_session
      expect(assigns(:group)).to eq(group)
    end
  end
 
  describe "POST create" do
    describe "with valid params" do
      it "creates a new Group" do
        expect {
          post :create, {:group => valid_attributes}, valid_session
        }.to change(Group, :count).by(1)
      end
 
      it "assigns a newly created group as @group" do
        post :create, {:group => valid_attributes}, valid_session
        expect(assigns(:group)).to be_a(Group)
        expect(assigns(:group)).to be_persisted
      end
 
      it "redirects to the created group" do
        post :create, {:group => valid_attributes}, valid_session
        expect(response).to redirect_to(Group.last)
      end
    end
 
    describe "with invalid params" do
      it "assigns a newly created but unsaved group as @group" do
        post :create, {:group => invalid_attributes}, valid_session
        expect(assigns(:group)).to be_a_new(Group)
      end
 
      it "re-renders the 'new' template" do
        post :create, {:group => invalid_attributes}, valid_session
        expect(response).to render_template("new")
      end
    end
  end
 
  describe "PUT update" do
    describe "with valid params" do
      let(:new_attributes) {
        skip("Add a hash of attributes valid for your model")
      }
 
      it "updates the requested group" do
        group = Group.create! valid_attributes
        put :update, {:id => group.to_param, :group => new_attributes}, valid_session
        group.reload
        skip("Add assertions for updated state")
      end
 
      it "assigns the requested group as @group" do
        group = Group.create! valid_attributes
        put :update, {:id => group.to_param, :group => valid_attributes}, valid_session
        expect(assigns(:group)).to eq(group)
      end
 
      it "redirects to the group" do
        group = Group.create! valid_attributes
        put :update, {:id => group.to_param, :group => valid_attributes}, valid_session
        expect(response).to redirect_to(group)
      end
    end
 
    describe "with invalid params" do
      it "assigns the group as @group" do
        group = Group.create! valid_attributes
        put :update, {:id => group.to_param, :group => invalid_attributes}, valid_session
        expect(assigns(:group)).to eq(group)
      end
 
      it "re-renders the 'edit' template" do
        group = Group.create! valid_attributes
        put :update, {:id => group.to_param, :group => invalid_attributes}, valid_session
        expect(response).to render_template("edit")
      end
    end
  end
 
  describe "DELETE destroy" do
    it "destroys the requested group" do
      group = Group.create! valid_attributes
      expect {
        delete :destroy, {:id => group.to_param}, valid_session
      }.to change(Group, :count).by(-1)
    end
 
    it "redirects to the groups list" do
      group = Group.create! valid_attributes
      delete :destroy, {:id => group.to_param}, valid_session
      expect(response).to redirect_to(groups_url)
    end
  end
 
end

Теперь если мы запустим все тесты в командной строке, они будут успешными. Пока ничего интересного мы не увидим, но Rails::API сейчас готов.

Добавление клиентской части

На клиентской части мы будем использовать Yeoman — утилита для генерации фронтенда. Сперва установи Yeoman и воспользуемся генератором generator-gulp-angular.

$ npm install -g yo
$ npm install -g generator-gulp-angular

Клиентскую часть будем держать в директории client.

$ mkdir client && cd $_

Теперь мы сгенерируем приложение ANgularJS. Для запуска я буду использовать следующие параметры:

  • AngularJS версии 1.3
  • jQuery: 2.x
  • библиотека ресурсов REST: ngResourse
  • Роутер: UI Router
  • Bootstrap
  • Реализация компонентов Bootstrap: AngularUI
  • CSS: Sass(Node)
  • JS: CoffeeScript
  • HTML шаблнизатор: Jade
$ yo gulp-angular fake_lunch_hub

Если еще запущен сервер Rails yf gjhne 3000, то остановите его, потому Gulp тоже будет запускать на 3000 порту. Запусти Gulp

$ gulp serve

Gulp теперь должен открываться на http://localhost:3000/#/ где вы увидите «Allo, Allo». Наше AngularJS приложение на месте. Оно пока не знает как связаться с Rails теперь займемся этой частью работы.

Установка прокси

Единственный способ чтобы наш фронтенд узнал о нашей бэкенд сервере это установка связи между нашими двумя приложениями http://our-front-end-app/api/whatever и http://our-rails-server/api/whatever. Приступим.

Если заглянуть внутрь client/gulp, то увидим там файл proxy.js. Я бы хотел переделать это файл немного, чтобы на proxy заработал, но к сожалению, он показался мне сложным в работе. Поэтому я удалил этот файл, чтобы он больше не мешал нам в будущем.

Создадим новый файл в clietn/gulp и назовем его server.js.

'use strict';
 
var gulp = require('gulp');
var browserSync = require('browser-sync');
var browserSyncSpa = require('browser-sync-spa');
var util = require('util');
var proxyMiddleware = require('http-proxy-middleware');
var exec = require('child_process').exec;
 
module.exports = function(options) {
 
  function browserSyncInit(baseDir, browser) {
    browser = browser === undefined ? 'default' : browser;
 
    var routes = null;
    if(baseDir === options.src || (util.isArray(baseDir) && baseDir.indexOf(options.src) !== -1)) {
      routes = {
        '/bower_components': 'bower_components'
      };
    }
 
    var server = {
      baseDir: baseDir,
      routes: routes,
      middleware: [
        proxyMiddleware('/api', { target: 'http://localhost:3000' })
      ]
    };
 
    browserSync.instance = browserSync.init({
      port: 9000,
      startPath: '/',
      server: server,
      browser: browser
    });
  }
 
  browserSync.use(browserSyncSpa({
    selector: '[ng-app]'// Only needed for angular apps
  }));
 
  gulp.task('rails', function() {
    exec("rails server");
  });
 
  gulp.task('serve', ['watch'], function () {
    browserSyncInit([options.tmp + '/serve', options.src]);
  });
 
  gulp.task('serve:full-stack', ['rails', 'serve']);
 
  gulp.task('serve:dist', ['build'], function () {
    browserSyncInit(options.dist);
  });
 
  gulp.task('serve:e2e', ['inject'], function () {
    browserSyncInit([options.tmp + '/serve', options.src], []);
  });
 
  gulp.task('serve:e2e-dist', ['build'], function () {
    browserSyncInit(options.dist, []);
  });
};

Далее список моих изменений в файле:
1. Сконфигурировал BrowserSync для запуска на 9000 порту и теперь Rails может спокойно работать на 3000.
2. Добавлена прослойка говорящая “send requests to /api to http://localhost:3000
3. Добавлена задача для запуска Rails server
4. Добавлен serve:full-stack для запуска старой задачи serve, но сначала запускается задача для rails

Перед тем как продолжим установим http-proxy-middleware:

$ npm install --save-dev http-proxy-middleware

Теперь мы можем запускать наши новые задачи. Убедитесь, что ни Rails ни Gulp нигде не запущены перед этим.

$ gulp serve:full-stack

Три вещи, которые теперь должны произойти:
1. Фронтенд сервер должен открываться на 9000 порту.
2. Если перейти по адресу http://localhost:9000/api/foo, получим страницу No route matches [GET] «/api/foo»
3. Rails будет запущен на 3000 порту.

Получение данных от Rails для показа нашему клиентскому приложению

Теперь мы хотим получить немного данных для показа в нашем AngularJS приложении. Создадим немного данных:

# This file should contain all the record creation needed to seed the database with its default values.
# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
#
# Examples:
#
#   cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
#   Mayor.create(name: 'Emanuel', city: cities.first)
 
Group.create([
  { name: 'Ben Franklin Labs' },
  { name: 'Snip Salon Software' },
  { name: 'GloboChem' },
  { name: 'TechCorp' },
])

Запустим добавление данных

$ rake db:seed

Добавим немного когда в наш src/app/index.coffee

angular.module 'fakeLunchHub', ['ngAnimate', 'ngCookies', 'ngTouch', 'ngSanitize', 'ngResource', 'ui.router', 'ui.bootstrap']
  .config ($stateProvider, $urlRouterProvider) ->
    $stateProvider
      .state "home",
        url: "/",
        templateUrl: "app/main/main.html",
        controller: "MainCtrl"
      .state "groups",
        url: "/groups",
        templateUrl: "app/views/groups.html",
        controller: "GroupsCtrl"
 
    $urlRouterProvider.otherwise '/'

Затем добавим GroupsCtrl:

angular.module "fakeLunchHub"
  .controller "GroupsCtrl", ($scope) ->

Руками создадим папку src/app/controllers

Наконец создадим наше view в src/app/views/groups.jade:

div.container
  div(ng-include="'components/navbar/navbar.html'")
  h1 Groups

Перейдем http://localhost:9000/#/groups, увидим надпись в теге h1 Groups. Пока мы не получили данных от Rails. Это следующий шаг.
Хорошая библиотека для AngularJS/Rails называется angularjs-rails-resource. Установим её:

$ bower install --save angularjs-rails-resource

Добавим две вещи в src/app/index.coffee: модуль Rails и ресурс Group.

angular.module 'fakeLunchHub', ['ngAnimate', 'ngCookies', 'ngTouch', 'ngSanitize', 'ngResource', 'ui.router', 'ui.bootstrap', 'rails']
  .config ($stateProvider, $urlRouterProvider) ->
    $stateProvider
      .state "home",
        url: "/",
        templateUrl: "app/main/main.html",
        controller: "MainCtrl"
      .state "groups",
        url: "/groups",
        templateUrl: "app/views/groups.html",
        controller: "GroupsCtrl"
 
    $urlRouterProvider.otherwise '/'
 
  .factory "Group", (RailsResource) ->
    class Group extends RailsResource
      @configure url: "/api/groups", name: "group"

Добавим строку кода в наш контроллере, которая посылает HTTP запрос:

angular.module "fakeLunchHub"
  .controller "GroupsCtrl", ($scope, Group) ->
    Group.query().then (groups) -> $scope.groups = groups

И немного кода в наш шаблон:

div.container
  div(ng-include="'components/navbar/navbar.html'")
  h1 Groups
 
  ul(ng-repeat="group in groups")
    li {{ group.name }}

Перейдем на http://localhost:9000/#/groups, Увидим группу имен. Поздравляем! Мы написали одностраничное приложение. Это простое и и не очень полезное приложение, но мы близки к хорошему началу.

Пока это все

#Angular#AngularJS#javascript#node js#React JS#Ruby#Ruby on Rails

Comments

  1. rgreg@mail.ru
    02.03.2016 - 05:06

    Что лучше выбрать руби или php ??

  2. RGREG@MAIL.RU
    31.05.2016 - 12:47

    Или PHP!

Добавить комментарий

Your email address will not be published / Required fields are marked *