本文是一个Rails新手的学习笔记,主要是对过去一个月中学习内容的总结,包括:
Agile Web Development with Rails 4
Rails 101
Rails for Zombies
水平有限, 错误再所难免(这也是我写出来的原因啦 :D), 还请诸位多多指教.
学习体验
Rails 的开发速度非常快,但学习速度是很慢的。DHH曾经提到过:相比learnability,他更看重usability。DHH的15分钟开发博客程序确实激动人心,但是不经过全面的学习,想要做出合格可用的产品是不现实的。
概述
学习Rails到底是在学习什么? 在我看来,主要是以下几个:
了解处理用户请求的完整路径
这是学习任何一个web框架,都要完成的任务。Rails相比其他框架,需要学习的点就是MVC 和 routes 文件的写法。
MVC
Rails框架强制你使用MVC模式进行开发,那什么是真正的MVC呢?那就是:
用户请求的永远是controller,服务器返回给浏览器的永远是view.
以用户注册为例,直觉的做法是用户点击首页的注册链接,转到有表格的view,用户填写信息之后submit,请求转到controller中的特定action,处理请求之后根据结果转回注册页面或者自动登录到首页。
这种做法并没有完全符合MVC,因为有通过硬编码的链接实现的view到view的跳转,Rails的做法是这样的:
点击首页的注册链接,跳转到users_controller
的 new,这个action计算用户需要填写的信息,初始化一个空的对象,转到new_html这个view,用户填写表格之后submit,请求转给create这个action,处理请求之后根据结果 redirect_to 到 new (重定向会让浏览器再次向服务器发起对controller下 new 的请求)或者自动登录到首页。
永远是 view -> controller -> view 的处理流程。
这并不是一个好例子,或许我该举一些“不在view中写业务逻辑、保持controller精简”之类的最佳实践,但是它们都被举烂了,我也就不再多说,我举得这个例子或许能让你对MVC有多一些的思考。
routes
routes.rb
这个文件无疑是任何 Rails 项目的灵魂, 我得到的建议是: 如果你打算研究一个 Rails 开源项目, 要打开的第二个文件永远是routes.rb
, 第一个当然是Gemfile
啦, :D.
这个文件的两个基本作用:
- 提供了
new_user_path
这样的写法,避免出现硬编码的链接 - 把用户地址栏的链接与
controller
中的action
相对应.
这就牵扯到了Rrails的一个重要特性: restful
. 以前写Servlet
, 经常会有/getUser?id=3
这样的链接, restful
的想法是, 既然本身已经是GET
请求了, 链接何必再有get
呢. Rails就按照restful
的思路, 把链接变成了: GET users/:id
. 在使用该链接的时候, 用users_path(@user)
这样的写法来传递id
这个参数.
restful
当然还有更"高大上"的作用, 例如分布式系统下的容错性之类的, 但是对于新手, 这样理解设计者的初衷就好了: 就是将updateUserInfo?id=3
deleteUser?id=4
这样的链接, 转成PUT users/3
DELETE users/3
.
新手经常搞不清楚path
的写法, 尤其是出现各种nested resources
的时候, 在终端输入rake routes
, 就可以看到了正确写法了.
ORM
主要学习的点包括migration
, seeds.rb
, 表关系及查询语句的编写.
migration
SQL无疑是非常好用的语言, 但有个致命的确定: 三五人的小团队, 没有专门的DBA, 如何保证所有开发者的数据库设计是一致的? 当然, 有许多的做法可以解决这个问题, 这些做法中migration是最好的之一.
它的基本思路, 就是用ruby代码来做数据库设计(增加新的表, 增加索引, 改变某列的数据类型), 每次修改都放在单独的良好命名的文件之中, 只要将其纳入版本管理, 通过rake db:migrate
命令, 就可以保证所有开发者的数据库设计师一致的.
同时借助seeds.rb
文件, 保证所有开发者的测试数据是一致的.
表关系
也就是表之间的主外键关系以及三范式的满足. 在我短暂的Rails开发旅程中, 大多数时间都花费在数据库设计上, 也使我对
程序 = 算法 + 数据结构
这一论断越来越信服.
Rails 提供了诸多功能方便开发者申明表之间的主外键关系, 包括has_many
belongs_to
has_many_through
等, 合理地利用该特性可以帮助你写出可读性非常好的代码, 例如在这样的主外键设计之下:
class Group < ActiveRecord::Base
has_many :group_users
has_many :members, :through => :group_users, :source => :user
end
class GroupUser < ActiveRecord::Base
belongs_to :user
belongs_to :group
end
class User < ActiveRecord::Base
has_many :group_users
has_many :participated_groups, :through => :group_users, :source => :group
end
可以用current_user.participated_groups
这样十分自然的语句来获取当前登录用户参加的社团.
查询语句
这一点没什么好说的, 就两点:
- 注意避免一些明显的错误, 例如N+1
- where返回的是数组,
find_by
返回的是找到的第一个值
常用gem的使用
这也是学习Rails中的很重要的一个点, 选择Rrails是为了快速开发, 那为什么不更快一点呢. devise
让你一份实现登陆与注册, 邮箱验证, 密码找回; bootstrap-rails
让你两分钟提升网站的设计感; will_paginate
让你不再操心分页的实现....
其他
当然还有许多重要的点要学习, 但是都没有上面几个那样有提纲挈领的作用. 都放到下一节当细节说吧.
tips
工具
主要就是Sublime Text
了, 有几个插件非常好用, 明显提高效率. 我装了Emmet
, ERB Snippets
和Ruby on Rails snippets
三个插件, 主要是实现
-
PE
然后tab
可以自动生成<%= -%>
-
ER
然后tab
可以自动生成<% %>
-
ul>li*2>a
然后tab
可以打出:
<li><a href=""></a></li>
<li><a href=""></a></li>
- 还有许多神奇的功能,
has_many
之类的, 大家自行查看其官网的cheat_sheet
ORM
scope的使用
scope :graveyard, where(:show_location => true , :location => 'graveyard')
add_index
给列添加索引, 以提升性能
主外键关系的一些额外属性
- 可以显示地设置外键
class Tweet < ActiveRecord::Base
has_one :location, :dependent => :destroy, :foreign_key => :tweeter_id
end
class Location < ActiveRecord::Base
belongs_to :tweet, :foreign_key => :tweeter_id
end
-
dependent: :destroy
来设置外键依赖 避免 N+1 issue
使用Zombie.includes(:brain).all
,来代替直接Zombie.all
, 然后view
中each打印zombie.brain
, 以提升性能update_attributes和update是一样的
rake的使用
参考这里 rake, 也可以使用rake -T
查看
- 使用
rake db:schema:dump
, 可以从现在的数据库创建schema文件 - 学习开源项目时, 使用
rake db:setup
. 三件事: 创建数据库, 读取schema文件, 从'seeds.rb'导入种子数据
controller
统一处理各action中的异常
- module定义在
concerns/current_cart_rb
中, 一方面是其中的方法在各个controller中共享, 其次防止其中的方法被当做action来调用. 注意该文件夹下的文件都是autoload
的, 不需要显式地写include
. - 注意是如何使用
callback
使set_cart
执行的.
class CartsController < ApplicationController
before_action :set_cart, only: [:show, :edit, :update, :destroy]
rescue_from ActiveRecord::RecordNotFound, with: :invalid_cart
def index
@carts = Cart.all
end
private
def invalid_cart
logger.error { "Attempt to access invalid cart #{params[:id]}" }
redirect_to store_path, notice: 'Invalid cart!'
end
end
module CurrentCart
extend ActiveSupport::Concern
private
def set_cart
@cart = Cart.find(session[:cart_id])
rescue ActiveRecord::RecordNotFound
@cart = Cart.create
session[:cart_id] = @cart.id
end
end
防止doble render
def new
if @cart.line_items.empty?
redirect_to store_url, notice: 'Your cart is empty.'
return
#redirect_to不会自动return
end
@order = Order.new
end
一些澄清
-before_filter
就是旧版本中的 before_action
- build
就是new
的别名
- save!
会引发RecordInvalid
异常, 而save
会返回boolean
值
- nil?
判断对象是否存在, 不存在的对象都是nil
的; empty?
判断是否为空字段,比如一个字符串是否为空串,或者一个数组中是否有值;object.blank?
相当于o
bject.nil?||object.empty?`
- 在befor类的钩子函数中,返回FALSE则不执行before后跟的操作, 此处容易出bug
model
错误信息
可以用errors.add(:base, 'Line Items present')
添加错误信息, 在controller中可以通过@line_item.errors
来获取.
view
body的class
可以通过<body class='<%= controller.controller_name %>'>
来命名body的class, 方便scss的书写.
select的实现
#view中
<%= f.select :pay_type, Order::PAYMENT_TYPES, prompt: 'Select a payment method' %>
# model中
PAYMENT_TYPES = ['Check', 'Credit Card', 'Purchase Order']
validates :pay_type, inclusion: PAYMENT_TYPES
一些澄清
-
button_to
默认是POST
请求,link_to
默认是GET
请求.