AngularJS on Rails 4.1.5 - Part 1

Angular seems to be the big craze as of late. Some may agree and some may not, but AngularJS is one of the next big contenders for being the number one choice of developers. At the time of writing this article, AngularJS is the 12th most watched project on GitHub.

Here I want to create a useful Rails application using Angular. The goal is to have a single-page application which allows us to select a screencast link on the left and view it on the right. An example of this would be found at

Originally I had presented this topic at our local ruby users group. My typical workflow is to write a blog post before presenting and have that post be a reference to my presentation. Since then, I have received a lot of feedback on how I could have enhanced the app. These posts (part 1 and 2) been re-written to reflect those changes. Special thanks goes to Tad Thorley for providing the excellent example application based off of the original. Also thanks goes out to those who have commented on these posts.


开始这个项目的时候我很难决定,是使用成熟Rails框架,还是用像 Sinatra非常经量级Ruby的web框架。 我也尝试使用Rails::API折中的方案(详看 Railscast)。最终,我用还是用Rails(4.1.5版)。 这个给我想要的灵活性,而且本文的范围也不想偏离在Rails应用中怎么样使用Angular。

那就开始编程之旅吧。我们先创建Rails应用,名字就叫 Angular Casts

$ rails new angular_casts ... $ cd angular_casts


我们的应用将会非常简单,存储视频信息在数据库中。所以用轻量级的SQLite数据库,也是Rails默认自带的。如果你不熟悉这个,你可以访问 了解更多信息。


  • title: 视频的标题
  • summary: 视频的摘要
  • duration: 视频的时间
  • link: 视频的原链接
  • published: 视频的发布时间
  • source: 视频的来源

我让咱们基于上面信息的创建Model与Controller,同时需要添加 video_url 字段,用来在咱们应用程序中播放Railscasts的视频。

$ rails g resource screencast title summary:text duration link published_at:datetime source video_url

通过上面 resource 脚本生成了一个Model与一个Controller,这个Controller将提供我们需要REST风格的API。同时我们也会看screencasts 的Controller路由已经自动添加到 config/routes.rb中了:

``` ruby config/routes.rb AngularCasts::Application.routes.draw do resources :screencasts ... end


    $ rake db:migrate; rake db:migrate RAILS_ENV=test

## 测试模型

###### 测试在此不是主要话题,因此就蜻蜓点水而过,如果你想学习更多关于测试知识,我推荐[](。

为了保证咱们的代码 健壮性、易维护性,我们必须添加代码测试。


    $ rake test

如果返回的结果是**0 tests, 0 assertions, 0 failures, 0 errors, 0 skips**。这说明测试功能正常,但还没有写任何一个测试用例代码。


* 确保每个视频信息中的必要数据都存在。
* 确保我们视频的惟一性,也就是说没有重复的视频信息。


``` yaml test/fixtures/screencasts.yml
# Read about fixtures at

  title: "Fast Rails Commands"
  summary: "Rails commands, such as generators, migrations, and tests, have a tendency to be slow because they need to load the Rails app each time. Here I show three tools to make this faster: Zeus, Spring, and Commands."
  duration: "8:06"
  link: ""
  published_at: "Thu, 04 Apr 2013 00:00:00 -0700"
  source: "railscasts"
  video_url: ""

  title: "Wizard Forms with Wicked"
  summary: "Creating a wizard form can be tricky in Rails. Learn how Wicked can help by turning a controller into a series of multiple steps."
  duration: "11:57"
  link: ""
  published_at: "Thu, 03 May 2012 00:00:00 -0700"
  source: "railscasts"
  video_url: ""

  title: "Sending HTML Email"
  summary: "HTML email can be difficult to code because any CSS should be made inline. Here I present a few tools for doing this including the premailer-rails3 and roadie gems."
  duration: "5:42"
  link: ""
  published_at: "Mon, 02 Jan 2012 00:00:00 -0800"
  source: "railscasts"
  video_url: ""

打开自动生成的文件test/models/screencast_test.rb 并添加些测试用例。如果你是使用Rails 3.x的话,相对应测试文件的是test/unit/screencast_test.rb。

