Mục đích của tài liệu này là trình bày các best practices và style
cho
việc phát triển ứng dụng với Rails.
Một vài mục chỉ áp dụng được với Rails 4.0+.
Bản hướng dẫn này khuyến nghị những phương án tốt nhất, do đó mọi người có thể đọc, hiểu, bảo trì code của nhau dễ dàng hơn. Những style này đều lấy từ nhu cầu thực tế, và nó sẽ giúp cuộc sống lập trình viên dễ thở hơn.
Những luật đã cũ rồi thì tôi không đề cập đến trong này, và hầu hết các luật đều dựa trên những hiểu biết của tôi trong lĩnh vực lập trình phần mềm chuyên nghiệp. Những phản hồi, góp ý, đóng góp luôn được hoan nghênh.
- Configuration
- Routing
- Controllers
- Models
- Migrations
- Views
- Internationalization
- Assets
- Mailers
- Active Support Core Extensions
- Time
- Bundler
- Managing processes
-
Những
custom initialization
thì đặt ở thư mụcconfig/initializers
. Những code này sẽ được load một lần duy nhất khi khởi động rails app. [link] -
Mỗi
initialization code
của từnggem
khác nhau thì nên đặt ở từng file khác nhau, và đặt tên tương ứng với tên gem, vd:carrierwave.rb
,active_admin.rb
... [link] -
Tạo từng file cấu hình cho mỗi môi trường riêng biệt (
development, test hay production
chẳng hạn), và đặt vào các thư mục tương ứng:config/environments/production
[link] -
Đánh dấu những assets để tiền biên dịch (precompilation) (nếu có):
# 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 )
-
k Những config mà dùng chung cho tất cả các môi trường thì đặt trong
config/application.rb
. [link] -
Nên có môi trường
staging
gần như làproduction
để phục vụ test trên môi trường thật. [link] -
Những config bổ sung thì viết vào các file
YAML
và đặt trong thư mụcconfig/
. [link]Từ Rails 4.2, các file config
YAML
có thể load ra bằng cách :Rails::Application.config_for(:yaml_file)
-
Nếu muốn thêm
action
vàoRESTful
resource (cân nhắc kỹ có thực sự cần hay không) dùngmember
vàcollection
routes. [link]# 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
-
Nếu cần định nghĩa nhiều
member/collection
routes, dùng block. [link]resources :subscriptions do member do get 'unsubscribe' # more routes end end resources :photos do collection do get 'search' # more routes end end
-
Dùng routes lồng nhau (nested routes) để thể hiện tốt hơn quan hệ giữa các ActiveRecord models. [link]
class Post < ActiveRecord::Base has_many :comments end class Comments < ActiveRecord::Base belongs_to :post end # routes.rb resources :posts do resources :comments end
-
Nếu lồng nhiều hơn 1 cấp, sử dụng tùy chọn
shallow: true
.resources :posts, shallow: true do resources :comments do resources :versions end end
-
Dùng
namespace
routes để nhóm các actions có liên quan. [link]namespace :admin do # Directs /admin/products/* to Admin::ProductsController # (app/controllers/admin/products_controller.rb) resources :products end
-
Không dùng route thuần cho controller. Nó sẽ làm cho tất cả các
action
trong cáccontroller
có thể truy cập được thông quaGET
request. [link]# very bad match ':controller(/:action(/:id(.:format)))'
-
Không dùng
match
để khai báo route trừ khi cần thiết để map các action vào với nhau[:get, :post, :patch, :put, :delete]
vào mộtaction
duy nhất với tùy chọn:via
. [link]
-
Giữ cho
controller
được "mỏng" - chỉ nên lấy dữ liệu choView
và không nên chứa bất kỳbusiness logic
nào.business logic
thì nên để trongmodel
hoặc trongservice
[link] -
Ngoài khởi tạo hay tìm kiếm, mỗi
controller action
nên gọi một phương thức. [link] -
Giữa
controller
vàview
chỉ nên dùng chung tối đa hai biến toàn cục [link]
-
Ưu tiên sử dụng
template
hơn làinline rendering
[link]
# 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/foo_controller.rb
class ProductsController < ApplicationController
def index
render :index
end
end
-
Ưu tiên
render plain:
hơnrender text:
. [link]
# 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!'
...
- Ưu tiên corresponding symbols cho HTTP status codes. Nó dễ hiểu hơn là dùng số cứng. [link]
# bad
...
render status: 500
...
# good
...
render status: :forbidden
...
-
Có thể tạo non-ActiveRecord model thỏa mái. [link]
-
Tên model nên đặt tên ngắn gọn, có nghĩa và không dùng từ viết tắt. [link]
-
Nếu cần model vẫn hỗ trợ các hành vi của ActiveRecord (vd: validation) mà không có
ActiveRecord database
, dùng ActiveAttr gem. [link]class Message include ActiveAttr::Model attribute :name attribute :email attribute :content attribute :priority attr_accessible :name, :email, :content 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
Xem thêm: RailsCast on the subject.
-
Unless they have some meaning in the business domain, don't put methods in your model that just format your data (like code generating HTML). These methods are most likely going to be called from the view layer only, so their place is in helpers. Keep your models for business logic and data-persistence only. [link]
-
Tránh thay thế các giá trị mặc định của ActiveRecord (như: tên bảng, khóa chính...) trừ khi có lý cho chính đáng. [link]
# bad - don't do this if you can modify the schema class Transaction < ActiveRecord::Base self.table_name = 'order' ... end
-
Nhóm các khối phương thưc
macro-style
(has_many
,validates
...) vào đầu lớp. [link]class User < ActiveRecord::Base # 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 # Rails4+ enums after attr macros, prefer the hash syntax enum gender: { female: 0, male: 1 } # 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
-
Ưu tiên dùng
has_many :through
hơnhas_and_belongs_to_many
. Dùnghas_many :through
cho phép ta có thêm các thuộc tính và các bước xác thực trongjoin model
. [link]# not so good - using has_and_belongs_to_many class User < ActiveRecord::Base has_and_belongs_to_many :groups end class Group < ActiveRecord::Base has_and_belongs_to_many :users end # preferred way - using has_many :through class User < ActiveRecord::Base has_many :memberships has_many :groups, through: :memberships end class Membership < ActiveRecord::Base belongs_to :user belongs_to :group end class Group < ActiveRecord::Base has_many :memberships has_many :users, through: :memberships end
-
Ưu tiên
self[:attribute]
hơnread_attribute(:attribute)
. [link]# bad def amount read_attribute(:amount) * 100 end # good def amount self[:amount] * 100 end
-
Ưu tiên
self[:attribute] = value
hơnwrite_attribute(:attribute, value)
. [link]# bad def amount write_attribute(:amount, 100) end # good def amount self[:amount] = 100 end
-
Luôn dùng "sexy" validations. [link]
# bad validates_presence_of :email validates_length_of :email, maximum: 100 # good validates :email, presence: true, length: { maximum: 100 }
-
Khi một
custom validation
mà dùng nhiều hơn mộtvalidation
khác, hoặc nó làregex mapping
thì nên tạo mộtcustom validator file
. [link]# 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
-
custom validators
đặt ở thư mụcapp/validators
. [link] -
Nếu
custom validators
được dùng đi dùng lại ở nhiều dự án thì nên đưa nó vàogem
. [link] -
Scope được phép dùng thỏa mái. [link]
class User < ActiveRecord::Base scope :active, -> { where(active: true) } scope :inactive, -> { where(active: false) } scope :with_orders, -> { joins(:orders).select('distinct(users.id)') } end
-
Khi scope quá phức tạp, nên chuyển sang
class method
và trả về mộtActiveRecord::Relation
[link]class User < ActiveRecord::Base def self.with_orders joins(:orders).select('distinct(users.id)') end end
-
Cẩn thận với phương thức
update_attribute
. Nó không gọimodel validations
(unlikeupdate_attributes
) và có thể sẽ làm lỗi model. [link] -
Dùng URL thân thiên, dễ đọc với người dùng. Thay vì chỉ show mình
id
trên URL, hãy show thêm vài thông tin có ích. Có nhiều cách để làm việc này: [link]-
Override phương thức
to_param
của model. Phương thức này được Rails dùng mặc định khi sinh URL cho đối tượng. Mặc định nó trả vềid
.class Person def to_param "#{id} #{name}".parameterize end end
Phương thức
parameterize
nên được gọi từ một String. Nên đặtid
ở đầu đểActiveRecord
có thể dùng được phương thứcfind
để gọi nó.-
Dùng
friendly_id
gem.class Person extend FriendlyId friendly_id :name, use: :slugged end
Xem thêm gem documentation.
-
-
Dùng
find_each
để duyệt qua một danh sách cácActiveRecord
. Lặp qua mộtcollection
các record từ CSDL (vd: dùng phương thứcall
) rất kém hiệu quả, nguyên do là mỗi lần nó sẽ tạo ra một đối tượng. Thay vào đó, hãy dùngfind_each
để gom chúng vào và làm một lần. [link]# 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
-
Từ khi Rails creates callbacks for dependent associations ra đời, luôn gọi
before_destroy
callbacks cùng vớiprepend: true
để validate. [link]# bad (roles will be deleted automatically even if super_admin? is true) has_many :roles, dependent: :destroy before_destroy :ensure_deletable def ensure_deletable fail "Cannot delete super admin." if super_admin? end # good has_many :roles, dependent: :destroy before_destroy :ensure_deletable, prepend: true def ensure_deletable fail "Cannot delete super admin." if super_admin? end
-
Định nghĩa tùy chọn
dependent
cho quan hệhas_many
vàhas_one
. [link]# bad class Post < ActiveRecord::Base has_many :comments end # good class Post < ActiveRecord::Base has_many :comments, dependent: :destroy end
-
Tránh nhúng biến vào String của câu truy vấn, nó sẽ dễ dẫn đến SQL injection. [link]
# bad - param will be interpolated unescaped Client.where("orders_count = #{params[:orders]}") # good - param will be properly escaped Client.where('orders_count = ?', params[:orders])
-
Nếu câu truy vấn có nhiều hơn một tham số, ưu tiên dùng phương án tham-số-là-tên (named placeholders) thay vì dùng
?
. Việc này sẽ giúp bạn tránh việc nhầm lẫn, sai sót khi các tham số không được đặt đúng thứ tự. [link]# okish Client.where( 'created_at >= ? AND created_at <= ?', params[:start_date], params[:end_date] ) # good Client.where( 'created_at >= :start_date AND created_at <= :end_date', start_date: params[:start_date], end_date: params[:end_date] )
-
Khi cần query một record bằng
id
, ưu tiên dùngfind
hơnwhere
[link]# bad User.where(id: id).take # good User.find(id)
-
Khi cần query một record bằng một vài attribute, ưu tiên dùng
find_by
hơnwhere
vàfind_by_attribute
. [link]# bad User.where(first_name: 'Bruce', last_name: 'Wayne').first # bad User.find_by_first_name_and_last_name('Bruce', 'Wayne') # good User.find_by(first_name: 'Bruce', last_name: 'Wayne')
-
Ưu tiên dùng
where.not
hơn là SQL thuần. [link]# bad User.where("id != ?", id) # good User.where.not(id: id)
-
Khi dùng query trực tiếp bằng
find_by_sql
, dùng heredocs cùng vớisquish
. Bạn sẽ format code theo SQL tốt hơn (xuống dòng, thụt đầu dòng, tô màu từ khóa) và được hiển thị tốt ở nhiều tools như GitHub, Atom, and RubyMine. [link]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
sẽ tự bỏ thụt đầu dòng hay ký hiệu xuống dòng, nên khi log ra sẽ rất khó đọc, kiểu như này:SELECT\n users.id, accounts.plan\n FROM\n users\n INNER JOIN\n acounts\n ON\n accounts.user_id = users.id
-
Đưa
schema.rb
(hoặcstructure.sql
) vào version control system (git, hg...). [link] -
Dùng
rake db:schema:load
thay vìrake db:migrate
để khởi tạo CSDL rỗng. [link] -
Khi cần tạo dữ liệu mặc định, ưu tiên tạo trong migration hơn là trong code (tầng application). [link]
# bad - application enforced default value def amount self[:amount] or 0 end
Mặc dù rất nhiều Rails dev khuyến nghị tạo dữ liệu mặc định trong code, tuy nhiên, điều này rất phiêu. Ví dụ như trường hợp nhiều app nhỏ nhỏ thường dùng chung một CSDL, sẽ dẫn đến việc mất tính toàn vẹn dữ liệu.
-
Dùng khóa ngoại (
foreign-key constraints
) để đảm bảo tính toàn vẹn dữ liệu. Ở Rails 4.2, ActiveRecord đã hỗ trợ foreign key constraints natively. [link] -
Khi viết các
migration
kiểu khởi tạobảng
, thêmcột
, dùngchange
thay vìup
vàdown
. Bởi vì trong trường hợp này thìRails
nó tự biết đểrevert, rollback
rồi. [link]# 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
-
Không dùng
model
trong migration. Vìmodel
có thể sẽ thay đổi trong tương lai (đổi tên, xóa phương thức...) [link]
-
Không gọi
model
trực tiếp trongview
. [link] -
Không xử lý logic phức tạp trong
view
, chuyển nó sangmodel
hoặchelper
. [link] -
Dùng
partial
để tránh lặp lại code (DRY). [link]
-
Không nên đặt các
string
,setting
trongmodel
,view
haycontroller
. Nên dùngI18n
để trong các file config và đặt ở thư mụcconfig/locales
. [link] -
Dùng
activerecord
scope để dịch cáclabel
của ActiveRecord. [link]en: activerecord: models: user: Member attributes: user: name: 'Full name'
Gọi
User.model_name.human
sẽ trả về "Member" vàUser.human_attribute_name("name")
sẽ trả về "Full name". -
Sử dụng hai thư mục
locales/models
vàlocales/views
để đặt các file tương ứng. [link]-
Lưu ý là thư mục
locales
cần được khai báo trongapplication.rb
để nó được load khi chạy app.# config/application.rb config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')]
-
-
Những option dùng chung, ví dụ như ngày tháng hay đơn vị...trong thư mục gốc của
locales
. [link] -
Dùng
I18n.t
thay choI18n.translate
vàI18n.l
thay choI18n.localize
. [link] -
Dùng
"lazy" lookup
cho text dùng trongviews
. Ví dụ ta có cấu trúc sau: [link]en: users: show: title: 'User details page'
Trong file
app/views/users/show.html.haml
, thay vì gọiusers.show.title
ta có thể dùng:= t '.title'
-
Dùng kiểu phân cấp bằng dấu chấm thay vì dùng
scope
, dễ đọc hơn. [link]# bad I18n.t :record_invalid, scope: [:activerecord, :errors, :messages] # good I18n.t 'activerecord.errors.messages.record_invalid'
Dùng assets pipeline
để tổ chức assets
.
-
Để giành thư mục
app/assets
cho các custom stylesheets, javascripts, hay images. [link] -
Dùng thư mục
lib/assets
cho cácassets
của thư viện. [link] -
Các thư viện bên thứ ba như jQuery hay bootstrap thì đặt ở thư mục
vendor/assets
. [link] -
Dùng
gemified versions
của assets khi có thể (vd: jquery-rails, jquery-ui-rails, bootstrap-sass, zurb-foundation). [link]
-
Đặt tên
SomethingMailer
sẽ giúp rõ nghĩa hơn về chức năng của nó, vàview
nào sẽ được sử dụng. [link] -
Dùng cả
HTML
vàplain-text
trongview templates
. [link] -
Bật tùy chọn thông báo lỗi khi gửi email thất bại, dùng trong môi trường
development
, mặc định là nó bị tắt. [link]# config/environments/development.rb config.action_mailer.raise_delivery_errors = true
-
Dùng một
local SMTP server
, chẳng hạn Mailcatcher trong môi trườngdevelopment
. [link]# config/environments/development.rb config.action_mailer.smtp_settings = { address: 'localhost', port: 1025, # more settings }
-
Cài đặt các thông số mặc định cho host name. [link]
# 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'
-
Nếu muốn nhúng
link
của server vàoemail
, Sử dụng phương thức_url
thay vì_path
. [link]# bad You can always find more info about this course <%= link_to 'here', course_path(@course) %> # good You can always find more info about this course <%= link_to 'here', course_url(@course) %>
-
Format
from
vàaddresses
theo mẫu: [link]# in your mailer class default from: 'Your Name <info@your_site.com>'
-
Cấu hình cho môi trường
test
: [link]# config/environments/test.rb config.action_mailer.delivery_method = :test
-
Cấu hình cho môi trường
development
vàproduction
, dùngsmtp
: [link]# config/environments/development.rb, config/environments/production.rb config.action_mailer.delivery_method = :smtp
-
Khi gửi
html email
, cácstyle
nên làminline
, vì một vàimail client
không hỗ trợ việc dùng cácstyle
ngoài. Việc này sẽ dẫn đến vấn đề là code rất khó bảo trì, và bị lặp code rất nhiều. Có 2 gem hỗ trợ khắc phục vấn đề này: premailer-rails và roadie. [link] -
Nên gửi email trong
background process
, hoặc dùng sidekiq gem. [link]
-
Ưu tiên dùng toán tử truy xuất an toàn của Ruby 2.3
&.
hơnActiveSupport#try!
. [link]
# bad
obj.try! :fly
# good
obj&.fly
-
Ưu tiên dùng các hàm trong thư viện chuẩn của Ruby hơn
các bí danh của
ActiveSupport
. [link]
# bad
'the day'.starts_with? 'th'
'the day'.ends_with? 'ay'
# good
'the day'.start_with? 'th'
'the day'.end_with? 'ay'
-
Ưu tiên dùng thư viện chuẩn của Ruby hơn các extension của
ActiveSupport
không thông dụng. [link]
# 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'
-
Ưu tiên dùng toán tử so sánh của
ActiveSupport
hơnArray#inquiry
,Numeric#inquiry
vàString#inquiry
. [link]
# 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'
# bad - Numeric#inquiry
0.positive?
0.negative?
# good
0 > 0
0 < 0
-
Cấu hình
timezone
trongapplication.rb
. [link]config.time_zone = 'Eastern European Time' # optional - note it can be only :utc or :local (default is :utc) config.active_record.default_timezone = :local
-
Không dùng
Time.parse
, thay vào đó hãy dùngTime.zone.parse
Time.parse
sẽ lấytimezone
của hệ thống. [link]# bad Time.parse('2018-10-02 19:05:37') # good Time.zone.parse('2018-10-02 19:05:37') # => Tue, 02 Oct 2018 19:05:37 JST +09:00 +02:00
-
Không dùng
Time.now
.Time.now
sẽ lấytimezone
của hệ thống. [link]# bad Time.now # good Time.zone.now # => Fri, 05 Oct 2018 10:36:40 JST +09:00 Time.current # Tương tự nhưng viết ngắn hơn
-
Nếu gem chỉ dùng trong môi trường
development
haytesting
, đặt chúng vào scope tương ứng. [link] -
Có một số gem sẽ có phiên bản khác nhau cho mỗi hệ điều hành khác nhau, trường hợp này thì đặt chúng vào
group
tương ứng:darwin
cho OS X vàlinux
cho Linux: [link]# Gemfile group :darwin do gem 'rb-fsevent' gem 'growl' end group :linux do gem 'rb-inotify' end
Để yêu cầu lập trình viên phải dev trên hệ điều hành nào đó, cấu hình nó trong file
config/application.rb
:platform = RUBY_PLATFORM.match(/(linux|darwin)/)[0].to_sym Bundler.require(platform)
-
Thêm file
Gemfile.lock
vào version control system (git, hg). Nó sẽ đảm bảo các thành viên trong team dùng chung một phiên bản của cácgem
. [link]
Dưới đây là những resources
cũng rất đáng quan tâm: