What’s rails?
Rails 是使用 Ruby 语言编写的网页程序开发框架,目的是为开发者提供常用组件,简化网页程序的开发。只需编写较少的代码,就能实现其他编程语言或框架难以企及的功能。经验丰富的 Rails 程序员会发现,Rails 让程序开发变得更有乐趣。
1.安装 Rails
执行命令,确认是否已经安装 Ruby 和 Sqlite3。Mac 默认安装了 Ruby,很多类 Unix 系统都自带了版本尚新的 SQLite3。
$ ruby -v
ruby 2.0.0p481 (2014-05-08 revision 45883) [universal.x86_64-darwin14]
$ sqlite3 --version
3.8.5 2014-08-15 22:37:57 c8ade949d4a2eb3bba4702a4a0e17b405e9b6ace
然后执行命令安装 rails :
$ sudo gem install rails
检查所有软件是否都正确安装了,可以执行下面的命令:
rails --version
如果显示的结果类似“Rails 4.2.1”,那么就可以继续往下读了。
1.1新建 Blog
执行命令:
$rails new blog
这个命令会在文件夹 blog 中新建一个 Rails 程序,然后执行 bundle install 命令安装 Gemfile 中列出的 gem。
注意的是把 Gemfile 里面的第一行改成 http://ruby.taobao.org。
生成 blog 程序后,进入该文件夹:
$ cd blog

1.2启动服务器
在 blog 文件夹中执行下面的命令:
$ rails server
会看到

上述命令会启动 WEBrick,这是 Ruby 内置的服务器。要查看程序,请打开一个浏览器窗口,访问 http://localhost:3000。应该会看到默认的 Rails 信息页面:

来到这里,整个搭建流程就基本成功搭建完成了。停止服务器,请在命令行中按 Ctrl+C 键。
2.显示“Hello, Rails!”
要在 Rails 中显示 “Hello, Rails!” ,需要新建一个控制器和视图。
控制器用来接受向程序发起的请求。路由决定哪个控制器会接受到这个请求。一般情况下,每个控制器都有多个路由,对应不同的动作。动作用来提供视图中需要的数据。
视图的作用是,以人类能看懂的格式显示数据。有一点要特别注意,数据是在控制器中获取的,而不是在视图中。视图只是把数据显示出来。默认情况下,视图使用 eRuby(嵌入式 Ruby)语言编写,经由 Rails 解析后,再发送给用户。
控制器可用控制器生成器创建,你要告诉生成器,我想要个名为 “welcome” 的控制器和一个名为 “index” 的动作,如下所示:
$ rails generate controller welcome index
运行上述命令后,Rails 会生成很多文件,以及一个路由。

在这些文件中,最重要的当然是控制器,位于 app/controllers/welcome_controller.rb,以及视图,位于 app/views/welcome/index.html.erb。
使用文本编辑器打开 app/views/welcome/index.html.erb 文件,删除全部内容,写入下面这行代码:
<h1>Hello, Rails!</h1>
2.1设置首页
在编辑器中打开 config/routes.rb 文件。
这是程序的路由文件,使用特殊的 DSL( domain-specific language,领域专属语言)编写,告知 Rails 请求应该发往哪个控制器和动作。文件中有很多注释,举例说明如何定义路由。其中有一行说明了如何指定控制器和动作设置网站的根路由。找到以 root 开头的代码行,去掉注释,变成这样:

root 'welcome#index' 告知 Rails,访问程序的根路径时,交给 welcome控制器中的 index 动作处理。get 'welcome/index' 告知 Rails,访问 http://localhost:3000/welcome/index 时,交给 welcome 控制器中的 index动作处理。get 'welcome/index' 是运行 rails generate controller welcome index 时生成的。
打开服务器后,就能看到:

