本文是一个Rails新手的学习笔记,主要是对过去一个月中学习内容的总结,包括:
-
Agile Web Development with Rails 4
-
Rails 101
-
Rails for Zombies
学习体验
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啦, 😄.
这个文件的两个基本作用:
- 提供了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让你不再操心分页的实现…
其他
当然还有许多重要的点要学习, 但是都没有上面几个那样有提纲挈领的作用. 都放到下一节当细节说吧.
工具
主要就是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, 方便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请求.