博日達爾·巴佐夫
榜樣很重要。
— 亞歷克斯·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 |