3开始使用
3.1 添加资源
Rails 提供了一个 resources 方法,可以声明一个符合 REST 架构的资源。创建文章资源后,config/routes.rb 文件的内容如下:
Rails.application.routes.draw do
resources :articles
root 'welcome#index'
end
执行 rake routes 任务,会看到定义了所有标准的 REST 动作。输出结果中各列的意义稍后会说明,现在只要留意 article 的单复数形式,这在 Rails 中有特殊的含义。

程序中要有个页面用来新建文章。一个比较好的选择是 /articles/new。这个路由前面已经定义了,可以访问。打开 http://localhost:3000/articles/new ,会看到如下的路由错误:

产生这个错误的原因是,没有定义用来处理该请求的控制器。解决这个问题的方法很简单:创建名为 ArticlesController 的控制器。执行下面的命令即可:
$ rails g controller articles
如下图:

现在刷新 http://localhost:3000/articles/new,会看到一个新错误:

手动创建动作只需在控制器中定义一个新方法。打开 app/controllers/articles_controller.rb 文件,在 ArticlesController 类中,定义 new方法,如下所示:
class ArticlesController < ApplicationController
def new
end
end
在 ArticlesController 中定义 new 方法后,再刷新 http://localhost:3000/articles/new,看到的还是个错误:

错误信息解释:
第一部分说明找不到哪个模板,这里,丢失的是 articles/new 模板。Rails 首先会寻找这个模板,如果找不到,再找名为 application/new 的模板。之所以这么找,是因为 ArticlesController 继承自 ApplicationController。
后面一部分是个 Hash。:locale 表示要找哪国语言模板,默认是英语("en")。:format 表示响应使用的模板格式,默认为 :html,所以 Rails 要寻找一个 HTML 模板。:handlers 表示用来处理模板的程序,HTML 模板一般使用 :erb,XML 模板使用 :builder,:coffee 用来把 CoffeeScript 转换成 JavaScript。
让这个程序正常运行,最简单的一种模板是 app/views/articles/new.html.erb。新建文件 app/views/articles/new.html.erb,写入如下代码:
<h1>New Article</h1>
3.1创建表单
要在模板中编写表单,可以使用“表单构造器”。Rails 中常用的表单构造器是 form_for。在 app/views/articles/new.html.erb 文件中加入以下代码:
<%= form_for :article do |f| %>
<p>
<%= f.label :title %><br>
<%= f.text_field :title %>
</p>
<p>
<%= f.label :text %><br>
<%= f.text_area :text %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
显示结果如下:

但是这个表单还有个问题。如果查看这个页面的源码,会发现表单 action 属性的值是 /articles/new。这就是问题所在,因为其指向的地址就是现在这个页面,而这个页面是用来显示新建文章表单的。
要想转到其他地址,就要使用其他的地址。这个问题可使用 form_for 方法的 :url 选项解决。在 Rails 中,用来处理新建资源表单提交数据的动作是 create,所以表单应该转向这个动作。
修改 app/views/articles/new.html.erb 文件中的 form_for,改成这样:
<%= form_for :article, url: articles_path do |f| %>
3.2创建文章
在 ArticlesController 类中定义 create 方法。在 app/controllers/articles_controller.rb 文件中 new 方法后面添加以下代码:
class ArticlesController < ApplicationController
def new
end
def create
render plain: params[:article].inspect
end
end
render 方法接受一个简单的 Hash 为参数,这个 Hash 的键是 plain,对应的值为 params[:article].inspect。params 方法表示通过表单提交的参数,返回 ActiveSupport::HashWithIndifferentAccess 对象,可以使用字符串或者 Symbol 获取键对应的值。
3.3创建 Article 模型
在 Rails 中,模型的名字使用单数,对应的数据表名使用复数。
创建模型,执行命令:
$ rails generate model Article title:string text:text
如下图:

这个命令告知 Rails,我们要创建 Article 模型,以及一个字符串属性 title 和文本属性 text。这两个属性会自动添加到 articles 数据表中,映射到 Article 模型。执行这个命令后,Rails 会生成一堆文件。现在我们只关注 app/models/article.rb 和 db/migrate/20150528013459_create_articles.rb(你得到的文件名可能有点不一样)这两个文件。后者用来创建数据库结构。
3.4运动迁移
执行命令:
$ rake db:migrate
Rails 会执行迁移操作,告诉你创建了 articles 表。

