Integrate AngularJS to Rails 4.0

1. Tổng quan

angular_rr.jpg

Dựa trên mô hình MVC, Ruby on Rails có khả năng tạo ra được một website hoàn chỉnh từ Database cho tới View. Tuy nhiên, với những ứng dụng có quy mô lớn hơn, việc server-side phải đảm nhiệm đủ cả 3 phần Model, View, Controller khiến cho hiệu năng hoạt động của server giảm đáng kể. Sự xuất hiện của các framework dành cho client-side như EmberJs, BackboneJS, AngularJs đã phần nào giúp đỡ server đảm nhiệm phần view để có thể tương tác với người sử dụng. Trong bài tìm hiểu trước đó, chúng ta đã đi tìm hiểu về EmberJs trên RoR. Hôm nay, bài viết sẽ đi tìm hiểu về một ứng dụng khác, AngularJS. angular_rr AngularJS cung cấp một mô hình thu nhỏ của MVC trên phía người sử dụng. Giống như RoR, phần view đảm nhiệm việc thực hiên các thẻ HTML cùng với CSS trong khi Controller và Model đóng vai trò tương tác với dữ liệu. Điều khác biệt lớn nhất là thay vì tương tác trực tiếp với database như RoR, AngularJS sử dụng dữ liệu JSON lấy được từ server-side để hiển thị lên View. Rails ở đây đóng vai trò thuần túy là tương tác trực tiếp với database (MySQL, MongoDB, Sqlite..) và cung cấp các API cho phép trả ra dữ liệu dựa trên các request từ Client side. Việc phân chia độc lập 2 phần Back-end và Front-end không những giúp tăng hiệu năng hoạt đông của server mà còn giúp developer phát triển Client side có nhiều chức năng hơn. Trong phần tiếp theo, chúng ta sẽ đi sâu vào việc tạo một ứng dụng trực tiếp sử dụng RoR và AngularJS

2. Xây dựng ứng dụng dựa trên RoR và AngularJS

Trong phần này, chúng ta sẽ đi xây dựng một ứng dụng tổng hợp tất cả các video trên Railscast. Có thể tham khảo ứng dụng trên github: https://github.com/ducthien1490/angular_railscast

2.1 Khởi tạo

Các khởi tạo cơ bản:

  • Tạo application:

$rails new angular_railscast $cd angular_railscas

  • Gemfile:
    source "https://rubygems.org"
    gem "rails", "4.1.2"
    gem "sqlite3"
    gem "sass-rails", "~> 4.0.3"
    gem "uglifier", ">= 1.3.0"
    gem "coffee-rails", "~> 4.0.0"
    gem "turbolinks"
    gem "jbuilder", "~> 2.0"
    gem "sdoc", "~> 0.4.0",     group: :doc
    gem "spring",   group: :development
    gem "feedjira"
    gem "pry-rails"
  • Tạo migration, controller và model:
$rails g resource screencast title summary:text duration link published_at:datetime source video_url
$rake db:migrate
app/models/screencast.rb:
    class Screencast < ActiveRecord::Base
      validates_presence_of :title, :summary,
        :duration, :link, :published_at, :source, :video_url
      validates_uniqueness_of :video_url
    end
  • Import data từ Railscast sử dụng Feedjia:

lib/screencast_importer.rb:

require "feedjira"

