Ruby on Rails 是一个使用 Ruby 语言写的开源 Web 应用框架,它是严格按照 MVC 结构开发的。目标是努力使自身保持简单,使用最少的配置和代码。后来的 Django(Python)、Laravel(PHP)、 ChicagoBoss(Erlang)等框架都借鉴了 rails 的设计思想。Twitter、GitHub、Groupon,国内的暴走漫画、薄荷网等前期都是用 rails 作为主要的开发框架。虽然现在已经每况日下(ruby小众、性能不佳、社区活跃度低、学习门槛高…),但 rails 仍是一个了不起的框架。本文使用 rails 搭建一个类似 Meetup 的平台,体会 rails 开发的一些基础要素。

Vagrant+VirtualBox 打造跨平台开发环境

Vagrant + VirtualBox 应该是目前为止我找到的安装 ubuntu 虚拟机最简单的方式。在 rails 项目中使用 vagrant 搭建开发环境最直观的好处有三个:

  • 快。vagrant 重新封包后的 box 很小(远小于 Vmware 克隆出来的虚拟机),因此安装 ubuntu 不到10分钟完成。
  • 共享文件夹。我可以用在 mac 使用 sublime+git 开发,同时在 ubuntu 虚拟机中运行 rails+mysql。二者同步。
  • 方便协作。与队友合作的时候打包一个已经配置好的 package 直接拿给队友用就可以了。免去了不同机子上折腾环境的问题,极大提高了效率。

这里有一篇详细介绍

Rails Way


rails 的根本骨架是上面的 MVC 结构。
浏览器一个请求进来(这个请求首次是由浏览器输入主页地址敲下 enter 之后,其它是从 view 中的link_path来的,比如按钮点击),首先由路由代码分发给响应的 controller 来响应该次请求(首次是返回root主页,其它都是在 controller 中定义的 action),controller 选择合适的 html 文件渲染,同时可能有对 model 层的操作,最终响应这次请求。数据放在 model 中,处理后传递给 controller。 具体可见这张图:

如网址首页 root 请求由 page controller 响应,page controller 选择 views 下的同名文件夹之下的 welcome.html (后缀一般是.html.erb )进行渲染。其余请求处理过程都是如此。

layout 解决代码重复问题

一个项目不同 html 间总有大量重复代码,如 welcome 和 about ,rails 用 layout 解决。打开本项目/layout/application.html.erb可以看到:

具体到需要复用该布局的html只要写yield中的内容即可。

Asset Pipeline

图片和css/js资源在assets下,分别由 application.js 和 application.css 统一管理。

这样在 application.html.erb 中不必再引入所有.css和.js,只要一句<%= stylesheet_link_tag "application" %> 就解决了,代码清爽了很多。多说一句,rails 自带支持 sass ,只要把后缀改为 .css.scss 就可以。

Rails 接口操作数据库

本项目用 mysql, rails 操作数据库有一套非常简便的接口。
创建数据库:

rake db:create

建表: 更改数据库的表结构,rails 给出的方法是migration

rails g migration CreateIssues

生成的数据库在 db/migrate 下:

数据库建好后就可以创建 model 了。

Partial 实现数据与模版分离

先补充一点:上面那张 MVC 的图中 controller 和 view 之间那条线是可以传递数据的。这也是 controller 读取 model 层数据交给 view 层进行显示的方式。

主页下边需要显示 issues 列表,包括 issue 的标题、评论数、评论数目等属性。

每条 issue 都从 controller 中添加数据和样式显然不合适。rails 的解决方案是partials ,把页面中固定的某个模块抽取出来。如本例中的_issue_list.html.erb 用来管理 issue_list 。具体步骤:先创建数据库并创建对应的 model,请求传来的时候,page_controller 读取数据库内容, partial 定义 model 的显示逻辑,html 用 render 插入 partial ,数据库的内容便呈现给用户了。rails 中的 partials 和 layout 机制都为简化 html 代码,优化架构发挥了很大作用。

CURD

读取展示

  • 先在model层为issues添加content,对issues表执行add_column操作。
  • 添加路由, get ‘/issues/:id’ => “issues#show”
  • generator 自动创建 controller,同时可以生成对应的 show 模版。controller中定义show action,view 中定义好样式。read 操作完成。

删除资源

  • 先从 view 入手,定义删除按钮和链接。
  • 添加路由delete 'issues/:id' => 'issues#destroy'
  • controller 中实现destroy方法(对 model 的操作)。

Strong Parameters 防止表单攻击

本项目中允许用户发布issue,是通过表单实现的。具体方法是 Issue 的create方法。

def create  
  Issue.create(params[:issue])
  redirect_to :root
end

这样可以向数据库中写入数据,但直接这么提交会报错。

ActiveModel::ForbiddenAttributesError

这个是 rails 为了防止坏人通过表单提交攻击网站,而采用的自我保护机制。如果不加说明坏蛋们就可以在params[:issue] 中人为植入其他的参数,比如admin: true这样就可以给他自己管理员权限了,所以必须要你自己指明哪些字段是允许直接用 params 里的参数来修改的。
rails3 创建或更新 Active Record 对象时,Model 中需要列一个白名单,声明哪些属性可以被 parameter 的数据更新。rails4 中用的是 Strong Parameters 的机制,Model 不再负责白名单的维护,把过滤非法属性的职责推给了 Controller。
View 层穿过来的数据会转化为一个ActionController::Parameters对象
过滤老的ActionController::Parameters对象,生成一个新的。

  • 只保留白名单属性
  • 实例变量 @permitted 赋为 true

只有 @permitted 为 true 时才可传入 model 层。 具体到本例: 到 issue_controller.rb 中添加:

private  
  def issue_params
    params.require(:issue).permit(:title, :content)
  end

