Rails 某个 Rails 5 项目走过的路

pinewong · 发布于 2017年09月07日 · 最后由 pinewong 回复于 2017年09月08日 · 836 次阅读
0967c2

最近做项目的一些小记录,有些地方可能考虑的不对,发出来一起讨论,希望大家可以一起提高😬

现在假设我们在开发一个小商店应用(以下用 Store),并且老板说可以用 Rails 5。

初始化应用

那第一步就是初始化依赖,做一些应用配置之类的,在这里记的小问题有:

PostgreSQL 中,添加 host 设置的同时,也需要设置用户名和密码

这里有 pg 登录途径的相关知识,因为有了 Unix Socket 方式,于是在保持默认安装的 pg(9.6) 配置时,应用的数据库配置文件只需要设置非常简单的几项就能正确连接数据库,但这里要注意的是,一旦在 database.yml 中设置了 host(即使是 localhost),这时候你还要设置用户名和密码才能连接,原因是 pg 此时改用用户名密码的登录方式了。

使用 dotenv 配置项目能方便项目的协作开发

这里拿出来对比的是 figaro 方案,这类方案使用的是 *.sample*.example 形式配置文件。这类配置文件需要手动将 *.sample 的最新配置更新到配置文件中,于是对于我们的 Store,如果有同事增加了一个系统标示的配置变量 app_code(这东西大家需要值都一样),他就需要第一步将该变量添加到 *.sample 文件并提交到版本库,第二步通知所有协作人员,对照 *.sample 文件更新本地的配置文件,对于部署时,也需要如此(此时通知的是运维人员)。

上面的重点其实不是这样操作较复杂,而是在 app_code 这个变量,这类变量其实不属于环境,他应该有默认值,在添加和更改时不需要通知其他非相关人员。而 dotenv 的配置形式是 .env(默认)加 .env.local, .env.production...(不同场景覆盖)形式,因此上述的 app_code 变量,我们可以写到 .env 文件中,db_user 这类一定需要配置协作人员配置的变量可以以注释形式写到 .env,并提示大家配到 env.local 中。

关于配置,上面是自己现在的想法,希望大家重重的提出我对 .sample 配置方案理解的误区,毕竟还是有这么多项目在使用这种方式😐 (我搜到的有安全问题,但我总觉的可以绕过这个问题啊)。

应用模型

初始化完后,就来到对应用模型的设计环节了。

多个模型里,结构一致、存值不同才需要单表继承

官方 Guides 中对单表继承的表述是对于两个数据结构一致但行为不同的模型,这里行为不同除了字面上不同方法外,也可以非常不同的验证、回调等。

Store 中,我们需要两种单据,借款单和还款单,这里两种单据结构基本可以理解成一致(两种单据一一对应),借款单上需要有还款单的信息,同理还款单也一致。此时,两个单据模型可以使用单表继承(共同继承一张基表),但其实这里应该需要的是一对一关联,因为两个单的数据其实是一致的,并且应该保持一致,这时候用单表继承了反而会徒增赋值一些重复字段的操作(将借款单模型的借款数据赋值到还款单的借款数据)。

persisted? (exists?) 才是判断是否创建成功最有效的方式

首先,我们要知道 Model.create 方法无论成功与否都返回了一个对象,一般情况下,我们会想到用 valid? 来判断:

user = User.create(name: "pine")
if user.valid?
  # 设想的成功情况
else
  # 失败情况
end

什么情况下不一定成功?

class One < ActiveRecord::Base
  has_many :twos
  after_create :create_twos_after_create
  def create_twos_after_create
    # Use bang method in callbacks, than it will rollback while create  two failed 
    twos.create!({})    # This will fail because lack of the column `number`
  end
end

class Two < ActiveRecord::Base
  validates :number, presence: true
end

此时 One.create 失败,但 One.create.valid? 返回 true,所以正确操作应该是(经二楼朋友提示,改用更精简的 persisted?):

# if User.exists?(user.id)
if user.persisted?
  # Success
