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 http://ember101.com.

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应用

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

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

$ rails new angular_casts ... $ cd angular_casts

创建Model与Controller

我们的应用将会非常简单,存储视频信息在数据库中。所以用轻量级的SQLite数据库,也是Rails默认自带的。如果你不熟悉这个,你可以访问http://guides.rubyonrails.org/getting_started.html#configuring-a-database 了解更多信息。

在创建Model这之前,我们需要确认建哪里字段,来存储对于我们是有用的信息。就此,列出几个有用的字段:

  • 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


在development与test环境中,创建数据库构结:

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

## 测试模型

###### 测试在此不是主要话题,因此就蜻蜓点水而过,如果你想学习更多关于测试知识,我推荐[http://railscasts.com/episodes/275-how-i-test](http://railscasts.com/episodes/275-how-i-test)。

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

开始运行rake测试任务,以确保测试正常运行:

    $ rake test

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

有几件事情我们需要测试:

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

在咱们写测试之前,让我们添加些测试数据到fixtures中:

``` yaml test/fixtures/screencasts.yml
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html

fast_rails_commands:
  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: "http://railscasts.com/episodes/412-fast-rails-commands"
  published_at: "Thu, 04 Apr 2013 00:00:00 -0700"
  source: "railscasts"
  video_url: "http://media.railscasts.com/assets/episodes/videos/412-fast-rails-commands.mp4"

wizard_forms_with_wicked:
  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: "http://railscasts.com/episodes/346-wizard-forms-with-wicked"
  published_at: "Thu, 03 May 2012 00:00:00 -0700"
  source: "railscasts"
  video_url: "http://media.railscasts.com/assets/episodes/videos/346-wizard-forms-with-wicked.mp4"

sending_html_emails:
  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: "http://railscasts.com/episodes/312-sending-html-email"
  published_at: "Mon, 02 Jan 2012 00:00:00 -0800"
  source: "railscasts"
  video_url: "http://media.railscasts.com/assets/episodes/videos/312-sending-html-email.mp4"

打开自动生成的文件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: 'http://railscasts.com/episodes/360-facebook-authentication', published_at: Date.parse('Mon, 25 Jun 2012 00:00:00 -0700'), source: 'railscasts', video_url: 'http://media.railscasts.com/assets/episodes/videos/360-facebook-authentication.mp4' } end

test "should be invalid if missing required data" do screencast = Screencast.new 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 = Screencast.new(@screencast_defaults) assert screencast.valid? end

test "should only allow one screencast with the same video url" do screencast = Screencast.new(@screencast_defaults) 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:

F.F

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中应该做加一个校验,使数据保证唯一性。

更新下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](https://github.com/feedjira/feedjira)(注:feedzirra作者把这个gem改名为feedjira了)!咱们把feedjira添加到Gemfile中吧,并把*turbolinks*, *jbuilder* and *sdoc*从Gemfile中去掉。同时也删除掉 *jquery-rails* ,因为我们将使用[CDN](https://en.wikipedia.org/wiki/Content_delivery_network)的jquery,而不合使用Asset Pipeline中的jquery,这将是第2部分进一步解释。

``` ruby Gemfile
source 'https://rubygems.org'

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

#注:feedzirra作者把这个gem改名为feedjira了
gem 'feedjira' 

现在安装gems:

$ 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 # http://www.ruby-doc.org/gems/docs/f/feedzirra-0.1.2/Feedzirra/Feed.html 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("http://feeds.feedburner.com/railscasts") 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](http://rubular.com/r/duWf3x2mSp)中的调试正则表达式结果。

现在让我在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](http://rake.rubyforge.org/)进入文档.

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

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

现在我们完成rake的代码了,让咱们执行以下命令:

$ rake screencast_sync:railscasts There are now 345 screencasts from Railscasts.com

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

统一URL接口前缀带用api

前端使用Angular展现,然后Angular用ajax进行调用服务端的JSON数据接口。

我们将调用下面的两个API接口完成简单功能:

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

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

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

最重要的是,我们想screencasts.json与screencasts/1.json这个接口上面加一个api如:

怎么在screencasts.json前面加一个api。根据路由命令空间规则修改如下:

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

根据上面的修改过的二个路由(routes),咱们现在对controller做些更新。如下代码:

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

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

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": "http://railscasts.com/episodes/415-upgrading-to-rails-4", "published_at": "2013-05-06T07:00:00.000Z", "source": "railscasts", "video_url": "http://media.railscasts.com/assets/episodes/videos/415-upgrading-to-rails-4.mp4", "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 }
  end

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

继续运行咱们的测试吧!

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

测试返回的结果,说明我们的测试通过了。


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

继续第二部份

Resources

Comments