Rails学习笔记

1704 查看


本文是一个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 SnippetsRuby 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请求.