class ScreencastImporter
  def self.import_railscasts
    Feedjira::Feed.add_common_feed_entry_element(:enclosure,
        :value => :url, :as => :video_url)
    Feedjira::Feed.add_common_feed_entry_element(
        'itunes:duration', :as => :duration)
    feed = Feedjira::Feed.fetch_and_parse(
        "http://feeds.feedburner.com/railscasts")
    feed.entries.each do |entry|
        title = entry.title.gsub(/^#\d+\s/, '')
        Screencast.where(video_url: entry.video_url).
            first_or_create(
                title:        title,
                summary:      entry.summary,
                duration:     entry.duration,
                link:         entry.url,
                published_at: entry.published,
                source:       "railscasts"
            )
    end
    Screencast.where(source: 'railscasts').count
  end
end

lib/tasks/sample_date.rake:

require "screencast_importer"

namespace :screencast_sync do
  desc "Sync all Railscast videos"
  task :railscasts => :environment do
    total = ScreencastImporter.import_railscasts
    puts "There are #{total} videos from Railscasts"
  end
end

Chạy rake để tạo dữ liệu mẫu:

rake screencast_sync:railscasts

2.2 Xây dựng API ở server-side

Định nghĩa routes

config/routes.rb:

Rails.application.routes.draw do
  scope :api do
    get "/screencasts(.:format)" => "screencasts#index"
    get "/screencasts/:id(.:format)" => "screencasts#show"
  end
end

Controller

app/controllers/screencasts_controller.rb:

class ScreencastsController < ApplicationController
  def index
    render json: Screencast.all
  end

  def show
    render json: Screencast.find(params[:id])
  end
end

Controller được viết tuân thủ theo Restful action sẽ trả dữ liệu ra dưới dạng JSON.

2.3 Xây dựng client-side

Khởi tạo

  • Như đã trình bày ở phần đầu, AngularJS tạo ra một mô hình MVC thu nhỏ trên client-side và tất cả đều được định nghĩa bên trong folder: app/assets/javascripts/angular/. Do đó ta cần phải tạo sub-folder dành cho AngularJS trong project:
$ mkdir -p app/assets/javascripts/angular/controllers \
           app/assets/javascripts/angular/directives \
           app/assets/javascripts/angular/services
  • Bên cạnh đó, ta cần phải thêm vào thư viện của AngularJS. Về cơ bản, thư viên Angular có thể thêm bằng sử dụng gem angular hoặc sử dụng CDN. Ở trong phần này, chúng ta sẽ thêm AngularJS library vào trong project sử dụng CDN của Google:
    <!DOCTYPE html>
    <html>
    <head>
      <title>AngularjsApp</title>
      <%= stylesheet_link_tag    'application', media: 'all'%>
      <%= csrf_meta_tags %>
    </head>
    <body>
    <header>
      Angular Casts
    </header>

    <%= yield %>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.6/angular.min.js"></script>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.6/angular-resource.min.js"></script>
    <%= javascript_include_tag 'application'%>

    </body>
</html>
  • Tiếp đến, ta khởi tạo Angular App bằng một file js.coffee và thêm vào trong file application.js:

    app/assets/javascripts/app.js.coffee:

window.App = angular.module("AngularCasts", ["ngResource"])
app/assets/javascripts/application.js:
//= require app
//= require_tree ./angular

Tạo home page

  • Tạo ra một controller và định nghĩa root dành cho home page:
$ rails g controller home index

config/routes.rb:

    Rails.application.routes.draw do
      scope :api do
        get "/screencasts(.:format)" => "screencasts#index"
        get "/screencasts/:id(.:format)" => "screencasts#show"
      end
      root to: "home#index"
    end

Từ đây trở đi là Angular !

  • Thêm vào view:

    app/views/layouts/application.html.erb:

<!DOCTYPE html>
<html ng-app="AngularCasts">
......
<body>
<header>
  Angular Casts
</header>
.....
  • Khởi tạo Controller dành cho AngularJS: app/assets/javascripts/angular/controllers/screencasts_ctrl.js.coffee:
    App.controller "ScreencastsCtrl", ["$scope", "Screencast", ($scope, Screencast) ->
      $scope.message = "Angular Hello World!
    ]
  • Hiển thị message trên home page:

    app/views/home/index.html.erb:

<div ng-controller="ScreencastsCtrl">
    <h1>Message: {{message}}</h1>
</div>

=> truy cập thử http://localhost:3000 để kiểm tra message !

  • Thêm một chút CSS (cũng không ít lắm 😄):
    body {
      font-size: 12px;
      font-family: Helvetica, sans-serif;
      background-color: #ddd;
      margin: 0px;
    }
    header {
      background-color: #4F4F4F;
      color: #fff;
      position: absolute;
      height: 36px;
      top: 0;
      left: 0;
      right: 0;
      font-size: 18px;
      line-height: 36px;
      font-weight: bold;
      padding-left: 15px;
    }
    #screencast-ctrl {
      background-color: #fff;
      position: absolute;
      top: 37px;
      width: 100%;
      bottom: 0;
      overflow: auto;
    }
    #screencast-list-container {
      background-color: #fff;
      position: absolute;
      min-height: 700px;
      width: 300px;
      top: 37px;
      left: 0;
      bottom: 0;
      overflow: auto;
      -webkit-overflow-scrolling: touch;
      ul {
        margin: 0px;
        list-style: none;
        padding: 0px;
        li {
          cursor: pointer;
          border-bottom: 1px solid #ddd;
          padding: 0 10px;
        }
      }
      h3 {
        font-size: 14px;
        small {
          font-size: 12px;
          color: #ccc;
          font-weight: normal;
        }
        &.active {
          color: red;
        }
      }
    }

    #screencast-view-container {
      position: absolute;
      border-left: 1px solid #d0d0d0;
      top: 37px;
      left: 300px;
      right: 0;
      bottom: 0;
      background-color: #fff;
      min-height: 700px;
      padding: 5px 25px;
      #player {
        border: 1px solid #000;
        max-width: 800px;
      }
    }
    h1 {
      padding-top: 30px;
    }
  • Khởi tạo service và thêm service vào trong controller:

    app/assets/javascripts/angular/services/screencast.js.coffee

    App.factory "Screencast", ["$resource", ($resource) ->
        $resource "/api/screencasts/:id", id: "@id"
    ]