3.5在控制器中保存数据
打开 app/controllers/articles_controller.rb文件,把 create 动作修改成这样:
def create
@article = Article.new(params[:article])
@article.save
redirect_to @article
end
在 Rails 中,每个模型可以使用各自的属性初始化,自动映射到数据库字段上。create 动作中的第一行就是这个目的(还记得吗,params[:article] 就是我们要获取的属性)。@article.save 的作用是把模型保存到数据库中。保存完后转向 show 动作。稍后再编写 show 动作。
再次访问 http://localhost:3000/articles/new,填写表单提交后,出现下面错误:

Rails 提供了很多安全防范措施保证程序的安全,你所看到的错误就是因为违反了其中一个措施。这个防范措施叫做“健壮参数”,我们要明确地告知 Rails 哪些参数可在控制器中使用。这里,我们想使用 title 和 text 参数。请把 create 动作修成成:
def create
@article = Article.new(article_params)
@article.save
redirect_to @article
end
private
def article_params
params.require(:article).permit(:title, :text)
end
3.6显示文章
在 rake routes 的输出中看到,show 动作的路由是:
article GET /articles/:id(.:format) articles#show
:id 的意思是,路由期望接收一个名为 id 的参数,在这个例子中,就是文章的 ID。
在 app/controllers/articles_controller.rb 文件中添加 show 动作,以及相应的视图文件。
def show
@article = Article.find(params[:id])
end
注意的是,我们调用 Article.find 方法查找想查看的文章,传入的参数 params[:id] 会从请求中获取 :id 参数。我们还把文章对象存储在一个实例变量中(以 @ 开头的变量),只有这样,变量才能在视图中使用。
然后,新建 app/views/articles/show.html.erb 文件,写入下面的代码:
<p>
<strong>Title:</strong>
<%= @article.title %>
</p>
<p>
<strong>Text:</strong>
<%= @article.text %>
</p>
3.7列出所有文章
列出所有文章,对应的路由是:
articles GET /articles(.:format) articles#index
在 app/controllers/articles_controller.rb 文件中,为ArticlesController 控制器添加 index 动作:
def index
@articles = Article.all
end
然后编写这个动作的视图,保存为 app/views/articles/index.html.erb。
3.8添加链接
打开 app/views/welcome/index.html.erb 文件,改成这样:
<h1>Hello, Rails!</h1>
<%= link_to 'My Blog', controller: 'articles' %>
link_to 是 Rails 内置的视图帮助方法之一,根据提供的文本和地址创建超链接。这上面这段代码中,地址是文章列表页面。
接下来添加到其他页面的链接。先在 app/views/articles/index.html.erb 中添加 “New Article” 链接,放在 <table>标签之前:
<%= link_to 'New article', new_article_path %>
点击这个链接后,会转向新建文章的表单页面。
然后在 app/views/articles/new.html.erb 中添加一个链接,位于表单下面,返回到 index 动作:
<%= form_for :article do |f| %>
...
<% end %>
<%= link_to 'Back', articles_path %>
最后,在 app/views/articles/show.html.erb 模板中添加一个链接,返回 index 动作,这样用户查看某篇文章后就可以返回文章列表页面了:
<p>
<strong>Title:</strong>
<%= @article.title %>
</p>
<p>
<strong>Text:</strong>
<%= @article.text %>
</p>
<%= link_to 'Back', articles_path %>
如下图显示:


3.9更新文章
首先,要在 ArticlesController 中添加 edit 动作:
def edit
@article = Article.find(params[:id])
end
视图中要添加一个类似新建文章的表单。新建 app/views/articles/edit.html.erb 文件,写入下面的代码:
<h1>Editing article</h1>
<%= form_for :article, url: article_path(@article), method: :patch do |f| %>
<% if @article.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@article.errors.count, "error") %> prohibited
this article from being saved:</h2>
<ul>
<% @article.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<p>
<%= f.label :title %><br>
<%= f.text_field :title %>
</p>
<p>
<%= f.label :text %><br>
<%= f.text_area :text %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
<%= link_to 'Back', articles_path %>
method: :patch 选项告诉 Rails,提交这个表单时使用 PATCH 方法发送请求。根据 REST 架构,更新资源时要使用 HTTP PATCH方法。
form_for 的第一个参数可以是对象,例如 @article,把对象中的字段填入表单。如果传入一个和实例变量(@article)同名的 Symbol(:article),效果也是一样。
然后,要在 app/controllers/articles_controller.rb 中添加 update 动作:
def update
@article = Article.find(params[:id])
if @article.update(article_params)
redirect_to @article
else
render 'edit'
end
end
private
def article_params
params.require(:article).permit(:title, :text)
end
新定义的 update 方法用来处理对现有文章的更新操作,接收一个 Hash,包含想要修改的属性。和之前一样,如果更新文章出错了,要再次显示表单。
最后,我们想在文章列表页面,在每篇文章后面都加上一个链接,指向 edit 动作。打开 app/views/articles/index.html.erb 文件,在 Show 链接后面添加Edit 链接:
<table>
<tr>
<th>Title</th>
<th>Text</th>
<th colspan="2"></th>
</tr>
<% @articles.each do |article| %>
<tr>
<td><%= article.title %></td>
<td><%= article.text %></td>
<td><%= link_to 'Show', article_path(article) %></td>
<td><%= link_to 'Edit', edit_article_path(article) %></td>
</tr>
<% end %>
</table>
还要在 app/views/articles/show.html.erb 模板的底部加上 Edit 链接:
...
<%= link_to 'Back', articles_path %>
<%= link_to 'Edit', edit_article_path(@article) %>
3.10删除文章
从数据库中删除文章。按照 REST 架构的约定,删除文章的路由是:
DELETE /articles/:id(.:format) articles#destroy
删除资源使用 DELETE 方法,路由会把请求发往 app/controllers/articles_controller.rb 中的 destroy 动作。destroy 动作现在还不存在,下面来添加:
def destroy
@article = Article.find(params[:id])
@article.destroy
redirect_to articles_path
end
最后,在 index 动作的模板 (app/views/articles/index.html.erb)中加上 Destroy 链接:
<h1>Listing Articles</h1>
<%= link_to 'New article', new_article_path %>
<table>
<tr>
<th>Title</th>
<th>Text</th>
<th colspan="3"></th>
</tr>
<% @articles.each do |article| %>
<tr>
<td><%= article.title %></td>
<td><%= article.text %></td>
<td><%= link_to 'Show', article_path(article) %></td>
<td><%= link_to 'Edit', edit_article_path(article) %></td>
<td><%= link_to 'Destroy', article_path(article),
method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</table>
如下图显示:

3.11添加数据验证
打开 app/models/article.rb 文件,修改成:
class Article < ActiveRecord::Base
validates :title, presence: true,
length: { minimum: 5 }
end
添加的这段代码可以确保每篇文章都有一个标题,而且至少有五个字符。
添加数据验证后,如果把不满足验证条件的文章传递给 @article.save,会返回 false。打开 app/controllers/articles_controller.rb 文件,会发现,我们还没在 create 动作中检查 @article.save 的返回结果。如果保存失败,应该再次显示表单。为了实现这种功能,请打开 app/controllers/articles_controller.rb 文件,把 new 和 create 动作改成:
打开 app/controllers/articles_controller.rb 文件,把 new 和 create 动作改成:
def new
@article = Article.new
end
def create
@article = Article.new(article_params)
if @article.save
redirect_to @article
else
render 'new'
end
end
private
def article_params
params.require(:article).permit(:title, :text)
end
在 app/views/articles/new.html.erb文件中检测错误消息:
<%= form_for :article, url: articles_path do |f| %>
<% if @article.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@article.errors.count, "error") %> prohibited
this article from being saved:</h2>
<ul>
<% @article.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<p>
<%= f.label :title %><br>
<%= f.text_field :title %>
</p>
<p>
<%= f.label :text %><br>
<%= f.text_area :text %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
<%= link_to 'Back', articles_path %>
再次访问 http://localhost:3000/articles/new,尝试发布一篇没有标题的文章,会看到一个很有用的错误提示。

