博日达尔·巴佐夫
榜样很重要。
— 亚历克斯·J·墨菲警官 / 机械战警
提示 | 您可以在 https://rails.rubystyle.guide 找到本指南的精美版本,其中导航功能得到了很大改进。 |
本指南的目标是为 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
提示 | 安装 gem install rouge |
该指南有以下语言版本:
日本人
俄语
提示 | RuboCop 是一个静态代码分析器 (linter) 和格式化程序,具有基于此风格指南的rubocop-rails 扩展。 |
将自定义初始化代码放入config/initializers
中。初始化程序中的代码在应用程序启动时执行。
将每个 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 实践, config.load_defaults
设置应与您的 Rails 版本匹配。
# good
config . load_defaults 6.1
避免创建除默认的development
、 test
和production
环境配置之外的其他环境配置。如果您需要类似生产的环境(例如暂存),请使用环境变量作为配置选项。
将任何其他配置保留在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)))'
不要使用match
来定义任何路由,除非需要使用:via
选项将[:get, :post, :patch, :put, :delete]
之间的多个请求类型映射到单个操作。
保持控制器精简 - 它们应该只检索视图层的数据,并且不应该包含任何业务逻辑(所有业务逻辑自然应该驻留在模型中)。
每个控制器操作应该(理想情况下)仅调用除初始查找或新方法之外的一种方法。
最小化控制器和视图之间传递的实例变量的数量。
动作过滤器选项中指定的控制器动作应该在词法范围内。为继承的操作指定的 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 状态代码的“神奇”数字。
# bad
...
render status : 403
...
# good
...
render status : :forbidden
...
自由引入非Active Record模型类。
使用有意义的(但简短的)名称来命名模型,不要使用缩写。
如果您需要支持类似 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 默认值(表名、主键等),除非您有充分的理由(例如不受您控制的数据库)。
# 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。
自由使用命名范围。
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 中显示模型的一些描述性属性,而不是其id
。实现这一目标的方法不止一种。
to_param
方法Rails 使用此方法构建对象的 URL。默认实现以字符串形式返回记录的id
。它可以被覆盖以包含另一个人类可读的属性。
class Person
def to_param
" #{ id } #{ name } " . parameterize
end
end
为了将其转换为 URL 友好的值,应在字符串上调用parameterize
。对象的id
需要位于开头,以便可以通过Active Record的find
方法找到它。
friendly_id
宝石它允许通过使用模型的一些描述性属性而不是其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 |