app/assets/javascripts/controllers/screencasts_ctrl.js.coffee:

    App.controller "ScreencastsCtrl", ["$scope", "Screencast", ($scope, Screencast) ->
      $scope.screencasts = Screencast.query()
    ]
  • Hiển thị tất cả các record lên view:

    app/views/home/index.html.erb:

<div ng-controller="ScreencastsCtrl">
  <div id="screencast-list-container">
    <ul>
      <li ng-repeat="screencast in screencasts">
        <h3>
          {{screencast.title}}
          <small>
            ({{screencast.duration}})
          </small>
        </h3>
      </li>
    </ul>
  </div>

  <div id="screencast-view-container">
    <h2>{{selectedScreencast.title}}</h2>
    <p>{{selectedScreencast.summary}}</p>
    <div flow-player="" id="player"></div>
    <p>
      Published at {{selectedScreencast.published_at | date: "mediumDate"}}
      - <a ng-href="{{selectedScreencast.link}}">{{selectedScreencast.link}}</a>
    </p>
  </div>
</div>
  • Thêm vào trong Controller action show khi chọn một video nào đó:
    App.controller "ScreencastsCtrl", ["$scope", "Screencast", ($scope, Screencast) ->
      $scope.screencasts = Screencast.query()
      $scope.selectedScreencast = null

      $scope.showScreencast = (screencast) ->
        $scope.selectedScreencast = screencast
    ]
  • Hiển thị video sử dụng Flowplayer và JQuery:

    app/views/layouts/application.html.erb:

......
    <%= yield %>
    <script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <script src="//releases.flowplayer.org/5.5.0/flowplayer.min.js"></script>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.6/angular.min.js"></script>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.6/angular-resource.min.js"></script>
<%= javascript_include_tag 'application'%>
.....
  • Thêm Flowplayer vào trong directives folder:
    App.directive "flowPlayer", ->
      (scope, element, attrs) ->
        scope.$watch "selectedScreencast", (screencast) ->
          if screencast
            element.flowplayer
              playlist: [[mp4: screencast.video_url]]
              ratio: 9/14
  • Thêm vào trong phần hiển thị chi tiết của video để có thể chạy video:
.............
      <div id="screencast-view-container">
        <h2>{{selectedScreencast.title}}</h2>
        <p>{{selectedScreencast.summary}}</p>
        <div flow-player="" id="player"></div>
        <p>
          Published at {{selectedScreencast.published_at | date: "mediumDate"}}
          - <a ng-href="{{selectedScreencast.link}}">{{selectedScreencast.link}}</a>
        </p>
      </div>
.............

=> Refresh browser and enjoy Railscast from your application!

3. Kết luận

Việc sử dụng các Javascript framework như AngularJS hay EmberJS để xây dựng View đang là một xu thế tất yếu của Web hiện đại: Lớn hơn, nhanh hơn, tương tác phong phú hơn. Do phía client-side đã được tối ưu dựa trên nền Javascript nên server không phải tốn quá nhiều tài nguyên cho việc thực hiện và quản lý view. Thêm vào đó, việc sử dụng JSON data một cách linh hoạt giúp chúng ta có thể dễ dàng tương tác với các thư viện Javascript khác thiên về UI/UX như DHTMLX.