else
  # Failed
end

这有详细解释:Ruby on Rails Active Record return value when create fails?

正确的终止回调

官方 Guides 中提到使用 throw :abort 手动终止回调一个回调链,并且给出了下面的提示:

当回调链停止后,Rails 会重新抛出除了 ActiveRecord::Rollback 和 ActiveRecord::RecordInvalid 之外的其他异常。这可能导致那些预期 save 和 update_attributes 等方法(通常返回 true 或 false )不会引发异常的代码出错。

但是经过测试,throw :abort 只在 before_ 类型的回调中能其作用,after_ 回调里,使用 throw :abort 会得到一个 UncaughtThrowError 的异常,因此,raise ActiveRecord::Rollback 应该是现在手动终止回调最佳的办法了。

回调里的异常正确处理并不是那么简单

同样使用上面 OneTwo 模型创建的例子,完整的异常的处理是怎么样的呢?大概应该是这样的:

# models
class One < ActiveRecord::Base
  has_many :twos
  after_create :create_twos_after_create
  def create_twos_after_create
    # Use bang method in callbacks, than it will rollback while create  two failed 
    twos.create!({})    # This will fail because lack of the column `number`
  rescue ActiveRecord::RecordInvalid => e  # This is exception of Two, but not One
    errors.add(:base, e)
    raise ActiveRecord::Rollback
  end
end

class Two < ActiveRecord::Base
  validates :number, presence: true
end
# controllers
class OneController < ActionController::Base
  def create
    one  One.create
    if One.exists?(one.id)
      redirect_to one
    else
      render :new
    end
  end
end
<!-- views -->
<ul>
  one.errors.full_messages.each do |message|
    <li><%= message %></li>
  end
</ul>

这样就基本完成了,上述主动捕获的 ActiveRecord::RecordInvalid 是其他模型的,上述 Guides 中提到的:

Rails 会重新抛出除了 ActiveRecord::Rollback 和 ActiveRecord::RecordInvalid 之外的其他异常

里,虽然 ActiveRecord::RecordInvalid 会被处理成回滚操作,但是 Rails 自身的处理会丢失了错误信息,因此,我们这里要捕获异常,并作记录错误信息操作,最后手动终止回调。

另外,因为这是不属于本模型的错误,我们把它加到 :base 键里。同理,如果回调中有其他可能需要终止执行的操作,也可以这样操作:

def create_twos_after_create
  # Use bang method in callbacks, than it will rollback while create  two failed 
  twos.create!({})    # This will fail because lack of the column `number`
  api_get!(...)
rescue ActiveRecord::RecordInvalid, ApiError => e  # When get exception after call API
  errors.add(:base, e)
  raise ActiveRecord::Rollback
end

开发应用

对于开发应用的过程,就有更多的小问题了:

字符串间的操作,请使用插入代替拼接

# Good
logger.info "#{object.name} - #{object.number}"
# Bad
logger.info object.name + ' - ' + object.number

上述两种方案里,后者当 object.name, object.number 出现 nil, 或其他非字符串类型值,你就会得到一个 TypeError 的异常,而第一种会帮我们尝试转化字符串,对于写 JS 多,第二种会更常用,可能需要注意一下。

正确的理解方法和变量

这个属于 Ruby 语言问题,Ruby 方法省略括号,给了我们很大方便,似乎是促使我们将方法和变量一同看待。这样使得语法精炼很多,但理解不深时也会容易造成一些误解。

先上一个案例,我现在在为 Store 中某件商品的分类调低优先级,商品 Product,分类 Category,因为业务,我们在 Product 模型中进行操作:

# models/product.rb
class Product < ActiveRecord::Base
  belongs_to :category
  def  low_category_priority(num = 1)
    # Wrong
    category.decrement(:priority, num)
    category.save
  end
end

代码就属于想当然写出来的,这里的操作是错误的,因为这样的代码写全(方法添加回括号,区分开变量和方法)其实是:

# Wrong with clear grammar
self.category().decrement(:priority, num)
self.category().save()

这样解释后,很明显,错误的原因是两步并非操纵一个对象,于是,正确的做法是引入一个变量:

# Right
category = self.category
category.decrement(:priority, num)
category.save

这里还有一个案例,我现在需要通过一个传参,清空所有参数,并且由于对 params 对象了解不够,用了最简单的赋值清空:

# controllers
def  index
  params = {} if "true" == params[:deny_all]
  ...
end

这里也是不正确的,当 "true" == params[:deny_all] 条件不成立时,params(此时是变量) 的值会被设成 nil。这里其实是 Ruby 的基础(不同于其他语言的几点地方),但有时业务一上来,可能就会没注意到这些了。具体问题是啥?我们继续进一步补全 Ruby 的语法:

def  index
  if "true" == self.params()[:deny_all]
    params = {} 
  else
    params = nil 
  end
  ...
end

这样问题就暴露的很明显了,这是 params 由方法变成变量引起的,这里正确的解决办法就不贴了,大概就是应该是去查看 params 文档,使用方法清空而非引入变量。

利用 document DOM 对动态节点绑定事件

当一个元素是用户 JS 触发插入的,我们该如何为该元素事先添加事件?

现在 Store 中有一个订单页,用户可以通过手动添加商品,这里,我们要为每一个商品项添加监听数量变动的事件,商品项中数量节点的类为 .product-count,我们应该如下操作:

// Right
$(document).on("change", ".product-count", function(e) {
  ...
});

// Wrong
$(".product-count").change(function(e) {
  ...
});

由于商品项元素是 JS 添加的,商品项中的 .product-count 元素也即属于动态节点,而 jQuery 提供了事例的方案为动态节点绑定时间,具体请参照文档:jQuery-On

为静态资源添加范围限制

Rails 默认会将所有的 css 和 js 分别打包成一个一个文件引入,为了提升程序健壮性和性能,我们要自己为专属资源添加范围限制,这里,最简单快捷的可以使用资源名化作范围。现在我要为管理模块的订单流水记录(admin/flow_records)的样式和脚本添加限制:

<!-- layouts/application.html.erb -->
<html>
<head></head>
<body class="<%= controller_path.gsub(/[\/\_]/, '-') %>"></body>
</html>
// javascripts/admin_flow_records.js
$(function() {
  if (!document.body.classList.contains('admin-flow-records')) { return }
  ...
});
/* stylesheets/admin_flow_records.scss */
body.admin-flow-records {
  ...
}

如此,资源限制就完成了,其他资源也可以快速实现限制。

待续...

共收到 6 条回复
24025

楼主写的不错, 借此提出一些我的看法

  1. valid? 是指模型验证没问题, 当然还不算创建完成, 有时我会用 persisted? 来判断是不是保存完成, 好像也可以判断 id 是否 nil 来判断.
  2. self.category().save , 方法无参数时还是建议省略括号的, 比如这里的 save 就没用括号
  3. 两步并非操纵一个对象, 我记得如果用includes(:category), 再用 self.category, 之后都能拿到同个对象, 希望有个人来告诉我我对了 😁
  4. params 的值也会变成 nil, 这里是因为原本的 params 是方法, 而这里你是变量,这里原因 ruby 变量声明提前
0967c2
24025u1450154824 回复

已修改更简洁的 persisted? ,非常感谢。

你指出的 self.category().save 的括号问题,其实意在区分是变量 category 还是方法 category 而有意加上的括号。你指出来,说明上面这些地方写的有些歧义,我已经在正文做了一些修改。

12128

category.decrement(:priority, num) category.save 感觉这里不应该出问题啊, decrement这个函数是怎么实现的?

5楼 已删除
12128

@pinewong 我试了下,感觉没问题啊,可以保存变动

0967c2

@48hour 谢谢指出,是我的问题,我有时间找找当初的问题的具体场景(可能是漏了什么限定)

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册