``` ruby test/models/screencast_test.rb require 'test_helper'

class ScreencastTest < ActiveSupport::TestCase setup do @screencast_defaults = { title: 'Facebook Authentication', summary: 'This will show how to create a new facebook application and configure it. Then add some authentication with the omniauth-facebook gem and top it off with a client-side authentication using the JavaScript SDK.', duration: '12:09', link: '', published_at: Date.parse('Mon, 25 Jun 2012 00:00:00 -0700'), source: 'railscasts', video_url: '' } end

test "should be invalid if missing required data" do screencast = assert !screencast.valid? [:title, :summary, :duration, :link, :published_at, :source, :video_url].each do |field_name| assert screencast.errors.keys.include? field_name end end

test "should be valid if required data exists" do screencast = assert screencast.valid? end

test "should only allow one screencast with the same video url" do screencast = screencast.video_url = screencasts(:fast_rails_commands).video_url assert !screencast.valid? assert screencast.errors[:video_url].include? "has already been taken" end end

测试写完,就让我再次运行 `rake test`。

``` bash
$ rake test
Run options: --seed 29768

# Running tests:


Finished tests in 0.048601s, 61.7271 tests/s, 61.7271 assertions/s.

  1) Failure:
ScreencastTest#test_should_be_invalid_if_missing_required_data [../angular_casts/test/models/screencast_test.rb:18]:
Failed assertion, no message given.

  2) Failure:
ScreencastTest#test_should_only_allow_one_screencast_with_the_same_video_url [../angular_casts/test/models/screencast_test.rb:32]:
Failed assertion, no message given.

3 tests, 3 assertions, 2 failures, 0 errors, 0 skips

你可能看到我们有3个组测试用例,3个断言和2个失败的测试用例。测试失败的原因是存在同相的数据 “should be valid if required data exists”,也就是说model中应该做加一个校验,使数据保证唯一性。


``` ruby 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


## 导入视频的数据

因为我们要从Railscasts的feeds中导入相关的信息,所以我们需要一个能够解析feeds的库。在此咱们择选[feedjira](注:feedzirra作者把这个gem改名为feedjira了)!咱们把feedjira添加到Gemfile中吧,并把*turbolinks*, *jbuilder* and *sdoc*从Gemfile中去掉。同时也删除掉 *jquery-rails* ,因为我们将使用[CDN](的jquery,而不合使用Asset Pipeline中的jquery,这将是第2部分进一步解释。

``` ruby Gemfile
source ''

gem 'rails', '4.1.5'
gem 'sqlite3'
gem 'sass-rails', '~> 4.0.3'
gem 'uglifier', '>= 1.3.0'
gem 'coffee-rails', '~> 4.0.0'

gem 'spring',        group: :development

gem 'feedjira' 


$ bundle install


在这里咱们创建一个简单的ruby类,用 feedjira的gem进行feed.xml数据抓取并解析保存添加到数据库中。

让我们开始创建一个新ScreencastImporter类,并将下面的代码复制粘贴到 lib/screencast_importer.rb中。

``` ruby lib/screencast_importer.rb require 'feedjira'

class ScreencastImporter def self.import_railscasts

# because the Railscasts feed is targeted at itunes, there is additional metadata that # is not collected by Feedzirra by default. By using add_common_feed_entry_element, # we can let Feedzirra know how to map those values. See more information at # Feedjira::Feed.add_common_feed_entry_element(:enclosure, :value => :url, :as => :video_url) Feedjira::Feed.add_common_feed_entry_element('itunes:duration', :as => :duration)

# Capture the feed and iterate over each entry feed = Feedjira::Feed.fetch_and_parse("") feed.entries.each do |entry|

# Strip out the episode number from the title title = entry.title.gsub(/#\d+\s/, '')

# Find or create the screencast data into our database 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' # set this manually ) end

# Return the number of total screencasts for the source Screencast.where(source: 'railscasts').count end end

注:17-18行是正则是去掉标题中的数字,如:“#412 Fast Rails Commands”正则以后将变成“Fast Rails Commands”。详细请[Rubular](中的调试正则表达式结果。

现在让我在rails console中试一试,刚刚写的导入的代码吧。

    $ rails c
    Loading development environment (Rails 4.1.5)

    >> require 'screencast_importer'
    => true

    >> ScreencastImporter.import_railscasts
    .... lots ... of ... feedback ....
    => 346

    >> Screencast.count
    => 346

###### 截止到现在Railscasts已经346个免费开放的视频。这个数字将会随着时间而增加的。

### 通过rake进行导入数据

用的rake的命令来代替,rails console手工导入数据吧。让我们创建一个数据导入的rake脚本。如果对rake不熟的话,点击查看[rake tasks](进入文档.

``` ruby lib/tasks/screencast_sync.rake
require 'screencast_importer'

