陈斌彬的技术博客

Stay foolish,stay hungry

Mac 搭建 Ruby on Rails 博客系统(原创)

What’s rails?

Rails 是使用 Ruby 语言编写的网页程序开发框架,目的是为开发者提供常用组件,简化网页程序的开发。只需编写较少的代码,就能实现其他编程语言或框架难以企及的功能。经验丰富的 Rails 程序员会发现,Rails 让程序开发变得更有乐趣。

1.安装 Rails

执行命令,确认是否已经安装 RubySqlite3。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

img

1.2启动服务器

在 blog 文件夹中执行下面的命令:

$ rails server

会看到

img

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

img

来到这里,整个搭建流程就基本成功搭建完成了。停止服务器,请在命令行中按 Ctrl+C 键。

2.显示“Hello, Rails!”

要在 Rails 中显示 “Hello, Rails!” ,需要新建一个控制器和视图。

控制器用来接受向程序发起的请求。路由决定哪个控制器会接受到这个请求。一般情况下,每个控制器都有多个路由,对应不同的动作。动作用来提供视图中需要的数据。

视图的作用是,以人类能看懂的格式显示数据。有一点要特别注意,数据是在控制器中获取的,而不是在视图中。视图只是把数据显示出来。默认情况下,视图使用 eRuby(嵌入式 Ruby)语言编写,经由 Rails 解析后,再发送给用户。

控制器可用控制器生成器创建,你要告诉生成器,我想要个名为 “welcome” 的控制器和一个名为 “index” 的动作,如下所示:

$ rails generate controller welcome index

运行上述命令后,Rails 会生成很多文件,以及一个路由。 img

在这些文件中,最重要的当然是控制器,位于 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 文件。

这是程序的路由文件,使用特殊的 DSLdomain-specific language,领域专属语言)编写,告知 Rails 请求应该发往哪个控制器和动作。文件中有很多注释,举例说明如何定义路由。其中有一行说明了如何指定控制器和动作设置网站的根路由。找到以 root 开头的代码行,去掉注释,变成这样:

img

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 时生成的。

打开服务器后,就能看到:

img

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 中有特殊的含义。

img

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

img

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

$ rails g controller articles

如下图:

img

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

img

手动创建动作只需在控制器中定义一个新方法。打开 app/controllers/articles_controller.rb 文件,在 ArticlesController 类中,定义 new方法,如下所示:

class ArticlesController < ApplicationController
  def new
  end
end

ArticlesController 中定义 new 方法后,再刷新 http://localhost:3000/articles/new,看到的还是个错误:

img

错误信息解释:

第一部分说明找不到哪个模板,这里,丢失的是 articles/new 模板。Rails 首先会寻找这个模板,如果找不到,再找名为 application/new 的模板。之所以这么找,是因为 ArticlesController 继承自 ApplicationController

后面一部分是个 Hash:locale 表示要找哪国语言模板,默认是英语("en"):format 表示响应使用的模板格式,默认为 :html,所以 Rails 要寻找一个 HTML 模板。:handlers 表示用来处理模板的程序,HTML 模板一般使用 :erbXML 模板使用 :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 %>

显示结果如下:

img

但是这个表单还有个问题。如果查看这个页面的源码,会发现表单 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].inspectparams 方法表示通过表单提交的参数,返回 ActiveSupport::HashWithIndifferentAccess 对象,可以使用字符串或者 Symbol 获取键对应的值。

3.3创建 Article 模型

在 Rails 中,模型的名字使用单数,对应的数据表名使用复数。

创建模型,执行命令:

$ rails generate model Article title:string text:text

如下图:

img

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

3.4运动迁移

执行命令:

$ rake db:migrate

Rails 会执行迁移操作,告诉你创建了 articles 表。 img

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,填写表单提交后,出现下面错误:

img

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_toRails 内置的视图帮助方法之一,根据提供的文本和地址创建超链接。这上面这段代码中,地址是文章列表页面。

接下来添加到其他页面的链接。先在 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 %>

如下图显示:

img

img

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>

如下图显示:

img

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 文件,把 newcreate 动作改成:

打开 app/controllers/articles_controller.rb 文件,把 newcreate 动作改成:

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,尝试发布一篇没有标题的文章,会看到一个很有用的错误提示。

img

4添加评论模型

4.1生成模型

在终端执行下面的命令:

$ rails generate model Comment commenter:string body:text article:references

如下图:

img

然后运行迁移,执行命令:

$ rake db:migrate

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

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

这个命令生成六个文件和一个空文件夹:

img

评论发布后,会转向文章显示页面,查看自己的评论是否显示出来了。所以,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) %>

img

img

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/formRails 很智能,能解析其中的斜线,知道要渲染 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 方法。除了 indexshow 动作,访问其他动作都要通过认证,所以在 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 基本认证对话框。

img

总结

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

参考