4添加评论模型
4.1生成模型
在终端执行下面的命令:
$ rails generate model Comment commenter:string body:text article:references
如下图:

然后运行迁移,执行命令:
$ rake db:migrate
Rails 相当智能,只会执行还没有运行的迁移,在命令行中会看到以下输出:

4.2模型关联
使用 Active Record 关联可以轻易的建立两个模型之间的关系。评论和文章之间的关联是这样的:
- 评论属于一篇文章
- 一篇文章有多个评论
我们要编辑 app/models/article.rb 文件,加入这层关系的另一端:
class Article < ActiveRecord::Base
has_many :comments
validates :title, presence: true,
length: { minimum: 5 }
end
4.3添加评论的路由
打开 config/routes.rb 文件,按照下面的方式修改:
resources :articles do
resources :comments
end
4.4生成控制器
$ rails generate controller Comments
这个命令生成六个文件和一个空文件夹:

评论发布后,会转向文章显示页面,查看自己的评论是否显示出来了。所以,CommentsController 中要定义新建评论的和删除垃圾评论的方法。
首先,修改显示文章的模板 (app/views/articles/show.html.erb),允许读者发布评论:
<p>
<strong>Title:</strong>
<%= @article.title %>
</p>
<p>
<strong>Text:</strong>
<%= @article.text %>
</p>
<h2>Add a comment:</h2>
<%= form_for([@article, @article.comments.build]) do |f| %>
<p>
<%= f.label :commenter %><br>
<%= f.text_field :commenter %>
</p>
<p>
<%= f.label :body %><br>
<%= f.text_area :body %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
<%= link_to 'Back', articles_path %>
| <%= link_to 'Edit', edit_article_path(@article) %>