namespace :screencast_sync do
  desc 'sync all missing screencasts from'
  task :railscasts => :environment do
    total = ScreencastImporter.import_railscasts
    puts "There are now #{total} screencasts from"


$ rake screencast_sync:railscasts There are now 345 screencasts from

rake 跑完了,导入成功了,让们继续往下吧。




  • /screencasts.json - 返回所有的screencasts数据

  • /screencasts/ID.json - 通过screencasts对应的ID,返回对应的单条数据。

咱们之前用使 resource 生成了,Controller对应的路由(routes)



``` ruby config/routes.rb AngularCasts::Application.routes.draw do scope :api do get "/screencasts(.:format)" => "screencasts#index" get "/screencasts/:id(.:format)" => "screencasts#show" end end

运行命令 `rake routes` 看下路由(routes)的变化。

    $ rake routes
    Prefix Verb URI Pattern                    Controller#Action
     GET /api/screencasts(.:format)     screencasts#index
     GET /api/screencasts/:id(.:format) screencasts#show


``` ruby app/controllers/screencast_controller.rb 
class ScreencastsController < ApplicationController
  # GET /screencasts
  # GET /screencasts.json
  def index
    render json: Screencast.all

  # GET /screencasts/:id
  # GET /screencasts/:id.json
  def show
    render json: Screencast.find(params[:id])

Rails启动起来,让我打开这个链接http://localhost:3000/api/screencasts.json。如果一切顺利,就可以看到JSON数据了。 同时也可以打开http://localhost:3000/api/screencasts/1.json 这个JSON链接,并且里头只有一组Screencast类的数据

``` javascript http://localhost:3000/api/screencasts/1.json { "id": 1, "title": "Upgrading to Rails 4", "summary": "With the release of Rails 4.0.0.rc1 it's time to try it out and report any bugs. Here I walk you through the steps to upgrade a Rails 3.2 application to Rails 4.", "duration": "12:44", "link": "", "published_at": "2013-05-06T07:00:00.000Z", "source": "railscasts", "video_url": "", "created_at": "2013-05-21T18:22:29.719Z", "updated_at": "2013-05-21T18:22:29.719Z" }

## 测试API

为了让程序稳健,API接口的测试当然必不可少的。这部份的测试不像看起来那么复杂的。I am not going to go over much explanation beyond the inline comments.

新建integration测试文件 *test/integration/api_screencasts_test.rb*.

``` ruby test/integration/api_screencasts_test.rb
require 'test_helper'

class ApiScreencastsTest < ActionDispatch::IntegrationTest
  test "get /api/screencasts.json" do
    get "/api/screencasts.json"
    assert_response :success
    assert body == Screencast.all.to_json
    screencasts = JSON.parse(response.body)
    assert screencasts.size == 3 # because there are three fixtures (see screencasts.yml)
    assert screencasts.any? { |s| s["title"] == screencasts(:fast_rails_commands).title }

  test "get /api/screencasts/:id" do
    screencast = screencasts(:fast_rails_commands)
    get "/api/screencasts/#{}.json"
    assert_response :success
    assert body == screencast.to_json
    assert JSON.parse(response.body)["title"] == screencast.title


$ rake test ... 5 tests, 18 assertions, 0 failures, 0 errors, 0 skips


Rails API部份先到这里吧!下一步就我们就把AngularJS与现在的rails应用结果在一起。