介绍
榜样很重要。
— 警官 Alex J. Murphy / RoboCop
本指南的目的是提供一套针对 Ruby on Rails 开发的最佳实践和风格规范。它是对现有的社区驱动的 Ruby 编码风格指南 的补充指南。
本 Rails 风格指南推荐最佳实践,以便现实世界的 Rails 程序员可以编写可由其他现实世界的 Rails 程序员维护的代码。反映现实世界使用的风格指南会被使用,而坚持一个被使用它的人拒绝的理想的风格指南则有风险根本不会被使用 - 无论它有多好。
本指南分为几个相关规则部分。我尝试添加了规则背后的理由(如果省略了,我假设它很明显)。
我并没有凭空想出所有规则 - 它们主要基于我作为专业软件工程师的丰富职业生涯、来自 Rails 社区成员的反馈和建议以及各种备受推崇的 Rails 编程资源。
注意
|
这里的一些建议仅适用于最近版本的 Rails。 |
您可以使用 AsciiDoctor PDF 生成本指南的 PDF 版本,并使用 以下命令 AsciiDoctor 生成 HTML 版本
# Generates README.pdf
asciidoctor-pdf -a allow-uri-read README.adoc
# Generates README.html
asciidoctor README.adoc
提示
|
安装
|
本指南的翻译提供以下语言版本
提示
|
RuboCop,一个静态代码分析器(linter)和格式化程序,有一个基于本风格指南的 rubocop-rails 扩展。
|
配置
配置初始化器
将自定义初始化代码放在 config/initializers
中。初始化器中的代码在应用程序启动时执行。
Gem 初始化器
将每个 gem 的初始化代码保存在一个单独的文件中,文件名与 gem 相同,例如 carrierwave.rb
、active_admin.rb
等。
开发/测试/生产配置
相应地调整开发、测试和生产环境的设置(在 config/environments/
下的相应文件中)。
标记要预编译的额外资产(如果有)。
# config/environments/production.rb
# Precompile additional assets (application.js, application.css,
#and all non-JS/CSS are already added)
config.assets.precompile += %w( rails_admin/rails_admin.css rails_admin/rails_admin.js )
应用程序配置
将适用于所有环境的配置保存在 config/application.rb
文件中。
加载 Rails 配置默认值
升级到较新的 Rails 版本时,应用程序的配置设置将保留在旧版本上。为了利用最新的推荐 Rails 实践,config.load_defaults
设置应与您的 Rails 版本匹配。
# good
config.load_defaults 6.1
类似生产环境的预发布环境
避免创建除 development
、test
和 production
默认值之外的额外环境配置。如果您需要类似生产环境的预发布环境,请使用环境变量进行配置选项。
YAML 配置
将任何额外的配置保存在 config/
目录下的 YAML 文件中。
自从 Rails 4.2 版本起,YAML 配置文件可以通过新的 `config_for` 方法轻松加载。
Rails::Application.config_for(:yaml_file)
路由
成员集合路由
当您需要为 RESTful 资源添加更多操作时(您真的需要它们吗?),请使用 `member` 和 `collection` 路由。
# bad
get 'subscriptions/:id/unsubscribe'
resources :subscriptions
# good
resources :subscriptions do
get 'unsubscribe', on: :member
end
# bad
get 'photos/search'
resources :photos
# good
resources :photos do
get 'search', on: :collection
end
多个成员集合路由
如果您需要定义多个 `member/collection` 路由,请使用替代的块语法。
resources :subscriptions do
member do
get 'unsubscribe'
# more routes
end
end
resources :photos do
collection do
get 'search'
# more routes
end
end
嵌套路由
使用嵌套路由来更好地表达 Active Record 模型之间的关系。
class Post < ApplicationRecord
has_many :comments
end
class Comment < ApplicationRecord
belongs_to :post
end
# routes.rb
resources :posts do
resources :comments
end
浅层路由
如果您需要嵌套路由超过 1 层,请使用 `shallow: true` 选项。这将为用户节省长 URL(例如 `posts/1/comments/5/versions/7/edit`)以及您节省长 URL 助手(例如 `edit_post_comment_version`)。
resources :posts, shallow: true do
resources :comments do
resources :versions
end
end
命名空间路由
使用命名空间路由来对相关操作进行分组。
namespace :admin do
# Directs /admin/products/* to Admin::ProductsController
# (app/controllers/admin/products_controller.rb)
resources :products
end
无通配符路由
切勿使用传统的通配符控制器路由。此路由将使所有控制器中的所有操作都可通过 GET 请求访问。
# very bad
match ':controller(/:action(/:id(.:format)))'
无匹配路由
除非需要使用 `:via` 选项将多个请求类型(例如 `[:get, :post, :patch, :put, :delete]`)映射到单个操作,否则不要使用 `match` 来定义任何路由。
控制器
瘦控制器
保持控制器精简 - 它们应该只检索视图层的数据,而不应该包含任何业务逻辑(所有业务逻辑都应该自然地驻留在模型中)。
单方法
每个控制器操作(理想情况下)应该只调用一个方法,除了初始的查找或新建操作。
共享实例变量
尽量减少在控制器和视图之间传递的实例变量数量。
词法作用域的动作过滤器
在 Action Filter 的选项中指定的控制器操作应在词法范围内。为继承操作指定的 ActionFilter 使其难以理解其对该操作的影响范围。
# bad
class UsersController < ApplicationController
before_action :require_login, only: :export
end
# good
class UsersController < ApplicationController
before_action :require_login, only: :export
def export
end
end
控制器:渲染
内联渲染
优先使用模板而不是内联渲染。
# very bad
class ProductsController < ApplicationController
def index
render inline: "<% products.each do |p| %><p><%= p.name %></p><% end %>", type: :erb
end
end
# good
## app/views/products/index.html.erb
<%= render partial: 'product', collection: products %>
## app/views/products/_product.html.erb
<p><%= product.name %></p>
<p><%= product.price %></p>
## app/controllers/products_controller.rb
class ProductsController < ApplicationController
def index
render :index
end
end
纯文本渲染
优先使用 render plain:
而不是 render text:
。
# bad - sets MIME type to `text/html`
...
render text: 'Ruby!'
...
# bad - requires explicit MIME type declaration
...
render text: 'Ruby!', content_type: 'text/plain'
...
# good - short and precise
...
render plain: 'Ruby!'
...
HTTP 状态码符号
优先使用 相应的符号 来代替数字 HTTP 状态码。它们是有意义的,并且对于不太了解的 HTTP 状态码来说,它们不像“魔术”数字。
# bad
...
render status: 403
...
# good
...
render status: :forbidden
...
模型
模型类
随意引入非 Active Record 模型类。
有意义的模型名称
使用有意义的(但简短的)名称来命名模型,不要使用缩写。
非 ActiveRecord 模型
如果您需要支持 ActiveRecord 类行为(如验证)但没有数据库功能的对象,请使用 ActiveModel::Model
。
class Message
include ActiveModel::Model
attr_accessor :name, :email, :content, :priority
validates :name, presence: true
validates :email, format: { with: /\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}\z/i }
validates :content, length: { maximum: 500 }
end
从 Rails 6.1 开始,您还可以使用 ActiveModel::Attributes
扩展来自 ActiveRecord 的属性 API。
class Message
include ActiveModel::Model
include ActiveModel::Attributes
attribute :name, :string
attribute :email, :string
attribute :content, :string
attribute :priority, :integer
validates :name, presence: true
validates :email, format: { with: /\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}\z/i }
validates :content, length: { maximum: 500 }
end
模型业务逻辑
除非它们在业务领域中具有某种意义,否则不要在模型中放置仅用于格式化数据的函数(如生成 HTML 代码的函数)。这些函数很可能只在视图层中被调用,因此它们应该放在助手函数中。将模型保留用于业务逻辑和数据持久化。
模型:Active Record
保留 Active Record 默认值
除非有充分的理由(例如不受您控制的数据库),否则避免更改 Active Record 默认值(表名、主键等)。
# bad - don't do this if you can modify the schema
class Transaction < ApplicationRecord
self.table_name = 'order'
...
end
始终追加到 ignored_columns
避免设置 ignored_columns
。它可能会覆盖之前的赋值,这几乎总是错误的。优先选择追加到列表而不是覆盖。
class Transaction < ApplicationRecord
# bad - it may overwrite previous assignments
self.ignored_columns = %i[legacy]
# good - the value is appended to the list
self.ignored_columns += %i[legacy]
...
end
枚举
优先使用哈希语法来定义 enum
。数组会使数据库值隐式,并且在中间插入/删除/重新排列值很可能会导致代码错误。
class Transaction < ApplicationRecord
# bad - implicit values - ordering matters
enum type: %i[credit debit]
# good - explicit values - ordering does not matter
enum type: {
credit: 0,
debit: 1
}
end
宏风格方法
将宏风格方法(has_many
、validates
等)分组放在类定义的开头。
class User < ApplicationRecord
# keep the default scope first (if any)
default_scope { where(active: true) }
# constants come up next
COLORS = %w(red green blue)
# afterwards we put attr related macros
attr_accessor :formatted_date_of_birth
attr_accessible :login, :first_name, :last_name, :email, :password
# Rails 4+ enums after attr macros
enum role: { user: 0, moderator: 1, admin: 2 }
# followed by association macros
belongs_to :country
has_many :authentications, dependent: :destroy
# and validation macros
validates :email, presence: true
validates :username, presence: true
validates :username, uniqueness: { case_sensitive: false }
validates :username, format: { with: /\A[A-Za-z][A-Za-z0-9._-]{2,19}\z/ }
validates :password, format: { with: /\A\S{8,128}\z/, allow_nil: true }
# next we have callbacks
before_save :cook
before_save :update_username_lower
# other macros (like devise's) should be placed after the callbacks
...
end
has_many :through
优先使用 has_many :through
而不是 has_and_belongs_to_many
。使用 has_many :through
可以在连接模型上添加额外的属性和验证。
# not so good - using has_and_belongs_to_many
class User < ApplicationRecord
has_and_belongs_to_many :groups
end
class Group < ApplicationRecord
has_and_belongs_to_many :users
end
# preferred way - using has_many :through
class User < ApplicationRecord
has_many :memberships
has_many :groups, through: :memberships
end
class Membership < ApplicationRecord
belongs_to :user
belongs_to :group
end
class Group < ApplicationRecord
has_many :memberships
has_many :users, through: :memberships
end
读取属性
优先使用 self[:attribute]
而不是 read_attribute(:attribute)
。
# bad
def amount
read_attribute(:amount) * 100
end
# good
def amount
self[:amount] * 100
end
写入属性
优先使用 self[:attribute] = value
而不是 write_attribute(:attribute, value)
。
# bad
def amount
write_attribute(:amount, 100)
end
# good
def amount
self[:amount] = 100
end
新式验证
始终使用 "新式" 验证.
# bad
validates_presence_of :email
validates_length_of :email, maximum: 100
# good
validates :email, presence: true, length: { maximum: 100 }
自定义验证方法
命名自定义验证方法时,请遵循以下简单规则:
-
validate :method_name
就像一个自然的语句 -
方法名称解释了它检查的内容
-
该方法通过其名称可以识别为验证方法,而不是谓词方法
# good
validate :expiration_date_cannot_be_in_the_past
validate :discount_cannot_be_greater_than_total_value
validate :ensure_same_topic_is_chosen
# also good - explicit prefix
validate :validate_birthday_in_past
validate :validate_sufficient_quantity
validate :must_have_owner_with_no_other_items
validate :must_have_shipping_units
# bad
validate :birthday_in_past
validate :owner_has_no_other_items
单属性验证
为了使验证易于阅读,请不要在每个验证中列出多个属性。
# bad
validates :email, :password, presence: true
validates :email, length: { maximum: 100 }
# good
validates :email, presence: true, length: { maximum: 100 }
validates :password, presence: true
自定义验证器文件
当自定义验证被使用多次或验证是某种正则表达式映射时,请创建一个自定义验证器文件。
# bad
class Person
validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i }
end
# good
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
record.errors[attribute] << (options[:message] || 'is not a valid email') unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
end
end
class Person
validates :email, email: true
end
应用程序验证器
将自定义验证器放在 app/validators
下。
自定义验证器 Gem
如果您维护着几个相关的应用程序或验证器足够通用,请考虑将自定义验证器提取到一个共享的 Gem 中。
命名范围
随意使用命名范围。
class User < ApplicationRecord
scope :active, -> { where(active: true) }
scope :inactive, -> { where(active: false) }
scope :with_orders, -> { joins(:orders).select('distinct(users.id)') }
end
命名范围类
当使用 lambda 和参数定义的命名范围变得过于复杂时,最好使用类方法来代替,该方法可以实现与命名范围相同的目的,并返回一个 ActiveRecord::Relation
对象。 当然,你可以用这种方式定义更简单的范围。
class User < ApplicationRecord
def self.with_orders
joins(:orders).select('distinct(users.id)')
end
end
回调顺序
按照回调执行的顺序声明回调。 有关参考,请参阅 可用回调。
# bad
class Person
after_commit :after_commit_callback
before_validation :before_validation_callback
end
# good
class Person
before_validation :before_validation_callback
after_commit :after_commit_callback
end
注意跳过模型验证
注意以下方法的行为。 它们不会运行模型验证,并且可能很容易破坏模型状态。
# bad
Article.first.decrement!(:view_count)
DiscussionBoard.decrement_counter(:post_count, 5)
Article.first.increment!(:view_count)
DiscussionBoard.increment_counter(:post_count, 5)
person.toggle :active
product.touch
Billing.update_all("category = 'authorized', author = 'David'")
user.update_attribute(:website, 'example.com')
user.update_columns(last_request_at: Time.current)
Post.update_counters 5, comment_count: -1, action_count: 1
# good
user.update_attributes(website: 'example.com')
用户友好的 URL
使用用户友好的 URL。 在 URL 中显示模型的一些描述性属性,而不是其 id
。 有多种方法可以实现这一点。
覆盖模型的 to_param
方法
Rails 使用此方法来构造指向对象的 URL。 默认实现将记录的 id
作为字符串返回。 可以覆盖它以包含另一个人类可读的属性。
class Person
def to_param
"#{id} #{name}".parameterize
end
end
为了将此转换为用户友好的值,应该在字符串上调用 parameterize
。 对象的 id
需要位于开头,以便 Active Record 的 find
方法可以找到它。
friendly_id
Gem
它允许通过使用模型的一些描述性属性而不是其 id
来创建人类可读的 URL。
class Person
extend FriendlyId
friendly_id :name, use: :slugged
end
查看 gem 文档 以获取有关其用法的更多信息。
find_each
使用 find_each
迭代 AR 对象集合。 从数据库中循环遍历记录集合(例如,使用 all
方法)非常低效,因为它会尝试一次性实例化所有对象。 在这种情况下,批处理方法允许你分批处理记录,从而大大减少内存消耗。
# bad
Person.all.each do |person|
person.do_awesome_stuff
end
Person.where('age > 21').each do |person|
person.party_all_night!
end
# good
Person.find_each do |person|
person.do_awesome_stuff
end
Person.where('age > 21').find_each do |person|
person.party_all_night!
end
before_destroy
由于 Rails 为依赖关联创建回调,因此始终使用 prepend: true
调用执行验证的 before_destroy
回调。
# bad (roles will be deleted automatically even if super_admin? is true)
has_many :roles, dependent: :destroy
before_destroy :ensure_deletable
def ensure_deletable
raise "Cannot delete super admin." if super_admin?
end
# good
has_many :roles, dependent: :destroy
before_destroy :ensure_deletable, prepend: true
def ensure_deletable
raise "Cannot delete super admin." if super_admin?
end
has_many
/has_one
依赖选项
为 has_many
和 has_one
关联定义 dependent
选项。
# bad
class Post < ApplicationRecord
has_many :comments
end
# good
class Post < ApplicationRecord
has_many :comments, dependent: :destroy
end
save!
在持久化 AR 对象时,始终使用抛出异常的 bang! 方法或处理方法返回值。这适用于 create
、save
、update
、destroy
、first_or_create
和 find_or_create_by
。
# bad
user.create(name: 'Bruce')
# bad
user.save
# good
user.create!(name: 'Bruce')
# or
bruce = user.create(name: 'Bruce')
if bruce.persisted?
...
else
...
end
# good
user.save!
# or
if user.save
...
else
...
end
模型:Active Record 查询
避免插值
避免在查询中使用字符串插值,因为它会使您的代码容易受到 SQL 注入攻击。
# bad - param will be interpolated unescaped
Client.where("orders_count = #{params[:orders]}")
# good - param will be properly escaped
Client.where('orders_count = ?', params[:orders])
命名占位符
当您的查询中有多个占位符时,请考虑使用命名占位符而不是位置占位符。
# okish
Client.where(
'orders_count >= ? AND country_code = ?',
params[:min_orders_count], params[:country_code]
)
# good
Client.where(
'orders_count >= :min_orders_count AND country_code = :country_code',
min_orders_count: params[:min_orders_count], country_code: params[:country_code]
)
find
当您需要通过主键 ID 检索单个记录并在记录未找到时引发 ActiveRecord::RecordNotFound
时,请优先使用 find
而不是 where.take!
、find_by!
和 find_by_id!
。
# bad
User.where(id: id).take!
# bad
User.find_by_id!(id)
# bad
User.find_by!(id: id)
# good
User.find(id)
find_by
当您需要通过一个或多个属性检索单个记录并在记录未找到时返回 nil
时,请优先使用 find_by
而不是 where.take
和 find_by_attribute
。
# bad
User.where(email: email).take
User.where(first_name: 'Bruce', last_name: 'Wayne').take
# bad
User.find_by_email(email)
User.find_by_first_name_and_last_name('Bruce', 'Wayne')
# good
User.find_by(email: email)
User.find_by(first_name: 'Bruce', last_name: 'Wayne')
哈希条件
优先将条件作为哈希传递给 where
和 where.not
,而不是使用 SQL 片段。
# bad
User.where("name = ?", name)
# good
User.where(name: name)
# bad
User.where("id != ?", id)
# good
User.where.not(id: id)
查找缺少的关系记录
如果您使用的是 Rails 6.1 或更高版本,请使用 where.missing 来查找缺少的关系记录。
# bad
Post.left_joins(:author).where(authors: { id: nil })
# good
Post.where.missing(:author)
按 id
排序
不要使用 id
列进行排序。尽管 ID 的顺序通常(偶然地)是按时间顺序排列的,但不能保证 ID 的顺序是按任何特定顺序排列的。使用时间戳列按时间顺序排序。作为奖励,意图更清晰。
# bad
scope :chronological, -> { order(id: :asc) }
# good
scope :chronological, -> { order(created_at: :asc) }
pluck
使用 pluck 从多个记录中选择单个值。
# bad
User.all.map(&:name)
# bad
User.all.map { |user| user[:name] }
# good
User.pluck(:name)
pick
使用 pick 从单个记录中选择单个值。
# bad
User.pluck(:name).first
# bad
User.first.name
# good
User.pick(:name)
压缩的 Heredocs
在 find_by_sql
等方法中指定显式查询时,请使用带有 squish
的 heredocs。这允许您使用换行符和缩进格式化 SQL,同时支持许多工具(包括 GitHub、Atom 和 RubyMine)中的语法高亮显示。
User.find_by_sql(<<-SQL.squish)
SELECT
users.id, accounts.plan
FROM
users
INNER JOIN
accounts
ON
accounts.user_id = users.id
# further complexities...
SQL
String#squish
会删除缩进和换行符,以便您的服务器日志显示流畅的 SQL 字符串,而不是类似这样的内容
SELECT\n users.id, accounts.plan\n FROM\n users\n INNER JOIN\n accounts\n ON\n accounts.user_id = users.id
size
优于 count
或 length
在查询 Active Record 集合时,优先使用 size
(根据集合是否已加载选择 count/length 行为)或 length
(始终加载整个集合并计算数组元素)而不是 count
(始终执行数据库查询以获取计数)。
# bad
User.count
# good
User.all.size
# good - if you really need to load all users into memory
User.all.length
使用范围
使用范围而不是使用模板为标量值定义比较条件。
# bad
User.where("created_at >= ?", 30.days.ago).where("created_at <= ?", 7.days.ago)
User.where("created_at >= ? AND created_at <= ?", 30.days.ago, 7.days.ago)
User.where("created_at >= :start AND created_at <= :end", start: 30.days.ago, end: 7.days.ago)
# good
User.where(created_at: 30.days.ago..7.days.ago)
# bad
User.where("created_at >= ?", 7.days.ago)
# good
User.where(created_at: 7.days.ago..)
# note - ranges are inclusive or exclusive of their ending, not beginning
User.where(created_at: 7.days.ago..) # produces >=
User.where(created_at: 7.days.ago...) # also produces >=
User.where(created_at: ..7.days.ago) # inclusive: produces <=
User.where(created_at: ...7.days.ago) # exclusive: produces <
# okish - there is no range syntax that would denote exclusion at the beginning of the range
Customer.where("purchases_count > :min AND purchases_count <= :max", min: 0, max: 5)
注意
|
Rails 6.0 或更高版本需要无穷范围 Ruby 2.6 语法,Rails 6.0.3 需要无始范围 Ruby 2.7 语法。 |
where.not
使用多个属性
避免将多个属性传递给 where.not
。在这种情况下,Rails 逻辑在 Rails 6.1 中发生了变化,现在将产生匹配其中任何一个条件的结果,例如 where.not(status: 'active', plan: 'basic')
将在计划为企业时返回具有活动状态的记录。
# bad
User.where.not(status: 'active', plan: 'basic')
# good
User.where.not('status = ? AND plan = ?', 'active', 'basic')
冗余的 all
使用 all
作为接收器是冗余的。在没有 all
的情况下,结果不会改变,因此应该删除它。
# bad
User.all.find(id)
User.all.order(:created_at)
users.all.where(id: ids)
user.articles.all.order(:created_at)
# good
User.find(id)
User.order(:created_at)
users.where(id: ids)
user.articles.order(:created_at)
注意
|
当 all 的接收器是关联时,有些方法的行为会因省略 all 而改变。
|
以下方法在没有 all
的情况下行为不同
因此,在考虑从这些方法的接收者中删除all
时,建议参考文档以了解行为的变化。
迁移
模式版本
将schema.rb
(或structure.sql
)置于版本控制之下。
数据库模式加载
使用rake db:schema:load
而不是rake db:migrate
来初始化空数据库。
默认迁移值
在迁移本身中强制执行默认值,而不是在应用程序层。
# bad - application enforced default value
class Product < ApplicationRecord
def amount
self[:amount] || 0
end
end
# good - database enforced
class AddDefaultAmountToProducts < ActiveRecord::Migration
def change
change_column_default :products, :amount, 0
end
end
虽然许多 Rails 开发人员建议仅在 Rails 中强制执行表默认值,但这是一种非常脆弱的方法,会使您的数据容易受到许多应用程序错误的影响。您还需要考虑大多数非平凡的应用程序与其他应用程序共享数据库,因此从 Rails 应用程序强制执行数据完整性是不可能的。
三态布尔值
对于 SQL 数据库,如果布尔列没有指定默认值,它将具有三个可能的值:true
、false
和 NULL
。布尔运算符以意想不到的方式工作与 NULL
。
例如,在 SQL 查询中,true AND NULL
为 NULL
(而不是 false),true AND NULL OR false
为 NULL
(而不是 false)。这会导致 SQL 查询返回意外的结果。
为了避免这种情况,布尔列应始终具有默认值和 NOT NULL
约束。
# bad - boolean without a default value
add_column :users, :active, :boolean
# good - boolean with a default value (`false` or `true`) and with restricted `NULL`
add_column :users, :active, :boolean, default: true, null: false
add_column :users, :admin, :boolean, default: false, null: false
外键约束
强制执行外键约束。从 Rails 4.2 开始,Active Record 本地支持外键约束。
# bad - does not add foreign keys
create_table :comment do |t|
t.references :article
t.belongs_to :user
t.integer :category_id
end
# good
create_table :comment do |t|
t.references :article, foreign_key: true
t.belongs_to :user, foreign_key: true
t.references :category, foreign_key: { to_table: :comment_categories }
end
更改与上/下
在编写构造性迁移(添加表或列)时,使用change
方法而不是up
和down
方法。
# the old way
class AddNameToPeople < ActiveRecord::Migration
def up
add_column :people, :name, :string
end
def down
remove_column :people, :name
end
end
# the new preferred way
class AddNameToPeople < ActiveRecord::Migration
def change
add_column :people, :name, :string
end
end
定义模型类迁移
如果您必须在迁移中使用模型,请确保定义它们,以避免将来出现损坏的迁移。
# db/migrate/<migration_file_name>.rb
# frozen_string_literal: true
# bad
class ModifyDefaultStatusForProducts < ActiveRecord::Migration
def change
old_status = 'pending_manual_approval'
new_status = 'pending_approval'
reversible do |dir|
dir.up do
Product.where(status: old_status).update_all(status: new_status)
change_column :products, :status, :string, default: new_status
end
dir.down do
Product.where(status: new_status).update_all(status: old_status)
change_column :products, :status, :string, default: old_status
end
end
end
end
# good
# Define `table_name` in a custom named class to make sure that you run on the
# same table you had during the creation of the migration.
# In future if you override the `Product` class and change the `table_name`,
# it won't break the migration or cause serious data corruption.
class MigrationProduct < ActiveRecord::Base
self.table_name = :products
end
class ModifyDefaultStatusForProducts < ActiveRecord::Migration
def change
old_status = 'pending_manual_approval'
new_status = 'pending_approval'
reversible do |dir|
dir.up do
MigrationProduct.where(status: old_status).update_all(status: new_status)
change_column :products, :status, :string, default: new_status
end
dir.down do
MigrationProduct.where(status: new_status).update_all(status: old_status)
change_column :products, :status, :string, default: old_status
end
end
end
end
有意义的外键命名
显式命名外键,而不是依赖 Rails 自动生成的 FK 名称。(https://guides.rubyonrails.net.cn/active_record_migrations.html#foreign-keys)
# bad
class AddFkArticlesToAuthors < ActiveRecord::Migration
def change
add_foreign_key :articles, :authors
end
end
# good
class AddFkArticlesToAuthors < ActiveRecord::Migration
def change
add_foreign_key :articles, :authors, name: :articles_author_id_fk
end
end
可逆迁移
不要在 change
方法中使用不可逆的迁移命令。可逆的迁移命令列在下面。 ActiveRecord::Migration::CommandRecorder
# bad
class DropUsers < ActiveRecord::Migration
def change
drop_table :users
end
end
# good
class DropUsers < ActiveRecord::Migration
def up
drop_table :users
end
def down
create_table :users do |t|
t.string :name
end
end
end
# good
# In this case, block will be used by create_table in rollback
# https://api.rubyonrails.net.cn/classes/ActiveRecord/ConnectionAdapters.html#method-i-drop_table
class DropUsers < ActiveRecord::Migration
def change
drop_table :users do |t|
t.string :name
end
end
end
视图
不要直接在视图中调用模型
永远不要直接从视图中调用模型层。
不要在视图中进行复杂的格式化
避免在视图中进行复杂的格式化。视图助手对于简单的情况很有用,但如果更复杂,则考虑使用装饰器或演示器。
局部模板
通过使用局部模板和布局来减少代码重复。
局部模板中不要使用实例变量
避免在局部模板中使用实例变量,而是将局部变量传递给 render
。局部模板可能在不同的控制器或操作中使用,其中变量可能具有不同的名称,甚至可能不存在。在这种情况下,未定义的实例变量不会引发异常,而局部变量会引发异常。
<!-- bad -->
<!-- app/views/courses/show.html.erb -->
<%= render 'course_description' %>
<!-- app/views/courses/_course_description.html.erb -->
<%= @course.description %>
<!-- good -->
<!-- app/views/courses/show.html.erb -->
<%= render 'course_description', course: @course %>
<!-- app/views/courses/_course_description.html.erb -->
<%= course.description %>
国际化
本地化文本
在视图、模型和控制器中不要使用字符串或其他特定于区域设置的设置。这些文本应移至 config/locales
目录中的区域设置文件。
翻译后的标签
当 Active Record 模型的标签需要翻译时,使用 activerecord
范围
en: activerecord: models: user: Member attributes: user: name: 'Full name'
然后 User.model_name.human
将返回 "Member",而 User.human_attribute_name("name")
将返回 "Full name"。这些属性的翻译将用作视图中的标签。
组织区域设置文件
将视图中使用的文本与 Active Record 属性的翻译分开。将模型的区域设置文件放在 locales/models
文件夹中,将视图中使用的文本放在 locales/views
文件夹中。
当使用额外的目录组织区域设置文件时,必须在application.rb
文件中描述这些目录才能加载它们。
# config/application.rb
config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')]
共享本地化
将共享的本地化选项(例如日期或货币格式)放在locales
目录根目录下的文件中。
简短 I18n
使用 I18n 方法的简短形式:I18n.t
而不是 I18n.translate
,以及 I18n.l
而不是 I18n.localize
。
延迟查找
从视图和控制器中使用“延迟”查找区域设置条目。假设我们有以下结构
en: users: show: title: 'User details page'
users.show.title
的值可以在模板 app/views/users/show.html.haml
中这样查找
# bad
= t 'users.show.title'
# good
= t '.title'
点分隔键
使用点分隔的区域设置键,而不是使用数组或单个符号指定:scope
选项。点分隔表示法更易于阅读和跟踪层次结构。
# bad
I18n.t :record_invalid, scope: [:activerecord, :errors, :messages]
# good
I18n.t :record_invalid, scope: 'activerecord.errors.messages'
I18n.t 'activerecord.errors.messages.record_invalid'
# bad
I18n.t :title, scope: :invitation
# good
I18n.t 'title.invitation'
资产
使用资产管道来利用应用程序中的组织结构。
保留 app/assets
保留 app/assets
用于自定义样式表、javascript 或图像。
lib/assets
将 lib/assets
用于不完全属于应用程序范围的库。
gem/assets
尽可能使用资产的 gem 化版本(例如jquery-rails、jquery-ui-rails、bootstrap-sass、zurb-foundation)。
邮件程序
邮件程序名称
将邮件程序命名为 SomethingMailer
。如果没有 Mailer 后缀,则无法立即清楚地知道什么是邮件程序以及哪些视图与邮件程序相关。
HTML 纯文本电子邮件
提供 HTML 和纯文本视图模板。
启用邮件传递错误
在开发环境中启用邮件传递失败时引发的错误。默认情况下,这些错误被禁用。
# config/environments/development.rb
config.action_mailer.raise_delivery_errors = true
本地 SMTP
在开发环境中使用本地 SMTP 服务器,例如 Mailcatcher。
# config/environments/development.rb
config.action_mailer.smtp_settings = {
address: 'localhost',
port: 1025,
# more settings
}
默认主机名
提供主机名的默认设置。
# config/environments/development.rb
config.action_mailer.default_url_options = { host: "#{local_ip}:3000" }
# config/environments/production.rb
config.action_mailer.default_url_options = { host: 'your_site.com' }
# in your mailer class
default_url_options[:host] = 'your_site.com'
电子邮件地址
正确格式化发件人和收件人地址。使用以下格式
# in your mailer class
default from: 'Your Name <info@your_site.com>'
如果您使用的是 Rails 6.1 或更高版本,可以使用 email_address_with_name
方法
# in your mailer class
default from: email_address_with_name('info@your_site.com', 'Your Name')
传递方法测试
确保测试环境的电子邮件传递方法设置为 test
# config/environments/test.rb
config.action_mailer.delivery_method = :test
传递方法 SMTP
开发和生产环境的传递方法应为 smtp
# config/environments/development.rb, config/environments/production.rb
config.action_mailer.delivery_method = :smtp
内联电子邮件样式
发送 HTML 电子邮件时,所有样式都应内联,因为某些邮件客户端无法处理外部样式。但是,这会使它们更难维护并导致代码重复。有两个类似的 gem 可以转换样式并将它们放在相应的 HTML 标签中:premailer-rails 和 roadie。
Active Support 核心扩展
Active Support 别名
优先使用 Ruby 的标准库方法而不是 ActiveSupport
别名。
# bad
'the day'.starts_with? 'th'
'the day'.ends_with? 'ay'
# good
'the day'.start_with? 'th'
'the day'.end_with? 'ay'
Active Support 扩展
优先使用 Ruby 的标准库而不是不常用的 Active Support 扩展。
# bad
(1..50).to_a.forty_two
1.in? [1, 2]
'day'.in? 'the day'
# good
(1..50).to_a[41]
[1, 2].include? 1
'the day'.include? 'day'
inquiry
优先使用 Ruby 的比较运算符而不是 Active Support 的 Array#inquiry
和 String#inquiry
。
# bad - String#inquiry
ruby = 'two'.inquiry
ruby.two?
# good
ruby = 'two'
ruby == 'two'
# bad - Array#inquiry
pets = %w(cat dog).inquiry
pets.gopher?
# good
pets = %w(cat dog)
pets.include? 'cat'
exclude?
优先使用 Active Support 的 exclude?
而不是 Ruby 的否定 include?
。
# bad
!array.include?(2)
!hash.include?(:key)
!string.include?('substring')
# good
array.exclude?(2)
hash.exclude?(:key)
string.exclude?('substring')
优先使用波浪号 heredoc 而不是 strip_heredoc
如果您使用的是 Ruby 2.3 或更高版本,优先使用波浪号 heredoc (<<~
) 而不是 Active Support 的 strip_heredoc
。
# bad
<<EOS.strip_heredoc
some text
EOS
# bad
<<-EOS.strip_heredoc
some text
EOS
# good
<<~EOS
some text
EOS
优先使用 to_fs
用于格式化字符串
如果您使用的是 Rails 7.0 或更高版本,优先使用 to_fs
而不是 to_formatted_s
。to_formatted_s
对一个经常使用的函数来说太繁琐了。
# bad
time.to_formatted_s(:db)
date.to_formatted_s(:db)
datetime.to_formatted_s(:db)
42.to_formatted_s(:human)
# good
time.to_fs(:db)
date.to_fs(:db)
datetime.to_fs(:db)
42.to_fs(:human)
时间
时区配置
在 application.rb
中相应地配置您的时区。
config.time_zone = 'Eastern European Time'
# optional - note it can be only :utc or :local (default is :utc)
config.active_record.default_timezone = :local
Time.parse
不要使用 Time.parse
。
# bad
Time.parse('2015-03-02 19:05:37') # => Will assume time string given is in the system's time zone.
# good
Time.zone.parse('2015-03-02 19:05:37') # => Mon, 02 Mar 2015 19:05:37 EET +02:00
to_time
不要使用 String#to_time
# bad - assumes time string given is in the system's time zone.
'2015-03-02 19:05:37'.to_time
# good
Time.zone.parse('2015-03-02 19:05:37') # => Mon, 02 Mar 2015 19:05:37 EET +02:00
Time.now
不要使用 Time.now
。
# bad
Time.now # => Returns system time and ignores your configured time zone.
# good
Time.zone.now # => Fri, 12 Mar 2014 22:04:47 EET +02:00
Time.current # Same thing but shorter.
优先使用 all_(day|week|month|quarter|year)
而不是日期/时间的范围
优先使用 all_(day|week|month|quarter|year)
而不是 beginning_of_(day|week|month|quarter|year)..end_of_(day|week|month|quarter|year)
来获取日期/时间的范围。
# bad
date.beginning_of_day..date.end_of_day
date.beginning_of_week..date.end_of_week
date.beginning_of_month..date.end_of_month
date.beginning_of_quarter..date.end_of_quarter
date.beginning_of_year..date.end_of_year
# good
date.all_day
date.all_week
date.all_month
date.all_quarter
date.all_year
持续时间
持续时间应用
如果未使用参数,优先使用 from_now
和 ago
而不是 since
、after
、until
或 before
。
# bad - It's not clear that the qualifier refers to the current time (which is the default parameter)
5.hours.since
5.hours.after
5.hours.before
5.hours.until
# good
5.hours.from_now
5.hours.ago
如果使用参数,优先使用 since
、after
、until
或 before
而不是 from_now
和 ago
。
# bad - It's confusing and misleading to read
2.days.from_now(yesterday)
2.days.ago(yesterday)
# good
2.days.since(yesterday)
2.days.after(yesterday)
2.days.before(yesterday)
2.days.until(yesterday)
避免对持续时间主题使用负数。始终优先使用允许使用正字面量的限定词。
# bad - It's confusing and misleading to read
-5.hours.from_now
-5.hours.ago
# good
5.hours.ago
5.hours.from_now
持续时间算术
使用持续时间方法而不是与当前时间加减。
# bad
Time.current - 1.minute
Time.zone.now + 2.days
# good
1.minute.ago
2.days.from_now
Bundler
开发/测试宝石
将仅用于开发或测试的 gem 放入 Gemfile 中的适当组。
仅使用优质 gem
在您的项目中仅使用成熟的 gem。如果您正在考虑包含一些鲜为人知的 gem,您应该首先仔细审查其源代码。
Gemfile.lock
不要从版本控制中删除 Gemfile.lock
。这不是一些随机生成的的文件 - 它确保所有团队成员在执行 bundle install
时获得相同的 gem 版本。
测试
集成测试
优先使用集成风格的控制器测试而不是功能风格的控制器测试,如 Rails 文档中所建议的那样。
# bad
class MyControllerTest < ActionController::TestCase
end
# good
class MyControllerTest < ActionDispatch::IntegrationTest
end
freeze_time
优先使用 ActiveSupport::Testing::TimeHelpers#freeze_time 而不是 ActiveSupport::Testing::TimeHelpers#travel_to,并使用当前时间作为参数。
# bad
travel_to(Time.now)
travel_to(DateTime.now)
travel_to(Time.current)
travel_to(Time.zone.now)
travel_to(Time.now.in_time_zone)
travel_to(Time.current.to_time)
# good
freeze_time
进一步阅读
有一些关于 Rails 风格的优秀资源,如果您有时间,可以考虑一下。
许可证
本作品根据 知识共享署名 3.0 通用许可协议 授权