create 方法改为:

Issue.create(issue_params)

rails 的 DRY(don’t repeat yourself) 原则

rails 简化代码有许多优秀的机制。

  • layout 抽取可复用代码。对应<%yield%>
  • partials 抽取页面中相对独立的模块。对应render。同样可起到代码复用的作用。
  • resources 集成 model 的 CURD 操作

资源间建立一对多关系

前面创建了 issues 表,接着要为每个 issue 添加 comment。 先创建 model:

rails g model comment content:text username:string email:string issue_id:integer

再创建对应的路由和 controller, controller 中定义 comment 的 create 方法。如何在/issues/show.html 中显示所有 comment? 这就涉及到在两个 model 间建立一对多的关系。

  • 确保 comments 表里面有issue_id这个字段,注意,名字一个字都不能错,因为要用它是和 issues 表产生映射关系的纽带。
  • 到 issue.rb 文件中添加has_many :comments
  • 到 comment.rb 中,添加belongs_to :issue

同理,user 和 comment/issues 各自之间的一对多关系都是这么映射的。


这样就可以直接使用issue.comments,comment.issue这样方便的表达。

def show  
   @issue = Issue.find(params[:id])
   @comments = @issue.comments
end

加密及验证

注册登录是一个网站最基本的用户管理模块。如何把注册表单中提交的明文密码进行加密存储,登录时又如何将用户输入的明文密码与加密后的密码进行匹配?rails 提供一个强大的接口解决 —— has_secure_password.

  • 在 Gemfile 里面添加 Bcrypt。
  • users 这张表里设置password_digest字段。(表明要存储的是加密后的密码)
  • User model添加has_secure_password.
  • controller 定义reate方法:
def create  
  user = User.new(user_params)
  user.save
  redirect_to :root
end

private  
  def user_params
    params.require(:user).permit!
  end

注意这里的strong parameter。使用has_secure_password方法后sign up 的行为(数据库创建新用户,密码加密后存储)就都自动完成了。至于登录时的密码匹配工作是由 authenticate 这个方法完成的(后边会看到)。 去数据库看一下:

rails c  
u = User.first  


可以看到密码是加密后的。

session

session 是 rails 中一个默认就有的方法,可以向里面存放数据,那么只要一直打开网站,那么用户存储的数据就一直存在。

规定 session 中存放user_id,怎么显示用户名?rails 中有一个约定俗成的方法current_user:

def current_user  
  @current_user ||= User.find(session[:user_id]) if session[:user_id]
end  
helper_method :current_user  

这样随时可以知道当前是否有用户在线以及在线用户信息。

记住我

session 存放的数据是临时性的,如果网站关闭了或是关机重启了,session 中的数据也就丢失了。实现remember me需要 cookie 。cookie 可以把服务器发过来的数据保留成一个本地硬盘上的一个文件。另外,rails 同样提供了一个方法叫cookies 可以方便开发者操作 cookie。
实现remember的关键代码:

cookies.permanent[:auth_token] = user.auth_token  

如果用户不打算 remember me:

cookies[:auth_token] = user.auth_token

这种效果跟 session 是一样的。
用户在 cookie 中怎么存储?
cookie 中的数据是要存到本地的,显然不能直接把用户id存起来。所以通过为每一个用户生成一串随机数,用来代表他的身份。数据库中为每个 User 添加auth token字段(base 64存储):

rails g migration AddAuthTokenToUsers auth_token:string  

生成auth_token方法:

before_create { generate_token(:auth_token) }

def generate_token(column)  
  begin
    self[column] = SecureRandom.urlsafe_base64
  end while User.exists?(column => self[column])
end  

表单验证

rails 自带的validator接口基本满足最常用的情况。user model中:

validates :name, :email, presence: true  
validates :name, :email, uniqueness: { case_sensitive: false }  

presence: true表明 name/email 都是必填项。uniqueness保证唯一性。validator 接口方法的触发时机是在向数据库中 save 之前。因此之前 user 的 create 方法中保存 cookie 前要加判断条件if user.save 。save 成功说明已经通过 validator 的验证。

国际化

rails中使用i18N实现国际化。
application.rb 设置语言类型:

config.i18n.default_locale = 'zh-CN'

书写 zh-CN.yml, 将需要翻译的词进行翻译。

优化体验

atwho

用到jquery-atwho-rails gem, comment_box 添加

基本原理就是定义一个数组,拿到页面上的所有用户名,检查有无重复。

hot keys

用到jquery.hotkeys, comment_box 添加

13对应回车键,ctrKey对应 ctrl,metaKey在 Mac 下对应 Command 键, Windows 下应该对应 Window 键。

markdown 格式支持

用到redcarpet, application_helper.rb 中:

html_safe :如果去掉,页面中刷新会出现 html 标签,这是一种安全机制,防止有人嵌入 html 代码来实现对网站的攻击。不过前面filter_html: true已经过滤掉了可能导致安全隐患的 html 标签。所以就可以放心的来添加 .html_safe来让 rails 放弃这种安全机制了。

代码高亮

用到pygemnt

权限控制

这里涉及到的只是非常简单的权限控制,包括只有登录用户(@current_user)能发布 issue,只有 issue 的作者本人可以删除或编辑
issue:

<% if current_user && current_user == @issue.user %> 

可以看出主要是通过current_user实现的,同时要设置好 user 和 issue 的一对多关系。

ActionMailer 实现 mail 服务

ActionMailer 用户注册后发送欢迎邮件。 代码写在user.save之后:

UserMailer.welcome_email(@user).deliver

生成 UserMailer 和 welcome email 方法。再修改 user mailer.rb,就跟 controller action 差不多,再写一个 view 文件--邮件正文。实现发邮件功能需要集成第三方服务Mailgun或国内的sendCloud

源码