Ruby on Rails 初探
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。