5重构
5.1渲染局部视图中的集合
看一下 app/views/articles/show.html.erb 模板,内容太多。下面使用局部视图重构。
新建 app/views/comments/_comment.html.erb 文件,写入下面的代码:
<p>
<strong>Commenter:</strong>
<%= comment.commenter %>
</p>
<p>
<strong>Comment:</strong>
<%= comment.body %>
</p>
然后把 app/views/articles/show.html.erb 修改成:
<p>
<strong>Title:</strong>
<%= @article.title %>
</p>
<p>
<strong>Text:</strong>
<%= @article.text %>
</p>
<h2>Comments</h2>
<%= render @article.comments %>
<h2>Add a comment:</h2>
<%= form_for([@article, @article.comments.build]) do |f| %>
<p>
<%= f.label :commenter %><br>
<%= f.text_field :commenter %>
</p>
<p>
<%= f.label :body %><br>
<%= f.text_area :body %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
<%= link_to 'Edit Article', edit_article_path(@article) %> |
<%= link_to 'Back to Articles', articles_path %>
这个视图会使用局部视图 app/views/comments/_comment.html.erb 渲染 @article.comments 集合中的每个评论。render 方法会遍历 @article.comments 集合,把每个评论赋值给一个和局部视图同名的本地变量,在这个例子中本地变量是 comment,这个本地变量可以在局部视图中使用。
5.2渲染局部视图中的表单
新建 app/views/comments/_form.html.erb 文件,写入:
<%= form_for([@article, @article.comments.build]) do |f| %>
<p>
<%= f.label :commenter %><br>
<%= f.text_field :commenter %>
</p>
<p>
<%= f.label :body %><br>
<%= f.text_area :body %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
然后把 app/views/articles/show.html.erb 改成:
<p>
<strong>Title:</strong>
<%= @article.title %>
</p>
<p>
<strong>Text:</strong>
<%= @article.text %>
</p>
<h2>Comments</h2>
<%= render @article.comments %>
<h2>Add a comment:</h2>
<%= render "comments/form" %>
<%= link_to 'Edit Article', edit_article_path(@article) %> |
<%= link_to 'Back to Articles', articles_path %>
第二个 render 方法的参数就是要渲染的局部视图,即 comments/form。Rails 很智能,能解析其中的斜线,知道要渲染 app/views/comments 文件夹中的 _form.html.erb 模板。
@article 变量在所有局部视图中都可使用,因为它是实例变量。
5.3删除评论
博客还有一个重要的功能是删除垃圾评论。为了实现这个功能,要在视图中添加一个连接,并在 CommentsController 中定义 destroy 动作。
先在 app/views/comments/_comment.html.erb 局部视图中加入删除评论的链接:
<p>
<strong>Commenter:</strong>
<%= comment.commenter %>
</p>
<p>
<strong>Comment:</strong>
<%= comment.body %>
</p>
<p>
<%= link_to 'Destroy Comment', [comment.article, comment],
method: :delete,
data: { confirm: 'Are you sure?' } %>
</p>
点击 Destroy Comment 链接后,会向 CommentsController 控制器发起 DELETE /articles/:article_id/comments/:id 请求。我们可以从这个请求中找到要删除的评论。下面在控制器中加入 destroy 动作 (app/controllers/comments_controller.rb):
class CommentsController < ApplicationController
def create
@article = Article.find(params[:article_id])
@comment = @article.comments.create(comment_params)
redirect_to article_path(@article)
end
def destroy
@article = Article.find(params[:article_id])
@comment = @article.comments.find(params[:id])
@comment.destroy
redirect_to article_path(@article)
end
private
def comment_params
params.require(:comment).permit(:commenter, :body)
end
end
destroy 动作先查找当前文章,然后在 @article.comments 集合中找到对应的评论,将其从数据库中删掉,最后转向显示文章的页面。
5.4删除关联对象
如果删除一篇文章,也要删除文章中的评论,不然这些评论会占用数据库空间。在 Rails 中可以在关联中指定 dependent 选项达到这一目的。把 Article模型(app/models/article.rb)修改成:
class Article < ActiveRecord::Base
has_many :comments, dependent: :destroy
validates :title, presence: true,
length: { minimum: 5 }
end
6安全认证
Rails 提供了一种简单的 HTTP 身份认证机制可以避免出现这种情况。
在 ArticlesController 中,我们要用一种方法禁止未通过认证的用户访问其中几个动作。我们需要的是 http_basic_authenticate_with方法,通过这个方法的认证后才能访问所请求的动作。
要使用这个身份认证机制,需要在 ArticlesController 控制器的顶部调用 http_basic_authenticate_with 方法。除了 index 和 show 动作,访问其他动作都要通过认证,所以在 app/controllers/articles_controller.rb 中,要这么做:
class ArticlesController < ApplicationController
http_basic_authenticate_with name: "dhh", password: "secret", except: [:index, :show]
def index
@articles = Article.all
end
# snipped for brevity
同时,我们还希望只有通过认证的用户才能删除评论。修改 CommentsController 控制器(app/controllers/comments_controller.rb):
class CommentsController < ApplicationController
http_basic_authenticate_with name: "dhh", password: "secret", only: :destroy
def create
@article = Article.find(params[:article_id])
...
end
# snipped for brevity
现在,如果想新建文章,会看到一个 HTTP 基本认证对话框。

总结
通过利用 Ruby on Rails 搭建一个简单的博客系统,学习到 Ruby 这门语言该怎么用,对 Rails 也有了一个深刻的实际理解。加油,继续努力!