├── CONTRIBUTING.md ├── README-zhCN.md ├── README-zhTW.md └── README.md /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 感謝你考慮貢獻,有幾點需要注意一下,謝謝: 2 | 3 | ## 發現新增或缺少的內容? 4 | 5 | 請從[英文原文](https://raw.github.com/bbatsov/rails-style-guide/master/README.md)拷貝至中文文件,再開始進行翻譯。 6 | 7 | 這樣省去了 markdown 格式及排版等問題。 8 | 9 | ## __在中文文章中,請在英數字前後加上一半形空格。__ 10 | 11 | No: 12 | 13 | 有1個美女愛上2個男人。 14 | 15 | Yes: 16 | 17 | 有 1 個美女愛上 2 個男人。 18 | 19 | ## 中文請用中文的標點符號。 20 | 21 | `.`、`,` 不是中文的句號跟逗號。 22 | 23 | ## 請不要更改原文或添加原文裡沒有的內容。 24 | 25 | 欲添加新內容請至[上游](https://github.com/bbatsov/rails-style-guide)提交。 26 | 27 | ## 請先預覽修改後的內容,確定沒有忘記包起來的代碼塊。 28 | 29 | ## markdown 行內代碼請與中文分開。 30 | 31 | No: 32 | 33 | Ruby2.0的`prepend`方法。 34 | 35 | Yes: 36 | 37 | Ruby 2.0 的 `prepend` 方法。 38 | 39 | ## 跟代碼有關的都要用行內代碼包起來 40 | 41 | No: 42 | 43 | 'x' 44 | 45 | Yes: 46 | 47 | `'x'` 48 | 49 | ## 避免一個 Pull Request 裡面包含多個提交。 50 | 51 | 參考:https://github.com/JuanitoFatas/ruby-style-guide/pull/13 52 | 53 | 多個提交我要手動 Merge,很難 review。 54 | 55 | 謝謝, 56 | 57 | _Juanito Fatas_ 58 | -------------------------------------------------------------------------------- /README-zhCN.md: -------------------------------------------------------------------------------- 1 | # 序幕 2 | 3 | > 榜样很重要。
4 | > -- 《机械战警》 Alex J. Murphy 警官 5 | 6 | 这份指南旨在提供一系列 Ruby on Rails 4 开发的最佳实践和风格惯例。本指南与社区驱动并制定的 [Ruby 编码风格指南](https://github.com/bbatsov/ruby-style-guide)可以互为补充。 7 | 8 | 本文中的一些建议只适用于 Rails 4.0+ 版本。 9 | 10 | 你可以使用 [Transmuter](https://github.com/TechnoGate/transmuter) 来生成本文的 PDF 或 HTML 版本。 11 | 12 | 本指南同时有以下语言的翻译版: 13 | 14 | * [英文原版](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README.md) 15 | * [繁體中文](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhTW.md) 16 | * [日语](https://github.com/satour/rails-style-guide/blob/master/README-jaJA.md) 17 | * [俄语](https://github.com/arbox/rails-style-guide/blob/master/README-ruRU.md) 18 | * [土耳其语](https://github.com/tolgaavci/rails-style-guide/blob/master/README-trTR.md) 19 | 20 | # Rails 风格指南 21 | 22 | 这份 Rails 风格指南推荐的是 Rails 的最佳实践,现实世界中的 Rails 程序员据此可以写出可维护的高质量代码。我们只说实际使用中的用法。指南再好,但里面说的过于理想化结果大家拒绝使用或者可能根本没人用,又有何意义。 23 | 24 | 本指南分为几个小节,每一小节由几条相关的规则构成。我尽力在每条规则后面说明理由(如果省略了说明,那是因为其理由显而易见)。 25 | 26 | 这些规则不是我凭空想象出来的——它们中的绝大部分来自我多年以来作为职业软件工程师的经验,来自 Rails 社区成员的反馈和建议,以及许多备受推崇的 Rails 编程资源。 27 | 28 | ## 目录 29 | 30 | * [配置](#配置) 31 | * [路由](#路由) 32 | * [控制器](#控制器) 33 | * [模型](#模型) 34 | * [ActiveRecord](#activerecord) 35 | * [ActiveRecord 查询](#activerecord-查询) 36 | * [迁移](#迁移) 37 | * [视图](#视图) 38 | * [国际化](#国际化) 39 | * [Assets](#assets) 40 | * [Mailers](#mailers) 41 | * [Time](#time) 42 | * [Bundler](#bundler) 43 | * [有缺陷的 Gem](#有缺陷的-gem) 44 | * [进程管理](#进程管理) 45 | 46 | ## 配置 47 | 48 | * 49 | 自定义的初始化代码应放在 `config/initializers` 目录下。 Initializers 目录中的代码在应用启动时被执行。 50 | [[link](#config-initializers)] 51 | 52 | * 53 | 每个 gem 的初始化代码应放在单独的文件中,并且文件名应与 gem 的名称相同。例如: `carrierwave.rb`, `active_admin.rb`。 54 | [[link](#gem-initializers)] 55 | 56 | * 57 | 相应地调整开发环境、测试环境及生产环境的配置(修改 `config/environments/` 目录下对应的文件) 58 | [[link](#dev-test-prod-configs)] 59 | 60 | * 添加需要预编译的额外静态资源文件(如果有的话): 61 | 62 | ```Ruby 63 | # config/environments/production.rb 64 | # 预编译额外的静态资源文件(application.js, application.css, 以及所有已经被加入的非 JS 或 CSS 的文件) 65 | config.assets.precompile += %w( rails_admin/rails_admin.css rails_admin/rails_admin.js ) 66 | ``` 67 | 68 | * 69 | 将所有环境下都通用的配置放在 `config/application.rb` 文件中。 70 | [[link](#app-config)] 71 | 72 | * 73 | 创建一个与生产环境高度相似的 `staging` 环境。 74 | [[link](#staging-like-prod)] 75 | 76 | * 77 | 其它配置应保存在 YAML 文件中,存放在 `config/` 目录下。 78 | [[link](#yaml-config)] 79 | 80 | 从 Rails 4.2 开始,可以通过 `config_for` 这个新方法轻松地加载 YAML 配置文件: 81 | 82 | ```Ruby 83 | Rails::Application.config_for(:yaml_file) 84 | ``` 85 | 86 | ## 路由 87 | 88 | * 89 | 当需要为一个 RESTful 资源添加动作时(你真的需要吗?),应使用 `member` 路由和 `collection` 路由。 90 | [[link](#member-collection-routes)] 91 | 92 | ```Ruby 93 | # 差 94 | get 'subscriptions/:id/unsubscribe' 95 | resources :subscriptions 96 | 97 | # 好 98 | resources :subscriptions do 99 | get 'unsubscribe', on: :member 100 | end 101 | 102 | # 差 103 | get 'photos/search' 104 | resources :photos 105 | 106 | # 好 107 | resources :photos do 108 | get 'search', on: :collection 109 | end 110 | ``` 111 | 112 | * 113 | 当需要定义多个 `member/collection` 路由时,应使用块结构。 114 | [[link](#many-member-collection-routes)] 115 | 116 | ```Ruby 117 | resources :subscriptions do 118 | member do 119 | get 'unsubscribe' 120 | # 更多路由 121 | end 122 | end 123 | 124 | resources :photos do 125 | collection do 126 | get 'search' 127 | # 更多路由 128 | end 129 | end 130 | ``` 131 | 132 | * 133 | 使用嵌套路由(nested routes),它可以更有效地表现 ActiveRecord 模型之间的关系。 134 | [[link](#nested-routes)] 135 | 136 | ```Ruby 137 | class Post < ActiveRecord::Base 138 | has_many :comments 139 | end 140 | 141 | class Comments < ActiveRecord::Base 142 | belongs_to :post 143 | end 144 | 145 | # routes.rb 146 | resources :posts do 147 | resources :comments 148 | end 149 | ``` 150 | 151 | * 152 | 使用命名空间路由来归类相关的动作。 153 | [[link](#namespaced-routes)] 154 | 155 | ```Ruby 156 | namespace :admin do 157 | # 将请求 /admin/products/* 交由 Admin::ProductsController 处理 158 | # (app/controllers/admin/products_controller.rb) 159 | resources :products 160 | end 161 | ``` 162 | 163 | * 164 | 不要使用旧式的控制器路由。这种路由会让控制器的所有动作都通过 GET 请求调用。 165 | [[link](#no-wild-routes)] 166 | 167 | ```Ruby 168 | # 非常差 169 | match ':controller(/:action(/:id(.:format)))' 170 | ``` 171 | 172 | * 173 | 不要使用 `match` 来定义任何路由,除非确实需要将多种请求映射到某个动作,这时可以通过 `via` 选项来指定请求类型,如 `[:get, :post, :patch, :put, :delete]`。 174 | [[link](#no-match-routes)] 175 | 176 | ## 控制器 177 | 178 | * 179 | 控制器应该保持苗条 ― 它们应该只为视图层提供数据,不应包含任何业务逻辑(所有业务逻辑都应当放在模型里)。 180 | [[link](#skinny-controllers)] 181 | 182 | * 183 | 每个控制器的动作(理论上)应当只调用一个除了初始的 find 或 new 之外的方法。 184 | [[link](#one-method)] 185 | 186 | * 187 | 控制器与视图之间共享不超过两个实例变量。 188 | [[link](#shared-instance-variables)] 189 | 190 | ## 模型 191 | 192 | * 193 | 自由地引入不是 ActiveRecord 的模型类。 194 | [[link](#model-classes)] 195 | 196 | * 197 | 模型的命名应有意义(但简短)且不含缩写。 198 | [[link](#meaningful-model-names)] 199 | 200 | * 201 | 如果需要模型类有与 ActiveRecord 类似的行为(如验证),但又不想有 ActiveRecord 的数据库功能,应使用 [ActiveAttr](https://github.com/cgriego/active_attr) 这个 gem。 202 | [[link](#activeattr-gem)] 203 | 204 | ```Ruby 205 | class Message 206 | include ActiveAttr::Model 207 | 208 | attribute :name 209 | attribute :email 210 | attribute :content 211 | attribute :priority 212 | 213 | attr_accessible :name, :email, :content 214 | 215 | validates_presence_of :name 216 | validates_format_of :email, :with => /\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}\z/i 217 | validates_length_of :content, :maximum => 500 218 | end 219 | ``` 220 | 221 | 更完整的示例请参考 [RailsCast on the subject](http://railscasts.com/episodes/326-activeattr)。 222 | 223 | ### ActiveRecord 224 | 225 | * 226 | 避免改动缺省的 ActiveRecord 惯例(表的名字、主键等),除非你有一个充分的理由(比如,不受你控制的数据库)。 227 | [[link](#keep-ar-defaults)] 228 | 229 | ```Ruby 230 | # 差 - 如果你能更改数据库的 schema,那就不要这样写 231 | class Transaction < ActiveRecord::Base 232 | self.table_name = 'order' 233 | ... 234 | end 235 | ``` 236 | 237 | * 238 | 把宏风格的方法调用(`has_many`, `validates` 等)放在类定义语句的最前面。 239 | [[link](#macro-style-methods)] 240 | 241 | ```Ruby 242 | class User < ActiveRecord::Base 243 | # 默认的 scope 放在最前(如果有的话) 244 | default_scope { where(active: true) } 245 | 246 | # 接下来是常量初始化 247 | COLORS = %w(red green blue) 248 | 249 | # 然后是 attr 相关的宏 250 | attr_accessor :formatted_date_of_birth 251 | 252 | attr_accessible :login, :first_name, :last_name, :email, :password 253 | 254 | # 紧接着是与关联有关的宏 255 | belongs_to :country 256 | 257 | has_many :authentications, dependent: :destroy 258 | 259 | # 以及与验证有关的宏 260 | validates :email, presence: true 261 | validates :username, presence: true 262 | validates :username, uniqueness: { case_sensitive: false } 263 | validates :username, format: { with: /\A[A-Za-z][A-Za-z0-9._-]{2,19}\z/ } 264 | validates :password, format: { with: /\A\S{8,128}\z/, allow_nil: true} 265 | 266 | # 下面是回调方法 267 | before_save :cook 268 | before_save :update_username_lower 269 | 270 | # 其它的宏(如 devise)应放在回调方法之后 271 | 272 | ... 273 | end 274 | ``` 275 | 276 | * 277 | `has_many :through` 优于 `has_and_belongs_to_many`。 使用 `has_many :through` 允许 join 模型有附加的属性及验证。 278 | [[link](#has-many-through)] 279 | 280 | ```Ruby 281 | # 不太好 - 使用 has_and_belongs_to_many 282 | class User < ActiveRecord::Base 283 | has_and_belongs_to_many :groups 284 | end 285 | 286 | class Group < ActiveRecord::Base 287 | has_and_belongs_to_many :users 288 | end 289 | 290 | # 更好 - 使用 has_many :through 291 | class User < ActiveRecord::Base 292 | has_many :memberships 293 | has_many :groups, through: :memberships 294 | end 295 | 296 | class Membership < ActiveRecord::Base 297 | belongs_to :user 298 | belongs_to :group 299 | end 300 | 301 | class Group < ActiveRecord::Base 302 | has_many :memberships 303 | has_many :users, through: :memberships 304 | end 305 | ``` 306 | 307 | * 308 | `self[:attribute]` 比 `read_attribute(:attribute)` 更好。 309 | [[link](#read-attribute)] 310 | 311 | ```Ruby 312 | # 差 313 | def amount 314 | read_attribute(:amount) * 100 315 | end 316 | 317 | # 好 318 | def amount 319 | self[:amount] * 100 320 | end 321 | ``` 322 | 323 | * 324 | `self[:attribute] = value` 优于 `write_attribute(:attribute, value)`。 325 | [[link](#write-attribute)] 326 | 327 | ```Ruby 328 | # 差 329 | def amount 330 | write_attribute(:amount, 100) 331 | end 332 | 333 | # 好 334 | def amount 335 | self[:amount] = 100 336 | end 337 | ``` 338 | 339 | * 340 | 总是使用新式的 ["sexy" 341 | 验证](http://thelucid.com/2010/01/08/sexy-validation-in-edge-rails-rails-3/)。 342 | [[link](#sexy-validations)] 343 | 344 | ```Ruby 345 | # 差 346 | validates_presence_of :email 347 | validates_length_of :email, maximum: 100 348 | 349 | # 好 350 | validates :email, presence: true, length: { maximum: 100 } 351 | ``` 352 | 353 | * 354 | 当一个自定义的验证规则使用次数超过一次时,或该验证规则是基于正则表达式时,应该创建一个自定义的验证规则文件。 355 | [[link](#custom-validator-file)] 356 | 357 | ```Ruby 358 | # 差 359 | class Person 360 | validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i } 361 | end 362 | 363 | # 好 364 | class EmailValidator < ActiveModel::EachValidator 365 | def validate_each(record, attribute, value) 366 | record.errors[attribute] << (options[:message] || 'is not a valid email') unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i 367 | end 368 | end 369 | 370 | class Person 371 | validates :email, email: true 372 | end 373 | ``` 374 | 375 | * 376 | 自定义验证规则应放在 `app/validators` 目录下。 377 | [[link](#app-validators)] 378 | 379 | * 380 | 如果你在维护数个相关的应用,或验证规则本身足够通用,可以考虑将自定义的验证规则抽象为一个共用的 gem。 381 | [[link](#custom-validators-gem)] 382 | 383 | * 384 | 自由地使用命名 scope。 385 | [[link](#named-scopes)] 386 | 387 | ```Ruby 388 | class User < ActiveRecord::Base 389 | scope :active, -> { where(active: true) } 390 | scope :inactive, -> { where(active: false) } 391 | 392 | scope :with_orders, -> { joins(:orders).select('distinct(users.id)') } 393 | end 394 | ``` 395 | 396 | * 397 | 当一个由 lambda 和参数定义的命名 scope 太过复杂时, 398 | 更好的方式是创建一个具有同样用途并返回 `ActiveRecord::Relation` 对象的类方法。这很可能让 scope 更加精简。 399 | [[link](#named-scope-class)] 400 | 401 | ```Ruby 402 | class User < ActiveRecord::Base 403 | def self.with_orders 404 | joins(:orders).select('distinct(users.id)') 405 | end 406 | end 407 | ``` 408 | 409 | 注意这种方式不允许命名 scope 那样的链式调用。例如: 410 | 411 | ```Ruby 412 | # 不能链式调用 413 | class User < ActiveRecord::Base 414 | def User.old 415 | where('age > ?', 80) 416 | end 417 | 418 | def User.heavy 419 | where('weight > ?', 200) 420 | end 421 | end 422 | ``` 423 | 424 | 这种方式下 `old` 和 `heavy` 可以单独工作,但不能执行 `User.old.heavy`。 425 | 若要链式调用,请使用下面的代码: 426 | 427 | ```Ruby 428 | # 可以链式调用 429 | class User < ActiveRecord::Base 430 | scope :old, -> { where('age > 60') } 431 | scope :heavy, -> { where('weight > 200') } 432 | end 433 | ``` 434 | 435 | * 436 | 注意 437 | [`update_attribute`](http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_attribute) 438 | 方法的行为。它不运行模型验证(与 `update_attributes` 不同),因此可能弄乱模型的状态。 439 | [[link](#beware-update-attribute)] 440 | 441 | * 442 | 应使用对用户友好的 URL。URL 中应显示模型的一些具有描述性的属性,而不是仅仅显示 `id`。有多种方法可以达到这个目的: 443 | [[link](#user-friendly-urls)] 444 | 445 | * 重写模型的 `to_param` 方法。Rails 使用该方法为对象创建 URL。该方法默认会以字符串形式返回记录的 `id` 项。 446 | 可以重写该方法以包含其它可读性强的属性。 447 | 448 | ```Ruby 449 | class Person 450 | def to_param 451 | "#{id} #{name}".parameterize 452 | end 453 | end 454 | ``` 455 | 456 | 为了将结果转换为一个 URL 友好的值,字符串应该调用 `parameterize` 方法。 457 | 对象的 `id` 属性值需要位于 URL 的开头,以便使用 ActiveRecord 的 `find` 方法查找对象。 458 | 459 | * 使用 `friendly_id` 这个 gem。它允许使用对象的一些描述性属性而非 `id` 来创建可读性强的 URL。 460 | 461 | ```Ruby 462 | class Person 463 | extend FriendlyId 464 | friendly_id :name, use: :slugged 465 | end 466 | ``` 467 | 468 | 查看 [gem documentation](https://github.com/norman/friendly_id) 以获得更多 `friendly_id` 的使用信息。 469 | 470 | * 471 | 应使用 `find_each` 来迭代一系列 ActiveRecord 对象。用循环来处理数据库中的记录集(如 `all` 方法)是非常低效率的,因为循环试图一次性得到所有对象。而批处理方法允许一批批地处理记录,因此需要占用的内存大幅减少。 472 | [[link](#find-each)] 473 | 474 | ```Ruby 475 | # 差 476 | Person.all.each do |person| 477 | person.do_awesome_stuff 478 | end 479 | 480 | Person.where('age > 21').each do |person| 481 | person.party_all_night! 482 | end 483 | 484 | # 好 485 | Person.find_each do |person| 486 | person.do_awesome_stuff 487 | end 488 | 489 | Person.where('age > 21').find_each do |person| 490 | person.party_all_night! 491 | end 492 | ``` 493 | 494 | * 495 | 因为 [Rails 为有依赖关系的关联添加了回调方法](https://github.com/rails/rails/issues/3458),应总是调用 496 | `before_destroy` 回调方法,调用该方法并启用 `prepend: true` 选项会执行验证。 497 | [[link](#before_destroy)] 498 | 499 | ```Ruby 500 | # 差——即使 super_admin 返回 true,roles 也会自动删除 501 | has_many :roles, dependent: :destroy 502 | 503 | before_destroy :ensure_deletable 504 | 505 | def ensure_deletable 506 | fail "Cannot delete super admin." if super_admin? 507 | end 508 | 509 | # 好 510 | has_many :roles, dependent: :destroy 511 | 512 | before_destroy :ensure_deletable, prepend: true 513 | 514 | def ensure_deletable 515 | fail "Cannot delete super admin." if super_admin? 516 | end 517 | ``` 518 | 519 | ### ActiveRecord 查询 520 | 521 | * 522 | 不要在查询中使用字符串插值,它会使你的代码有被 SQL 注入攻击的风险。 523 | [[link](#avoid-interpolation)] 524 | 525 | ```Ruby 526 | # 差——插值的参数不会被转义 527 | Client.where("orders_count = #{params[:orders]}") 528 | 529 | # 好——参数会被适当转义 530 | Client.where('orders_count = ?', params[:orders]) 531 | ``` 532 | 533 | * 534 | 当查询中有超过 1 个占位符时,应考虑使用名称占位符,而非位置占位符。 535 | [[link](#named-placeholder)] 536 | 537 | ```Ruby 538 | # 一般般 539 | Client.where( 540 | 'created_at >= ? AND created_at <= ?', 541 | params[:start_date], params[:end_date] 542 | ) 543 | 544 | # 好 545 | Client.where( 546 | 'created_at >= :start_date AND created_at <= :end_date', 547 | start_date: params[:start_date], end_date: params[:end_date] 548 | ) 549 | ``` 550 | 551 | * 552 | 当只需要通过 id 查询单个记录时,优先使用 `find` 而不是 `where`。 553 | [[link](#find)] 554 | 555 | ```Ruby 556 | # 差 557 | User.where(id: id).take 558 | 559 | # 好 560 | User.find(id) 561 | ``` 562 | 563 | * 564 | 当只需要通过属性查询单个记录时,优先使用 `find_by` 而不是 `where`。 565 | [[link](#find_by)] 566 | 567 | ```Ruby 568 | # 差 569 | User.where(first_name: 'Bruce', last_name: 'Wayne').first 570 | 571 | # 好 572 | User.find_by(first_name: 'Bruce', last_name: 'Wayne') 573 | ``` 574 | 575 | * 576 | 当需要处理多条记录时,应使用 `find_each`。 577 | [[link](#find_each)] 578 | 579 | ```Ruby 580 | # 差——一次性加载所有记录 581 | # 当 users 表有成千上万条记录时,非常低效 582 | User.all.each do |user| 583 | NewsMailer.weekly(user).deliver_now 584 | end 585 | 586 | # 好——分批检索记录 587 | User.find_each do |user| 588 | NewsMailer.weekly(user).deliver_now 589 | end 590 | ``` 591 | 592 | * 593 | `where.not` 比书写 SQL 更好。 594 | [[link](#where-not)] 595 | 596 | ```Ruby 597 | # 差 598 | User.where("id != ?", id) 599 | 600 | # 好 601 | User.where.not(id: id) 602 | ``` 603 | 604 | ## 迁移 605 | 606 | * 607 | 应使用版本控制工具记录 `schema.rb` (或 `structure.sql` )的变化。 608 | [[link](#schema-version)] 609 | 610 | * 611 | 应使用 `rake db:scheme:load` 而不是 `rake db:migrate` 来初始化空数据库。 612 | [[link](#db-schema-load)] 613 | 614 | * 615 | 应在迁移文件中设置默认值,而不是在应用层面设置。 616 | [[link](#default-migration-values)] 617 | 618 | ```Ruby 619 | # 差——在应用中设置默认值 620 | def amount 621 | self[:amount] or 0 622 | end 623 | ``` 624 | 625 | 虽然许多 Rails 开发者建议在 Rails 中强制使用表的默认值,但这会使数据受到许多应用 bug 的影响,因而导致应用极其难以维护。考虑到大多数有一定规模的 Rails 应用都与其它应用共享数据库,保持应用的数据完整性几乎是不可能的。 626 | 627 | * 628 | 务必使用外键约束。在 Rails 4.2 中,ActiveRecord 本身已经支持外键约束。 629 | [[link](#foreign-key-constraints)] 630 | 631 | * 632 | 书写建设性的迁移(添加表或列)时,应使用 `change` 方法而不是 `up` 或 `down` 方法。 633 | [[link](#change-vs-up-down)] 634 | 635 | ```Ruby 636 | # 老式写法 637 | class AddNameToPeople < ActiveRecord::Migration 638 | def up 639 | add_column :people, :name, :string 640 | end 641 | 642 | def down 643 | remove_column :people, :name 644 | end 645 | end 646 | 647 | # 新式写法(更好) 648 | class AddNameToPeople < ActiveRecord::Migration 649 | def change 650 | add_column :people, :name, :string 651 | end 652 | end 653 | ``` 654 | 655 | * 656 | 不要在迁移中使用模型类。由于模型的变化,模型类也一直处在变化当中,过去运行正常的迁移可能不知什么时候就不能正常进行了。 657 | [[link](#no-model-class-migrations)] 658 | 659 | ## 视图 660 | 661 | * 662 | 不要直接从视图调用模型层。 663 | [[link](#no-direct-model-view)] 664 | 665 | * 666 | 复杂的格式化不应放在视图中,而应提取为视图 helper 或模型中的方法。 667 | [[link](#no-complex-view-formatting)] 668 | 669 | * 670 | 应使用 partial 模版与布局来减少代码重复。 671 | [[link](#partials)] 672 | 673 | ## 国际化 674 | 675 | * 676 | 不应在视图、模型或控制器里添加语言相关的设置,应在 `config/locales` 目录下进行设置。 677 | [[link](#locale-texts)] 678 | 679 | * 680 | 当 ActiveRecord 模型的标签需要被翻译时,应使用`activerecord` scope: 681 | [[link](#translated-labels)] 682 | 683 | ``` 684 | en: 685 | activerecord: 686 | models: 687 | user: Member 688 | attributes: 689 | user: 690 | name: "Full name" 691 | ``` 692 | 693 | 然后 `User.model_name.human` 会返回 "Member" ,而 `User.human_attribute_name("name")` 会返回 "Full name"。这些属性的翻译会作为视图中的标签。 694 | 695 | 696 | * 697 | 把在视图中使用的文字与 ActiveRecord 的属性翻译分开。把模型使用的语言文件放在 `models` 目录下,把视图使用的文字放在 `views` 目录下。 698 | [[link](#organize-locale-files)] 699 | 700 | * 当使用额外目录来设置语言文件时,应在 `application.rb` 文件里列出这些目录以加载设置。 701 | 702 | ```Ruby 703 | # config/application.rb 704 | config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')] 705 | ``` 706 | 707 | * 708 | 把共享的本地化选项,如日期或货币格式,放在 `locales` 的根目录下。 709 | [[link](#shared-localization)] 710 | 711 | * 712 | 应使用精简形式的 I18n 方法: 713 | 使用 `I18n.t` 而非 `I18n.translate`; 714 | 使用 `I18n.l` 而非 `I18n.localize`。 715 | [[link](#short-i18n)] 716 | 717 | * 718 | 应使用 "懒惰" 查询来获取视图中使用的文本。假设我们有以下结构: 719 | [[link](#lazy-lookup)] 720 | 721 | ``` 722 | en: 723 | users: 724 | show: 725 | title: "User details page" 726 | ``` 727 | 728 | `users.show.title` 的数值能这样被 `app/views/users/show.html.haml` 获取: 729 | 730 | ```Ruby 731 | = t '.title' 732 | ``` 733 | 734 | * 735 | 应在控制器与模型中使用点分隔的键,而非指定 `:scope` 选项。点分隔的调用更容易阅读,也更易追踪层级关系。 736 | [[link](#dot-separated-keys)] 737 | 738 | ```Ruby 739 | # 差 740 | I18n.t :record_invalid, :scope => [:activerecord, :errors, :messages] 741 | 742 | # 好 743 | I18n.t 'activerecord.errors.messages.record_invalid' 744 | ``` 745 | 746 | * 747 | 更详细的 Rails i18n 信息可以在 [Rails Guides](http://guides.rubyonrails.org/i18n.html) 找到。 748 | [[link](#i18n-guides)] 749 | 750 | ## Assets 751 | 752 | 应使用 [assets pipeline](http://guides.rubyonrails.org/asset_pipeline.html) 来管理应用的资源结构。 753 | 754 | * 755 | 自定义的样式表、JavaScript 文件或图片文件,应放在 `app/assets` 目录下。 756 | [[link](#reserve-app-assets)] 757 | 758 | * 759 | 把自己开发但不好归类的库文件,应放在 `lib/assets/` 目录下。 760 | [[link](#lib-assets)] 761 | 762 | * 763 | 第三方代码,如 [jQuery](http://jquery.com/) 或 [bootstrap](http://twitter.github.com/bootstrap/),应放在 `vendor/assets` 目录下。 764 | [[link](#vendor-assets)] 765 | 766 | * 767 | 尽可能使用资源的 gem 版。例如: 768 | [jquery-rails](https://github.com/rails/jquery-rails), 769 | [jquery-ui-rails](https://github.com/joliss/jquery-ui-rails), 770 | [bootstrap-sass](https://github.com/thomas-mcdonald/bootstrap-sass), 771 | [zurb-foundation](https://github.com/zurb/foundation) 772 | [[link](#gem-assets)] 773 | 774 | ## Mailers 775 | 776 | * 777 | 应将 mailer 命名为 `SomethingMailer`。若没有 Mailer 后缀,不能立即断定它是否为一个 mailer,也不能断定哪个视图与它有关。 778 | [[link](#mailer-name)] 779 | 780 | * 781 | 提供 HTML 与纯文本两份视图模版。 782 | [[link](#html-plain-email)] 783 | 784 | * 785 | 在开发环境下应显示发信失败错误。这些错误默认是关闭的。 786 | [[link](#enable-delivery-errors)] 787 | 788 | ```Ruby 789 | # config/environments/development.rb 790 | 791 | config.action_mailer.raise_delivery_errors = true 792 | ``` 793 | 794 | * 795 | 在开发环境下使用诸如 [Mailcatcher](https://github.com/sj26/mailcatcher) 的本地 SMTP 服务器。 796 | [[link](#local-smtp)] 797 | 798 | ```Ruby 799 | # config/environments/development.rb 800 | 801 | config.action_mailer.smtp_settings = { 802 | address: 'localhost', 803 | port: 1025, 804 | # 更多设置 805 | } 806 | ``` 807 | 808 | * 809 | 为域名设置默认项。 810 | [[link](#default-hostname)] 811 | 812 | ```Ruby 813 | # config/environments/development.rb 814 | config.action_mailer.default_url_options = { host: "#{local_ip}:3000" } 815 | 816 | # config/environments/production.rb 817 | config.action_mailer.default_url_options = { host: 'your_site.com' } 818 | 819 | # 在 mailer 类中 820 | default_url_options[:host] = 'your_site.com' 821 | ``` 822 | 823 | * 824 | 若需要在邮件中添加到网站的超链接,应总是使用 `_url` 方法,而非 825 | `_path` 方法。`_url` 方法产生的超链接包含域名,而 `_path` 826 | 方法产生相对链接。 827 | [[link](#url-not-path-in-email)] 828 | 829 | ```Ruby 830 | # 差 831 | You can always find more info about this course 832 | <%= link_to 'here', course_path(@course) %> 833 | 834 | # 好 835 | You can always find more info about this course 836 | <%= link_to 'here', course_url(@course) %> 837 | ``` 838 | 839 | 840 | * 841 | 正确地设置寄件人与收件人地址的格式。应使用下列格式: 842 | [[link](#email-addresses)] 843 | 844 | ```Ruby 845 | # 在你的 mailer 类中 846 | default from: 'Your Name ' 847 | ``` 848 | 849 | * 850 | 确保测试环境下的 email 发送方法设置为 `test`: 851 | [[link](#delivery-method-test)] 852 | 853 | ```Ruby 854 | # config/environments/test.rb 855 | 856 | config.action_mailer.delivery_method = :test 857 | ``` 858 | 859 | * 860 | 开发环境和生产环境下的发送方法应设置为 `smtp`: 861 | [[link](#delivery-method-smtp)] 862 | 863 | ```Ruby 864 | # config/environments/development.rb, config/environments/production.rb 865 | 866 | config.action_mailer.delivery_method = :smtp 867 | ``` 868 | 869 | * 870 | 当发送 HTML 邮件时,所有样式应为行内样式,这是因为某些客户端不能正确显示外部样式。然而,这使得邮件难以维护理并会导致代码重复。有两个类似的 gem 可以转换样式,并将样式放在对应的 html 标签里: [premailer-rails3](https://github.com/fphilipe/premailer-rails3) 和 871 | [roadie](https://github.com/Mange/roadie)。 872 | [[link](#inline-email-styles)] 873 | 874 | * 875 | 避免在产生页面响应的同时发送邮件。若有多个邮件需要发送,这会导致页面加载延迟甚至请求超时。有鉴于此,应使用 [sidekiq](https://github.com/mperham/sidekiq) 这个 gem 在后台发送邮件。 876 | [[link](#background-email)] 877 | 878 | ## Time 879 | 880 | * 881 | 在 `application.rb` 里设置相应的时区。 882 | [[link](#time-now)] 883 | 884 | ```Ruby 885 | config.time_zone = 'Eastern European Time' 886 | # 可选配置——注意取值只能是 :utc 或 :local 中的一个(默认为 :utc) 887 | config.active_record.default_timezone = :local 888 | ``` 889 | 890 | * 891 | 不要使用 `Time.parse`。 892 | [[link](#time-parse)] 893 | 894 | ```Ruby 895 | # 差 896 | Time.parse('2015-03-02 19:05:37') # => 会假设时间是基于操作系统的时区。 897 | 898 | # 好 899 | Time.zone.parse('2015-03-02 19:05:37') # => Mon, 02 Mar 2015 19:05:37 EET +02:00 900 | ``` 901 | 902 | * 903 | 不要使用 `Time.now`。 904 | [[link](#time-now)] 905 | 906 | ```Ruby 907 | # 差 908 | Time.now # => 无视所配置的时区,返回操作系统时间。 909 | 910 | # 好 911 | Time.zone.now # => Fri, 12 Mar 2014 22:04:47 EET +02:00 912 | Time.current # 结果同上,但更简洁 913 | ``` 914 | 915 | ## Bundler 916 | 917 | * 918 | 只在开发环境或测试环境下使用的 gem 应进行适当的分组。 919 | [[link](#dev-test-gems)] 920 | 921 | * 922 | 在项目中只使用广为人知的 gem。如果你考虑引入某些鲜为人所知的 gem,应该先仔细检查一下其源代码。 923 | [[link](#only-good-gems)] 924 | 925 | * 926 | 关于多个开发者使用不同操作系统的项目,与操作系统有关的 gem 默认情况下会产生经常变动的 `Gemfile.lock`。 在 Gemfile 文件里,所有与 OS X 相关的 gem 放在 `darwin` 群组,而所有与 Linux 有关的 gem 应放在 `linux` 群组: 927 | [[link](#os-specific-gemfile-locks)] 928 | 929 | ```Ruby 930 | # Gemfile 931 | group :darwin do 932 | gem 'rb-fsevent' 933 | gem 'growl' 934 | end 935 | 936 | group :linux do 937 | gem 'rb-inotify' 938 | end 939 | ``` 940 | 941 | 要在正确的环境下加载合适的 gem,需添加以下代码至 `config/application.rb` : 942 | 943 | ```Ruby 944 | platform = RUBY_PLATFORM.match(/(linux|darwin)/)[0].to_sym 945 | Bundler.require(platform) 946 | ``` 947 | 948 | * 949 | 不要把 `Gemfile.lock` 文件从版本控制里移除。这可不是一个随机产生的文件——它的目的是确保你所有的团队成员执行 `bundle install` 时,获得相同版本的 gem 。 950 | [[link](#gemfile-lock)] 951 | 952 | ## 有缺陷的 Gem 953 | 954 | 这是一个有问题的或有更好替代物的 gem 列表。不要在项目中使用它们。 955 | 956 | * [rmagick](http://rmagick.rubyforge.org/) - 这个 gem 因大量消耗内存而臭名昭著。应使用 [minimagick](https://github.com/probablycorey/mini_magick) 来替代它。 957 | 958 | * [autotest](http://www.zenspider.com/ZSS/Products/ZenTest/) - 测试自动化的过时方案,远不及 [guard](https://github.com/guard/guard) 和 [watchr](https://github.com/mynyml/watchr)。 959 | 960 | * [rcov](https://github.com/relevance/rcov) - 代码覆盖率工具,不兼容 Ruby 1.9。应使用 [SimpleCov](https://github.com/colszowka/simplecov) 来替代它。 961 | 962 | * [therubyracer](https://github.com/cowboyd/therubyracer) - 内存杀手,强烈不建议在生产环境中使用。建议使用 `node.js` 来替代它。 963 | 964 | 这仍是一个完善中的列表,欢迎添加流行但有缺陷的 gem。 965 | 966 | ## 进程管理 967 | 968 | * 969 | 如果项目依赖各种外界的进程,应使用 [foreman](https://github.com/ddollar/foreman) 来管理它们。 970 | [[link](#foreman)] 971 | 972 | # 延伸阅读 973 | 974 | 以下是几个极好的讲述 Rails 风格的资源,闲暇时可以考虑延伸阅读: 975 | 976 | * [The Rails 4 Way](http://www.amazon.com/The-Rails-Addison-Wesley-Professional-Ruby/dp/0321944275) 977 | * [Ruby on Rails Guides](http://guides.rubyonrails.org/) 978 | * [The RSpec Book](http://pragprog.com/book/achbd/the-rspec-book) 979 | * [The Cucumber Book](http://pragprog.com/book/hwcuc/the-cucumber-book) 980 | * [Everyday Rails Testing with RSpec](https://leanpub.com/everydayrailsrspec) 981 | * [Better Specs for RSpec](http://betterspecs.org) 982 | 983 | # 贡献 984 | 985 | 本指南的每条建议都不是定案。我渴望与对 Rails 编码风格有兴趣的大家一起协作,创造出一份对整个 Ruby 社区都有益的资源。 986 | 987 | 欢迎 open tickets 或发送带有改进的 pull request。在此提前感谢您的帮助! 988 | 989 | 您可以通过 [gittip](https://www.gittip.com/bbatsov) 对本项目(以及 RuboCop 项目)进行捐赠支持。 990 | 991 | [![Support via Gittip](https://rawgithub.com/twolfson/gittip-badge/0.2.0/dist/gittip.png)](https://www.gittip.com/bbatsov) 992 | 993 | ## 如何贡献? 994 | 995 | 只需遵循[贡献指南](https://github.com/bbatsov/rails-style-guide/blob/master/CONTRIBUTING.md)即可。 996 | 997 | # 许可证 998 | 999 | ![Creative Commons License](http://i.creativecommons.org/l/by/3.0/88x31.png) 1000 | This work is licensed under a [Creative Commons Attribution 3.0 Unported License](http://creativecommons.org/licenses/by/3.0/deed.zh) 1001 | 1002 | # 口耳相传 1003 | 1004 | 一份社区驱动的风格指南,若不为人所知,那有何用。请在微博转发这份指南,分享给你的朋友或同事。我们得到的每个评论、建议或意见都可以让这份指南变得更好一点。而我们想要拥有的是尽可能好的指南,不是吗? 1005 | 1006 | 共勉之,
1007 | [Bozhidar](https://twitter.com/bbatsov) 1008 | -------------------------------------------------------------------------------- /README-zhTW.md: -------------------------------------------------------------------------------- 1 | # 序幕 2 | 3 | > Role models are important.
4 | > -- 機器戰警 Alex J. Murphy 5 | 6 | 這份指南目的於示範一整套 Rails 3 開發的風格慣例及最佳實踐。這是一份與由現存社群所驅動的 [Ruby 程式碼風格指南](https://github.com/bbatsov/ruby-style-guide)互補的指南。 7 | 8 | 而本指南中[測試 Rails 應用](#testing)小節擺在[開發 Rails 應用](#developing)之後,因為我相信[行為驅動開發](http://en.wikipedia.org/wiki/Behavior_Driven_Development) 9 | (BDD) 是最佳的軟體開發之道。銘記在心吧。 10 | 11 | Rails 是一個堅持己見的框架,而這也是一份堅持己見的指南。在我的心裡,我堅信 [RSpec](https://www.relishapp.com/rspec) 優於 Test::Unit,[Sass](http://sass-lang.com/) 優於 CSS 以及 12 | [Haml](http://haml-lang.com/),([Slim](http://slim-lang.com/)) 優於 Erb。所以不要期望在這裡找到 Test::Unit, CSS 及 Erb 的忠告。 13 | 14 | 某些忠告僅適用於 Rails 3.1+ 以上版本。 15 | 16 | 你可以使用 [Transmuter](https://github.com/TechnoGate/transmuter) 來產生本指南的一份 PDF 或 HTML 複本。 17 | 18 | 本指南被翻譯成下列語言: 19 | 20 | * [简体中文](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhCN.md) 21 | * [英文原版](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README.md) 22 | * [德文](https://github.com/arbox/de-rails-style-guide/blob/master/README-deDE.md) 23 | * [日文](https://github.com/satour/rails-style-guide/blob/master/README-jaJA.md) 24 | * [俄文](https://github.com/arbox/rails-style-guide/blob/master/README-ruRU.md) 25 | * [土耳其文](https://github.com/tolgaavci/rails-style-guide/blob/master/README-trTR.md) 26 | * [韓文](https://github.com/pureugong/rails-style-guide/blob/master/README-koKR.md) 27 | 28 | # 目錄 29 | 30 | * [開發 Rails 應用程式](#開發-rails-應用程式) 31 | * [設定檔 (Config)](#設定檔-config) 32 | * [路由 (Routes)](#路由-routes) 33 | * [控制器 (Controller)](#控制器-controller) 34 | * [資料模型 (Model)](#資料模型-model) 35 | * [遷移 (Migrations)](#遷移-migrations) 36 | * [視圖 (Views)](#視圖-views) 37 | * [國際化 (I18n)](#國際化-i18n) 38 | * [資產 (Assets)](#資產-assets) 39 | * [郵件 (Mailers)](#郵件-mailers) 40 | * [打包 (Bundler)](#打包-bundler) 41 | * [貴重的 Gems](#貴重的-gems) 42 | * [缺陷的 Gems](#有缺陷的-gems) 43 | * [管理處理程序 (Process)](#管理處理程序-process) 44 | * [測試 Rails 應用程式](#測試-rails-應用程式) 45 | * [Cucumber](#cucumber) 46 | * [RSpec](#rspec) 47 | 48 | 49 | # 開發 Rails 應用程式 50 | 51 | ## 設定檔 (config) 52 | 53 | * 把自訂的初始化程式碼放在 `config/initializers`。在 initializers 內的程式碼會在應用程式啟動時執行。 54 | * 每一個 gem 相關的初始化程式碼應當使用同樣的名稱,放在不同的文件裡,如: `carrierwave.rb`, `active_admin.rb`, 等等。 55 | * 為開發、測試及生產環境分別調整設定(在 `config/environments/` 下對應的文件) 56 | * 標記額外的資產 (assets) 給預編譯(如果有的話): 57 | 58 | ```Ruby 59 | # config/environments/production.rb 60 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) 61 | config.assets.precompile += %w( rails_admin/rails_admin.css rails_admin/rails_admin.js ) 62 | ``` 63 | 64 | * 將所有環境都通用的設定檔放在 `config/application.rb` 檔案。 65 | * 另外開一個與生產環境(production enviroment)幾乎相同的 `staging` 環境。 66 | 67 | ## 路由 (Routes) 68 | 69 | * 當你需要加入一個或多個動作 (action) 至一個 RESTful 資源時(你真的需要嗎?),使用 `member` and `collection` 路由。 70 | 71 | ```Ruby 72 | # 劣 73 | get 'subscriptions/:id/unsubscribe' 74 | resources :subscriptions 75 | 76 | # 優 77 | resources :subscriptions do 78 | get 'unsubscribe', on: :member 79 | end 80 | 81 | # 劣 82 | get 'photos/search' 83 | resources :photos 84 | 85 | # 優 86 | resources :photos do 87 | get 'search', on: :collection 88 | end 89 | ``` 90 | 91 | * 若需要定義多個 `member/collection` 路由,請改用區塊語法(block syntax)。 92 | 93 | ```Ruby 94 | resources :subscriptions do 95 | member do 96 | get 'unsubscribe' 97 | # 更多路由 98 | end 99 | end 100 | 101 | resources :photos do 102 | collection do 103 | get 'search' 104 | # 更多路由 105 | end 106 | end 107 | ``` 108 | 109 | * 使用巢狀路由(nested routes)來更佳地表達各 ActiveRecord 資料模型之間的關係。 110 | 111 | ```Ruby 112 | class Post < ActiveRecord::Base 113 | has_many :comments 114 | end 115 | 116 | class Comments < ActiveRecord::Base 117 | belongs_to :post 118 | end 119 | 120 | # routes.rb 121 | resources :posts do 122 | resources :comments 123 | end 124 | ``` 125 | 126 | * 使用命名空間路由來分類相關的行為。 127 | 128 | ```Ruby 129 | namespace :admin do 130 | # Directs /admin/products/* to Admin::ProductsController 131 | # (app/controllers/admin/products_controller.rb) 132 | resources :products 133 | end 134 | ``` 135 | 136 | * 不要使用地圖砲路由。這種路由會讓每個控制器的動作透過 GET 請求存取。 137 | 138 | ```Ruby 139 | # 極劣 140 | match ':controller(/:action(/:id(.:format)))' 141 | ``` 142 | 143 | ## 控制器 (Controller) 144 | 145 | * 讓你的控制器保持苗條──它們應該只替視圖層取出資料且不包含任何業務邏輯(所有業務邏輯應當放在資料模型裡)。 146 | * 每個控制器裡的動作 (action) 應當只呼叫一個除了初始的 find 或 new 以外的方法(理想狀態)。 147 | * 控制器與視圖之間共享不超過兩個實體變數 (instance variable)。 148 | 149 | ## 資料模型 (Model) 150 | 151 | * 請任意引入不是 ActiveRecord 的資料模型。 152 | * 替資料模型命名有意義(但簡短)且不帶縮寫的名字。 153 | * 如果你需要普通的資料模型有著 ActiveRecord 的行為,比方說驗證,可使用 [ActiveAttr](https://github.com/cgriego/active_attr) gem。 154 | 155 | ```Ruby 156 | class Message 157 | include ActiveAttr::Model 158 | 159 | attribute :name 160 | attribute :email 161 | attribute :content 162 | attribute :priority 163 | 164 | attr_accessible :name, :email, :content 165 | 166 | validates_presence_of :name 167 | validates_format_of :email, :with => /\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}\z/i 168 | validates_length_of :content, :maximum => 500 169 | end 170 | ``` 171 | 172 | 更完整的範例,參考 [RailsCast on the subject](http://railscasts.com/episodes/326-activeattr)。 173 | 174 | ### ActiveRecord 175 | 176 | * 避免改動預設的 ActiveRecord(表的名字、主鍵,等等),除非你有一個非常好的理由(像是不受你控制的資料庫)。 177 | * 把巨集風格的方法放在類別定義的前面(`has_many`, `validates`, 等等)。 178 | 179 | ```Ruby 180 | class User < ActiveRecord::Base 181 | # 預設的 scope 放在最前面(如果有的話) 182 | default_scope { where(active: true) } 183 | 184 | # 接下來是常數 185 | GENDERS = %w(male female) 186 | 187 | # 然後放一些 attr 相關的巨集 188 | attr_accessor :formatted_date_of_birth 189 | 190 | attr_accessible :login, :first_name, :last_name, :email, :password 191 | 192 | # 緊接著是關聯的巨集 193 | belongs_to :country 194 | 195 | has_many :authentications, dependent: :destroy 196 | 197 | # 以及巨集的驗證 198 | validates :email, presence: true 199 | validates :username, presence: true 200 | validates :username, uniqueness: { case_sensitive: false } 201 | validates :username, format: { with: /\A[A-Za-z][A-Za-z0-9._-]{2,19}\z/ } 202 | validates :password, format: { with: /\A\S{8,128}\z/, allow_nil: true} 203 | 204 | # 接著是回呼 205 | before_save :cook 206 | before_save :update_username_lower 207 | 208 | # 其它的巨集 (像 devise 的) 應該放在回呼的後面 209 | 210 | ... 211 | end 212 | ``` 213 | 214 | * 偏好 `has_many :through` 勝於 `has_and_belongs_to_many`。使用 `has_many :through` 允許在 join 資料模型有附加的屬性及驗證 215 | 216 | ```Ruby 217 | # 使用 has_and_belongs_to_many 218 | class User < ActiveRecord::Base 219 | has_and_belongs_to_many :groups 220 | end 221 | 222 | class Group < ActiveRecord::Base 223 | has_and_belongs_to_many :users 224 | end 225 | 226 | # 建議的寫法 - 使用 has_many :through 227 | class User < ActiveRecord::Base 228 | has_many :memberships 229 | has_many :groups, through: :memberships 230 | end 231 | 232 | class Membership < ActiveRecord::Base 233 | belongs_to :user 234 | belongs_to :group 235 | end 236 | 237 | class Group < ActiveRecord::Base 238 | has_many :memberships 239 | has_many :users, through: :memberships 240 | end 241 | ``` 242 | 243 | * 務必使用新的 ["sexy" validation](http://thelucid.com/2010/01/08/sexy-validation-in-edge-rails-rails-3/)。 244 | * 如果一個自訂的驗證程序使用超過一次,或驗證程序是透過某個正則表達式的時候,請建立一個自訂的 validator 檔。 245 | 246 | ```Ruby 247 | # 劣 248 | class Person 249 | validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i } 250 | end 251 | 252 | # 優 253 | class EmailValidator < ActiveModel::EachValidator 254 | def validate_each(record, attribute, value) 255 | record.errors[attribute] << (options[:message] || 'is not a valid email') unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i 256 | end 257 | end 258 | 259 | class Person 260 | validates :email, email: true 261 | end 262 | ``` 263 | 264 | * 所有自訂的驗證器應放在一個共享的 gem 。 265 | * 可任意使用具名的作用域 (scope)。 266 | 267 | ```Ruby 268 | class User < ActiveRecord::Base 269 | scope :active, -> { where(active: true) } 270 | scope :inactive, -> { where(active: false) } 271 | 272 | scope :with_orders, -> { joins(:orders).select('distinct(users.id)') } 273 | end 274 | ``` 275 | 276 | * 將具名的作用域包在 `lambda` 裡使其延遲初始化。 277 | 278 | ```Ruby 279 | # 劣 280 | class User < ActiveRecord::Base 281 | scope :active, where(active: true) 282 | scope :inactive, where(active: false) 283 | 284 | scope :with_orders, joins(:orders).select('distinct(users.id)') 285 | end 286 | 287 | # 優 288 | class User < ActiveRecord::Base 289 | scope :active, -> { where(active: true) } 290 | scope :inactive, -> { where(active: false) } 291 | 292 | scope :with_orders, -> { joins(:orders).select('distinct(users.id)') } 293 | end 294 | ``` 295 | 296 | *按:在 Rails 4 會強制使用 lambda* 297 | 298 | * 當一個由 lambda 及參數定義的作用域變得過於複雜時,更好的方式是建立一個作為同樣用途的類別方法,並回傳一個 `ActiveRecord::Relation` 物件。你也可以這麼定義更精簡的作用域。 299 | 300 | ```Ruby 301 | class User < ActiveRecord::Base 302 | def self.with_orders 303 | joins(:orders).select('distinct(users.id)') 304 | end 305 | end 306 | ``` 307 | 308 | * 注意 `update_attribute` 方法的行為。它不會執行資料模型驗證(不同於 `update_attributes` )並且可能把資料模型狀態給搞砸。 309 | * 使用用戶友好的網址。在網址顯示具描述性的資料模型屬性,而不只是 `id` 。 310 | 有不止一種方法可以達成: 311 | * 覆寫資料模型的 `to_param` 方法。這是 Rails 用來給物件建立網址的方法。預設的實作會以字串形式回傳該 `id` 的記錄。它可以用另一個人類可讀的屬性來覆寫。 312 | 313 | ```Ruby 314 | class Person 315 | def to_param 316 | "#{id} #{name}".parameterize 317 | end 318 | end 319 | ``` 320 | 321 | 為了要轉換成對網址友好 (URL-friendly)的值,字串應當呼叫 `parameterize` 。物件的 `id` 要放在開頭,以便給 ActiveRecord 的 `find` 方法查找。 322 | 323 | * 使用 `friendly_id` gem。它允許藉由某些具描述性的資料模型屬性來建立人類可讀的網址,而不是用 `id` 。 324 | 325 | ```Ruby 326 | class Person 327 | extend FriendlyId 328 | friendly_id :name, use: :slugged 329 | end 330 | ``` 331 | 332 | 查看 [gem 說明文件](https://github.com/norman/friendly_id)獲得更多關於使用的資訊。 333 | 334 | ### ActiveResource 335 | 336 | * 如果 HTTP 回應是一個與現有的格式(XML 和 JSON)不同的格式,或是需要某些額外的格式解析,這時候請建立一個自訂格式,並在類別中使用它。自訂格式應當實作下列方法:`extension`, `mime_type`, 337 | `encode` 以及 `decode`。 338 | 339 | ```Ruby 340 | module ActiveResource 341 | module Formats 342 | module Extend 343 | module CSVFormat 344 | extend self 345 | 346 | def extension 347 | 'csv' 348 | end 349 | 350 | def mime_type 351 | 'text/csv' 352 | end 353 | 354 | def encode(hash, options = nil) 355 | # 資料以新格式編碼並回傳 356 | end 357 | 358 | def decode(csv) 359 | # 資料以新格式解碼並回傳 360 | end 361 | end 362 | end 363 | end 364 | end 365 | 366 | class User < ActiveResource::Base 367 | self.format = ActiveResource::Formats::Extend::CSVFormat 368 | 369 | ... 370 | end 371 | ``` 372 | 373 | * 若要讓產生的網址不包含副檔名,請覆寫 `ActiveResource::Base` 的 `element_path` 及 `collection_path` 方法,並移除副檔名。 374 | 375 | ```Ruby 376 | class User < ActiveResource::Base 377 | ... 378 | 379 | def self.collection_path(prefix_options = {}, query_options = nil) 380 | prefix_options, query_options = split_options(prefix_options) if query_options.nil? 381 | "#{prefix(prefix_options)}#{collection_name}#{query_string(query_options)}" 382 | end 383 | 384 | def self.element_path(id, prefix_options = {}, query_options = nil) 385 | prefix_options, query_options = split_options(prefix_options) if query_options.nil? 386 | "#{prefix(prefix_options)}#{collection_name}/#{URI.parser.escape id.to_s}#{query_string(query_options)}" 387 | end 388 | end 389 | ``` 390 | 391 | 如有任何改動網址的需求時,這些方法也可以被覆寫。 392 | 393 | ## 遷移 (Migrations) 394 | 395 | * 把 `schema.rb` 放進版本控制系統裡面。 396 | * 用 `rake db:scheme:load` 來初始化空的資料庫,而不是用 `rake db:migrate`。 397 | * 用 `rake db:test:prepare` 來更新測試資料庫的 schema。 398 | * 避免在資料表裡放預設資料。請使用資料模型層來取代。 399 | 400 | ```Ruby 401 | def amount 402 | self[:amount] or 0 403 | end 404 | ``` 405 | 406 | 然而 `self[:attr_name]` 卻相當常見,你也可以考慮使用更繁瑣的 `read_attribute` 來取代(有爭議,但更好讀): 407 | 408 | ```Ruby 409 | def amount 410 | read_attribute(:amount) or 0 411 | end 412 | ``` 413 | 414 | * 在寫建設性的遷移時(加表格或加欄位),請使用 Rails 3.1 的新方式 - 使用 `change` 方法取代 `up` 與 `down` 方法。 415 | 416 | 417 | ```Ruby 418 | # 以前的寫法 419 | class AddNameToPerson < ActiveRecord::Migration 420 | def up 421 | add_column :persons, :name, :string 422 | end 423 | 424 | def down 425 | remove_column :person, :name 426 | end 427 | end 428 | 429 | # 推薦的新寫法 430 | class AddNameToPerson < ActiveRecord::Migration 431 | def change 432 | add_column :persons, :name, :string 433 | end 434 | end 435 | ``` 436 | 437 | ## 視圖 (Views) 438 | 439 | * 不要直接從視圖呼叫資料模型層 (Model)。 440 | * 不要在視圖裡做複雜的格式化,把它們寫成方法丟到 helper 或 model 裡面。 441 | * 使用 partial view 與佈局 (layouts) 來減少重複的程式碼。 442 | * 給自訂的檢驗器 (validators) 加上 [瀏覽器端的驗證器](https://github.com/bcardarella/client_side_validations)。方法如下: 443 | * 宣告一個由 `ClientSideValidations::Middleware::Base` 繼承來的自訂 validator 444 | 445 | ```Ruby 446 | module ClientSideValidations::Middleware 447 | class Email < Base 448 | def response 449 | if request.params[:email] =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i 450 | self.status = 200 451 | else 452 | self.status = 404 453 | end 454 | super 455 | end 456 | end 457 | end 458 | ``` 459 | 460 | * 開新檔案 461 | `public/javascripts/rails.validations.custom.js.coffee` 並且包進 `application.js.coffee` 裡面: 462 | 463 | ```Ruby 464 | # app/assets/javascripts/application.js.coffee 465 | #= require rails.validations.custom 466 | ``` 467 | 468 | * 加上瀏覽器端的驗證器: 469 | 470 | ```Ruby 471 | #public/javascripts/rails.validations.custom.js.coffee 472 | clientSideValidations.validators.remote['email'] = (element, options) -> 473 | if $.ajax({ 474 | url: '/validators/email.json', 475 | data: { email: element.val() }, 476 | async: false 477 | }).status == 404 478 | return options.message || 'invalid e-mail format' 479 | ``` 480 | 481 | ## 國際化 (I18n) 482 | 483 | * 視圖、資料模型與控制器裡都不應該使用特定語言的設定值或字串。這些文字應搬到在 `config/locales` 下的語系檔裡。 484 | * 要翻譯 ActiveRecord 資料模型的文字標籤時,請使用 `activerecord` 作用域: 485 | 486 | ``` 487 | zh-TW: 488 | activerecord: 489 | models: 490 | user: "會員" 491 | attributes: 492 | user: 493 | name: "全名" 494 | ``` 495 | 496 | 這樣子 `User.model_name.human` 會回傳 "會員" ,而 `User.human_attribute_name("name")` 會回傳 "全名"。這些屬性的翻譯會被視圖作為標籤使用。 497 | 498 | * 把在視圖使用的文字與 ActiveRecord 的屬性翻譯分別放在不同的資料夾。把給資料模型使用的語系檔放在名為 `models` 的資料夾,給視圖使用的文字放在名為 `views` 的資料夾。 499 | * 把額外的語系檔放進各別資料夾之後,要在 `application.rb` 檔裡面指定這些資料夾,才能載入。 500 | 501 | ```Ruby 502 | # config/application.rb 503 | config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,ym​​l}')] 504 | ``` 505 | 506 | * 把共用的語系選項,像是日期或貨幣格式,直接放在 `locale` 資料夾底下。 507 | * 請使用精簡形式的 I18n 方法: `I18n.t` ,而不是 `I18n.translate` ;使用 `I18n.l` ,而不是 `I18n.localize`。 508 | * 使用「懶惰法」來查詢視圖中使用的文字。假設我們有以下結構: 509 | 510 | ``` 511 | zh-TW: 512 | users: 513 | show: 514 | title: "使用者詳細資料" 515 | ``` 516 | 517 | `users.show.title` 的值在 `app/views/users/show.html.haml` 裡面可以這樣子查到: 518 | 519 | ```Ruby 520 | = t '.title' 521 | ``` 522 | 523 | * 在控制器與資料模型使用「點分隔」的 key,來取代指定 `:scope` 選項。點分隔的呼叫方式,更容易閱讀及追蹤層級。 524 | 525 | ```Ruby 526 | # 這樣子呼叫 527 | I18n.t 'activerecord.errors.messages.record_invalid' 528 | 529 | # 而不是這樣 530 | I18n.t :record_invalid, :scope => [:activerecord, :errors, :messages] 531 | ``` 532 | 533 | * 關於 Rails i18n 更詳細的資訊可以在 [Rails Guides](http://guides.rubyonrails.org/i18n.html) 找到。 534 | 535 | ## 資產 (Assets) 536 | 537 | 在應用程式裡,利用 [Assets Pipeline](http://guides.rubyonrails.org/asset_pipeline.html) 來組織資產。 538 | 539 | * 保留 `app/assets` 給自定的樣式表、Javascripts 或圖片。 540 | * 把自己開發,但不屬於應用程式本身的函式庫,放在 `lib/assets`。 541 | * 第三方程式如 [jQuery](http://jquery.com/) 或 [bootstrap](http://twitter.github.com/bootstrap/) 應放在`vendor/assets`。 542 | * 盡可能使用包成 gem 的 assets 。 (如: [jquery-rails](https://github.com/rails/jquery-rails))。 543 | 544 | ## 郵件 (Mailers) 545 | 546 | * 把 mails 命名為 `SomethingMailer`。沒有 Mailer 結尾的話,不能一望即知誰是 Mailer,以及跟哪個視圖有關。 547 | * 要同時提供 HTML 與純文字的視圖模版。 548 | * 在你的開發環境打開寄信失敗時要拋出錯誤的選項。這些錯誤預設是不會拋出的。 549 | 550 | ```Ruby 551 | # config/environments/development.rb 552 | 553 | config.action_mailer.raise_delivery_errors = true 554 | ``` 555 | 556 | * 在開發環境使用 `smtp.gmail.com` 設定 SMTP 伺服器(當然了,除非你自己有本機 SMTP 伺服器)。 557 | 558 | ```Ruby 559 | # config/environments/development.rb 560 | 561 | config.action_mailer.smtp_settings = { 562 | address: 'smtp.gmail.com', 563 | # 更多設定 564 | } 565 | ``` 566 | 567 | * 要提供預設的主機名稱 (hostname)。 568 | 569 | ```Ruby 570 | # config/environments/development.rb 571 | config.action_mailer.default_url_options = {host: "#{local_ip}:3000"} 572 | 573 | 574 | # config/environments/production.rb 575 | config.action_mailer.default_url_options = {host: 'your_site.com'} 576 | 577 | # 在你的 mailer 類別 578 | default_url_options[:host] = 'your_site.com' 579 | ``` 580 | 581 | * 如果你需要在 email 裡設超連結到你的網站,務必使用 `_url` 方法,而不是 `_path` 方法。 `_url` 方法包含了主機名稱,而 `_path` 方法則沒有。 582 | 583 | ```Ruby 584 | # 錯誤 585 | You can always find more info about this course 586 | = link_to 'here', url_for(course_path(@course)) 587 | 588 | # 正確 589 | You can always find more info about this course 590 | = link_to 'here', url_for(course_url(@course)) 591 | ``` 592 | 593 | * 應把寄件人與收件人地址的格式給寫正確。格式如下: 594 | 595 | ```Ruby 596 | # 在你的 mailer 類別 597 | default from: 'Your Name ' 598 | ``` 599 | 600 | * 確認測試環境的 email 發送方法設定為 `test` : 601 | 602 | ```Ruby 603 | # config/environments/test.rb 604 | 605 | config.action_mailer.delivery_method = :test 606 | ``` 607 | 608 | * 開發環境與生產環境的發送方法應為 `smtp` : 609 | 610 | ```Ruby 611 | # config/environments/development.rb, config/environments/production.rb 612 | 613 | config.action_mailer.delivery_method = :smtp 614 | ``` 615 | 616 | * 當發送 HTML email 時,所有樣式應為行內樣式 (inline style),這是由於某些 email 軟體處理外部樣式表會有問題。雖然這樣會讓程式更難維護、程式碼也容易重覆。有兩個 gem 可以把樣式表轉換成行內樣式,並將放在對應的 html 標籤裡: [premailer-rails3](https://github.com/fphilipe/premailer-rails3) 和 [roadie](https:// github.com/Mange/roadie)。 617 | 618 | * 應避免在發送回應的同時同步寄出 email,因為這樣會造成網頁載入時間過久、而且要是寄送多個 email 還可能會造成連線逾時。請使用 [delayed_job](https://github.com/tobi/delayed_job) gem 來把寄送 email 放到背景去處理。 619 | 620 | ## 打包 (Bundler) 621 | 622 | * 把只給開發環境或測試環境的 gem 在 Gemfile 檔裡面妥善分組。 623 | * 在你的專案中只使用公認的 gem。如果你考慮引入某些鮮為人所知的 gem ,你應該先仔細審查它的原始碼。 624 | * 要是開發人員各自使用不同的作業系統,那麼與作業系統相關的那些 gem 會導致 `Gemfile.lock` 經常變動。解決方法是,在 Gemfile 裡,把與 OS X 相關的 gem 放在 `darwin` 群組,與 Linux 相關的 gem 放在 `linux` 群組: 625 | 626 | ```Ruby 627 | # Gemfile 628 | group :darwin do 629 | gem 'rb-fsevent' 630 | gem 'growl' 631 | end 632 | 633 | group :linux do 634 | gem 'rb-inotify' 635 | end 636 | ``` 637 | 638 | 要在正確的環境 require 正確的 gem,請新增以下程式碼至 `config/application.rb` : 639 | 640 | ```Ruby 641 | platform = RUBY_PLATFORM.match(/(linux|darwin)/)[0].to_sym 642 | Bundler.require(platform) 643 | ``` 644 | 645 | * 不要把 `Gemfile.lock` 檔從版本控制系統裡移出。這不是隨機產生的文件──它確保你所有的成員執行 `bundle install` 時,都拿到相同版本的 gem 。 646 | 647 | ## 貴重的 Gems 648 | 649 | 一個最重要的程式設計理念是「不要重造輪子!」。若你遇到一個特定問題,你應該要在你開始手刻之前,找一下是否有現有的解決方案。以下是一些在很多 Rails 專案中的「無價至寶」 gem 列表(全部相容 Rails 3.1): 650 | 651 | * [active_admin](https://github.com/gregbell/active_admin) - 有了 ActiveAdmin,建立 Rails 應用的管理介面就像兒戲。你會有一個很好的後台,圖形化 CRUD 介面以及更多東西。非常靈活且可客製化。 652 | * [better_errors](https://github.com/charliesome/better_errors) - Better Errors 用更好更有效的錯誤頁面,取代了 Rails 標準的錯誤頁面。不僅可用在 Rails,任何將 Rack 當作 middleware 的 app 都可使用。 653 | * [bullet](https://github.com/flyerhzm/bullet) - Bullet 就是為了幫助你提升應用程式的效能而打造的 gem (藉由減少資料庫查詢)。會在你開發應用程式時,替你注意你的資料庫查詢,並在需要 eager loading (N+1 查詢) 時、或你在不必要的情況使用 eager loading 時,或是在應該要使用 counter cache 時,都會提醒你。 654 | * [cancan](https://github.com/ryanb/cancan) - CanCan 是一個權限管理的 gem, 655 | 讓你可以管制用戶可存取的資源。所有的權限都定義在一個檔案裡(ability.rb),並提供許多方便的方法,讓你在整個應用程式裡都可以檢查及確保權限是否獲准。 656 | * [capybara](https://github.com/jnicklas/capybara) - Capybara 旨在簡化整合測試 Rack 應用程式的流程,像是 Rails、Sinatra 或 Merb。 Capybara 模擬了真實用戶使用 web 應用程式的互動過程。它與你用的測試工具無關,並原生搭載 Rack::Test 及 Selenium 支援。透過外部 gem 支援 HtmlUnit、WebKit 及 env.js 。與 RSpec & Cucumber 一起使用時工作良好。 657 | * [carrierwave](https://github.com/jnicklas/carrierwave) - Rails 最新的檔案上傳的解決方案。支援上傳檔案到本地儲存與雲端儲存(及很多其它的酷玩意)。良好結合了 ImageMagick 來進行圖片後處理。 658 | * [client_side_validations](https://github.com/bcardarella/client_side_validations) - 659 | 一個很棒的 gem,替你從現有的伺服器端資料模型驗證,自動產生 Javascript 瀏覽器端驗證。強烈推薦! 660 | * [compass-rails](https://github.com/chriseppstein/compass) - 一個優秀的 gem,加入了某些 css 框架的支持。包括了一些 sass mixin ,讓你減少 css 檔的程式碼,並幫你解決瀏覽器相容問題。 661 | * [cucumber-rails](https://github.com/cucumber/cucumber-rails) - Cucumber 是一個由 Ruby 所寫,開發功能測試的頂級工具。 cucumber-rails 提供了 Cucumber 的 Rails 整合。 662 | * [devise](https://github.com/plataformatec/devise) - Devise 是 Rails 應用程式的登入系統完整解決方案。多數情況偏好使用 devise 來開始客製登入流程。 663 | * [fabrication](http://fabricationgem.org/) - 一個很好的 fixture 測資替代品(編輯推薦)。 664 | * [factory_girl](https://github.com/thoughtbot/factory_girl) - Fabrication 的替代品。一個成熟的 fixture 測資產生器。 Fabrication 的精神領袖先驅。 665 | * [ffaker](https://github.com/EmmanuelOga/ffaker) - 產生假資料的實用 gem(名字、地址,等等)。 666 | * [feedzirra](https://github.com/pauldix/feedzirra) - 非常快速、靈活的 RSS / Atom Feed 解析器。 667 | * [friendly_id](https://github.com/norman/friendly_id) - 透過使用某些具描述性的資料模型屬性,而不是使用 id,來讓你建立人類可讀的網址。 668 | * [globalize3](https://github.com/svenfuchs/globalize3.git) - Globalize3 是 Globalize Gem 的後繼者,針對 ActiveRecord 3.x 設計。基於新的 I18n API 打造而成,並幫 ActiveRecord 資料模型新增了交易功能 (transaction)。 669 | * [guard](https://github.com/guard/guard) - 監控檔案變化並呼叫任務的極佳 gem。搭載了很多實用的擴充。樂勝 autotest 與 [watchr](https://github.com/mynyml/watchr)。 670 | * [haml-rails](https://github.com/indirect/haml-rails) - haml-rails 提供了 Haml 的 Rails 整合。 671 | * [haml](http://haml-lang.com) - Haml 是一個簡潔的資料模型語言,被很多人認為(包括我)遠優於Erb。 672 | * [kaminari](https://github.com/amatsuda/kaminari) - 很棒的分頁解決方案。 673 | * [machinist](https://github.com/notahat/machinist) - fixture 測資不好玩,Machinist 才好玩。 674 | * [rspec-rails](https://github.com/rspec/rspec-rails) - RSpec 是 Test::MiniTest 的替代品。我不高度推薦 RSpec。 rspec-rails 提供了 RSpec 的 Rails 整合。 675 | * [simple_form](https://github.com/plataformatec/simple_form) - 一旦用過 simple_form(或 formatastic),你就回不去 Rails 預設的表單產生器了。它提供很棒的 DSL 可以建立表單,讓你不必在意表單的 HTML 怎麼寫。 676 | * [simplecov-rcov](https://github.com/fguillen/simplecov-rcov) - 為了 SimpleCov 打造的 RCov formatter。若你想使用 SimpleCov 搭配 Hudson 持續整合伺服器 (CI Server),它很有用。 677 | * [simplecov](https://github.com/colszowka/simplecov) - 檢查程式碼覆蓋率 (code coverage) 的工具。但不像 RCov,它完全相容 Ruby 1.9。它有精美的報表。必須用! 678 | * [slim](http://slim-lang.com) - Slim 是一個簡潔的模版語言,被視為是遠遠優於 HAML 的程式語言 (至於 Erb 就不用說了) 。唯一會阻止我大規模地使用它的是,主流 IDE 及編輯器對它的支援不好。但它的效能是非凡的。 679 | * [spork](https://github.com/sporkrb/spork) - 一個給測試框架(RSpec / Cucumber)用的 DRb 伺服器,每次運行前確保 fork 出一個乾淨的測試狀態。簡單的說,預載很多測試環境的結果是大幅降低你的測試啟動時間,絕對必須用! 680 | * [sunspot](https://github.com/sunspot/sunspot) - 基於 SOLR 的全文搜尋引擎。 681 | 682 | 這不是完整的清單,其它的 gem 也可以在之後加進來。以上清單上的所有 gems 皆經測試,處於活躍開發階段,有社群,程式碼的品質很高。 683 | 684 | ## 有缺陷的 Gems 685 | 686 | 這是一個有問題的或被別的 gem 取代的 gem 清單。你應該在你的專案裡避免使用它們。 687 | 688 | * [rmagick](http://rmagick.rubyforge.org/) - 這個 gem 因大量消耗記憶體而聲名狼藉。請改用 [minimagick](https://github.com/probablycorey/mini_magick)。 689 | * [autotest](http://www.zenspider.com/ZSS/Products/ZenTest/) - 自動化測試的舊方法。遠不如 guard 及 [watchr](https://github.com/mynyml/watchr)。 690 | * [rcov](https://github.com/relevance/rcov) - 程式碼覆蓋率工具,不相容於 Ruby 1.9。請改用 [SimpleCov](https://github.com/colszowka/simplecov)。 691 | * [therubyracer](https://github.com/cowboyd/therubyracer) - 極度不鼓勵在生產模式使用這個 gem,它會消耗大量的記憶體。我會推薦改用 `node.js`。 692 | 693 | 這仍是一個完善中的清單。請告訴我受人歡迎但有缺陷的 gems 。 694 | 695 | ## 管理處理程序 (process) 696 | 697 | * 若你的專案依賴各種外部的處理程序,使用 [foreman](https://github.com/ddollar/foreman) 來管理它們。 698 | 699 | # 測試 Rails 應用程式 700 | 701 | 也許 BDD 方法是實作一個新功能最好的方法。你從開始寫一些高階的測試(通常使用 Cucumber),然後使用這些測試來驅使你實作功能。一開始你給功能的視圖寫測試,並使用這些測試來建立相關的視圖。接著,你寫控制器測試要求把資料丟給視圖用,藉此來實作控制器。最後你實作資料模型的測試,以及資料模型自身。 702 | 703 | ## Cucumber 704 | 705 | * 用 `@wip` (工作進行中)標籤來標記尚未完成的情境 (scenario)。這些情境將不納入考慮,且不會被標記為測試失敗。當完成這個情境且功能測試通過時,為了把此情境加至測試套件裡,請移除 `@wip` 標籤。 706 | * 修改預設的 profile,排除掉標記為 `@javascript` 的情境。它們將使用瀏覽器來測試,建議停用它們來增進一般情境的執行速度。 707 | * 替標記著 `@javascript` 的情境,設定另一個 profile。 708 | * profile 可在 `cucumber.yml` 檔案裡設定。 709 | 710 | ```Ruby 711 | # profile 的定義: 712 | profile_name: --tags @tag_name 713 | ``` 714 | 715 | * 用這個指令來執行特定的 profile: 716 | 717 | ``` 718 | cucumber -p profile_name 719 | ``` 720 | 721 | * 若使用 [fabrication](http://fabricationgem.org/) 來替換 fixtures,請使用預先定義的 [fabrication steps](http://fabricationgem.org/#!cucumber-steps)。 722 | * 不要使用舊的 `web_steps.rb` 來定義步驟![最新版 Cucumber 已移除 web steps](http://aslakhellesoy.com/post/11055981222/the-training-wheels-came-off) ,用這個會導致多餘的情境,這些情境無法正確反映出應用程式的領域。 723 | * 當檢查一元素的可見文字時(如超連結、按鈕),請檢查元素的文字而不是檢查 id。這樣可以查出 i18n 的問題。 724 | * 為同物件的各種功能,各自建立不同的 feature: 725 | 726 | ```Ruby 727 | # 劣 728 | Feature: Articles 729 | # ... 功能實作 ... 730 | 731 | # 優 732 | Feature: Article Editing 733 | # ... 功能實作 ... 734 | 735 | Feature: Article Publishing 736 | # ... 功能實作 ... 737 | 738 | Feature: Article Search 739 | # ... 功能實作 ... 740 | 741 | ``` 742 | 743 | * 每一個 feature 有三個主要成分: 744 | * Title 745 | * Narrative - 簡短說明這個 feature 關於什麼。 746 | * Acceptance criteria - 每個由獨立步驟組成的一套情境。 747 | * 最常見的格式稱為 Connextra 格式。 748 | 749 | ```Ruby 750 | In order to [benefit] ... 751 | A [stakeholder]... 752 | Wants to [feature] ... 753 | ``` 754 | 755 | 這種格式最常見,但並不強求要這樣寫, narrative 敘述句可以因功能的複雜度而任意書寫。 756 | 757 | * 可任意使用情境概述使你的情境可備作它用(keep your scenarios DRY)。 758 | 759 | ```Ruby 760 | Scenario Outline: User cannot register with invalid e-mail 761 | When I try to register with an email "" 762 | Then I should see the error message "" 763 | 764 | Examples: 765 | |email |error | 766 | | |The e-mail is required| 767 | |invalid email |is not a valid e-mail | 768 | ``` 769 | 770 | * 情境的步驟放在 `step_definitions` 目錄下的 `.rb` 檔。步驟檔命名慣例為 `[description]_steps.rb`。步驟根據不同的標準放在不同的檔案裡。每一個 feature 可能有一個步驟檔(`home_page_steps.rb`) 771 | 。也可以給每個特定物件的 feature,開一個步驟檔(`articles_steps.rb`)。 772 | * 使用多行步驟參數來避免重複 773 | 774 | ```Ruby 775 | Scenario: User profile 776 | Given I am logged in as a user "John Doe" with an e-mail "user@test.com" 777 | When I go to my profile 778 | Then I should see the following information: 779 | |First name|John | 780 | |Last name |Doe | 781 | |E-mail |user@test.com| 782 | 783 | # 步驟: 784 | Then /^I should see the following information:$/ do |table| 785 | table.raw.each do |field, value| 786 | find_field(field).value.should =~ /#{value}/ 787 | end 788 | end 789 | ``` 790 | 791 | * 使用複合步驟來讓情境可備作它用(Keep your scenarios DRY) 792 | 793 | ```Ruby 794 | # ... 795 | When I subscribe for news from the category "Technical News" 796 | # ... 797 | 798 | # 步驟: 799 | When /^I subscribe for news from the category "([^"]*)"$/ do |category| 800 | steps %Q{ 801 | When I go to the news categories page 802 | And I select the category #{category} 803 | And I click the button "Subscribe for this category" 804 | And I confirm the subscription 805 | } 806 | end 807 | ``` 808 | * 務必使用 Capybara 的否定配對來取代在肯定情況裡使用 should_not,這樣子當 ajax 操作逾時就會重試。見 [Capybara 的 README 檔](https://github.com/jnicklas/capybara)獲得更多說明。 809 | 810 | ## RSpec 811 | 812 | * 每個測試案例應只有一個期望值 (expection)。 813 | 814 | ```Ruby 815 | # 劣 816 | describe ArticlesController do 817 | #... 818 | 819 | describe 'GET new' do 820 | it 'assigns new article and renders the new article template' do 821 | get :new 822 | assigns[:article].should be_a_new Article 823 | response.should render_template :new 824 | end 825 | end 826 | 827 | # ... 828 | end 829 | 830 | # 優 831 | describe ArticlesController do 832 | #... 833 | 834 | describe 'GET new' do 835 | it 'assigns a new article' do 836 | get :new 837 | assigns[:article].should be_a_new Article 838 | end 839 | 840 | it 'renders the new article template' do 841 | get :new 842 | response.should render_template :new 843 | end 844 | end 845 | 846 | end 847 | ``` 848 | 849 | * 應大量使用 `describe` 及 `context` 。 850 | * `describe` 區塊的命名方式應如下: 851 | * 非方法使用 "description" 852 | * 實體方法使用井字號 "#method" 853 | * 類別方法使用點 ".method" 854 | 855 | ```Ruby 856 | class Article 857 | def summary 858 | #... 859 | end 860 | 861 | def self.latest 862 | #... 863 | end 864 | end 865 | 866 | # the spec... 867 | describe Article do 868 | describe '#summary' do 869 | #... 870 | end 871 | 872 | describe '.latest' do 873 | #... 874 | end 875 | end 876 | ``` 877 | 878 | * 使用 [fabricators](http://fabricationgem.org/) 來建立測資物件。 879 | 880 | * 應大量使用 mocks 與 stubs。 881 | 882 | ```Ruby 883 | # mocking 一個資料模型 884 | article = mock_model(Article) 885 | 886 | # stubbing 一個方法 887 | Article.stub(:find).with(article.id).and_return(article) 888 | ``` 889 | 890 | * 當 mocking 一個資料模型時,可以使用 `as_null_object` 方法,讓輸出的物件只回應我們有 stub 的方法,不理會其他方法。 891 | 892 | ```Ruby 893 | article = mock_model(Article).as_null_object 894 | ``` 895 | 896 | * 使用 `let` 區塊,不要用 `before(:each)` 區塊來為 spec 測試案例建立資料。 `let` 區塊會被延遲求值 (lazily evaluated)。 897 | 898 | ```Ruby 899 | # 使用這個: 900 | let(:article) { Fabricate(:article) } 901 | 902 | # ... 而不是這個: 903 | before(:each) { @article = Fabricate(:article) } 904 | ``` 905 | 906 | * 盡可能使用 `subject`。 907 | 908 | ```Ruby 909 | describe Article do 910 | subject { Fabricate(:article) } 911 | 912 | it 'is not published on creation' do 913 | subject.should_not be_published 914 | end 915 | end 916 | ``` 917 | 918 | * 盡可能使用 `specify`。它是 `it` 的同義詞,但在沒 docstring 的情況下更好讀。 919 | 920 | ```Ruby 921 | # 劣 922 | describe Article do 923 | before { @article = Fabricate(:article) } 924 | 925 | it 'is not published on creation' do 926 | @article.should_not be_published 927 | end 928 | end 929 | 930 | # 優 931 | describe Article do 932 | let(:article) { Fabricate(:article) } 933 | specify { article.should_not be_published } 934 | end 935 | ``` 936 | 937 | * 盡可能使用 `its` 。 938 | 939 | ```Ruby 940 | # 劣 941 | describe Article do 942 | subject { Fabricate(:article) } 943 | 944 | it 'has the current date as creation date' do 945 | subject.creation_date.should == Date.today 946 | end 947 | end 948 | 949 | # 優 950 | describe Article do 951 | subject { Fabricate(:article) } 952 | its(:creation_date) { should == Date.today } 953 | end 954 | ``` 955 | 956 | * 如果要建立共用的 spec 群組,請使用 `shared_examples`。 957 | 958 | ```Ruby 959 | # 劣 960 | describe Array do 961 | subject { Array.new [7, 2, 4] } 962 | 963 | context "initialized with 3 items" do 964 | its(:size) { should eq(3) } 965 | end 966 | end 967 | 968 | describe Set do 969 | subject { Set.new [7, 2, 4] } 970 | 971 | context "initialized with 3 items" do 972 | its(:size) { should eq(3) } 973 | end 974 | end 975 | 976 | # 優 977 | shared_examples "a collection" do 978 | subject { described_class.new([7, 2, 4]) } 979 | 980 | context "initialized with 3 items" do 981 | its(:size) { should eq(3) } 982 | end 983 | end 984 | 985 | describe Array do 986 | it_behaves_like "a collection" 987 | end 988 | 989 | describe Set do 990 | it_behaves_like "a collection" 991 | end 992 | 993 | 994 | ### 視圖 (Views) 995 | 996 | * 視圖測試的目錄結構要與 `app/views` 裡面的結構一致。舉例來說, `app/views/users` 的視圖測試應放在 `spec/views/users`。 997 | * 視圖測試的命名慣例是把 `_spec.rb` 加到視圖名稱的後面,舉例來說,視圖 `_form.html.haml` 有一個對應的測試叫做 `_form.html.haml_spec.rb`。 998 | * 每個視圖測試檔都需要 `spec_helper.rb`。 999 | * 外圍的 `describe` 區塊要使用不包含 `app/views` 前綴的視圖路徑,這在 `render` 方法沒有傳入參數的時候會用到。 1000 | 1001 | ```Ruby 1002 | # spec/views/articles/new.html.haml_spec.rb 1003 | require 'spec_helper' 1004 | 1005 | describe 'articles/new.html.haml' do 1006 | # ... 1007 | end 1008 | ``` 1009 | 1010 | * 務必要在視圖測試裡面 mock 資料模型。視圖的目的只有顯示資訊而已。 1011 | * 原本由控制器提供給視圖使用的實體變數(instance variable),可以用 `assign` 方法來提供。 1012 | 1013 | ```Ruby 1014 | # spec/views/articles/edit.html.haml_spec.rb 1015 | describe 'articles/edit.html.haml' do 1016 | it 'renders the form for a new article creation' do 1017 | assign( 1018 | :article, 1019 | mock_model(Article).as_new_record.as_null_object 1020 | ) 1021 | render 1022 | rendered.should have_selector('form', 1023 | method: 'post', 1024 | action: articles_path 1025 | ) do |form| 1026 | form.should have_selector('input', type: 'submit') 1027 | end 1028 | end 1029 | ``` 1030 | 1031 | * 最好使用 capybara 的否定情況選擇器,而非 should_not 配上正面情況。 1032 | 1033 | ```Ruby 1034 | # 劣 1035 | page.should_not have_selector('input', type: 'submit') 1036 | page.should_not have_xpath('tr') 1037 | 1038 | # 優 1039 | page.should have_no_selector('input', type: 'submit') 1040 | page.should have_no_xpath('tr') 1041 | ``` 1042 | 1043 | * 當視圖要使用 helper 方法時,要先把這些方法給 stub 掉,這件事要在 `template` 物件裡面做: 1044 | 1045 | ```Ruby 1046 | # app/helpers/articles_helper.rb 1047 | class ArticlesHelper 1048 | def formatted_date(date) 1049 | # ... 1050 | end 1051 | end 1052 | 1053 | # app/views/articles/show.html.haml 1054 | = "Published at: #{formatted_date(@article.published_at)}" 1055 | 1056 | # spec/views/articles/show.html.haml_spec.rb 1057 | describe 'articles/show.html.haml' do 1058 | it 'displays the formatted date of article publishing' do 1059 | article = mock_model(Article, published_at: Date.new(2012, 01, 01)) 1060 | assign(:article, article) 1061 | 1062 | template.stub(:formatted_date).with(article.published_at).and_return('01.01.2012') 1063 | 1064 | render 1065 | rendered.should have_content('Published at: 01.01.2012') 1066 | end 1067 | end 1068 | ``` 1069 | 1070 | * helper specs 測試檔要要從視圖 specs 測試裡面拆出來,放在 `spec/helpers` 目錄下。 1071 | 1072 | ### 控制器 1073 | 1074 | * 請 mock 資料模型並 stub 他們的方法。測試控制器時不應依賴於資料模型的建立。 1075 | * 請只測試控制器需負責的行為: 1076 | * 某幾個特定方法的執行 1077 | * 從動作 (action) 回傳的資料 - assigns, 等等。 1078 | * 動作所產生的結果 - template render, redirect, 等等。 1079 | 1080 | ```Ruby 1081 | # 常用的控制器 spec 範例 1082 | # spec/controllers/articles_controller_spec.rb 1083 | # 我們只對控制器應執行的動作感興趣 1084 | # 所以我們 mock 資料模型及 stub 它的方法 1085 | # 並且專注在控制器該做的事情上 1086 | 1087 | describe ArticlesController do 1088 | # 資料模型將會在測試中被所有控制器的方法所使用 1089 | let(:article) { mock_model(Article) } 1090 | 1091 | describe 'POST create' do 1092 | before { Article.stub(:new).and_return(article) } 1093 | 1094 | it 'creates a new article with the given attributes' do 1095 | Article.should_receive(:new).with(title: 'The New Article Title').and_return(article) 1096 | post :create, message: { title: 'The New Article Title' } 1097 | end 1098 | 1099 | it 'saves the article' do 1100 | article.should_receive(:save) 1101 | post :create 1102 | end 1103 | 1104 | it 'redirects to the Articles index' do 1105 | article.stub(:save) 1106 | post :create 1107 | response.should redirect_to(action: 'index') 1108 | end 1109 | end 1110 | end 1111 | ``` 1112 | 1113 | * 當控制器根據不同參數有不同行為時,請使用 context。 1114 | 1115 | ```Ruby 1116 | # 一個在控制器中使用 context 的典型例子是,建立或更新物件時,可能因為儲存成功與否而有不同行為。 1117 | 1118 | describe ArticlesController do 1119 | let(:article) { mock_model(Article) } 1120 | 1121 | describe 'POST create' do 1122 | before { Article.stub(:new).and_return(article) } 1123 | 1124 | it 'creates a new article with the given attributes' do 1125 | Article.should_receive(:new).with(title: 'The New Article Title').and_return(article) 1126 | post :create, article: { title: 'The New Article Title' } 1127 | end 1128 | 1129 | it 'saves the article' do 1130 | article.should_receive(:save) 1131 | post :create 1132 | end 1133 | 1134 | context 'when the article saves successfully' do 1135 | before { article.stub(:save).and_return(true) } 1136 | 1137 | it 'sets a flash[:notice] message' do 1138 | post :create 1139 | flash[:notice].should eq('The article was saved successfully.') 1140 | end 1141 | 1142 | it 'redirects to the Articles index' do 1143 | post :create 1144 | response.should redirect_to(action: 'index') 1145 | end 1146 | end 1147 | 1148 | context 'when the article fails to save' do 1149 | before { article.stub(:save).and_return(false) } 1150 | 1151 | it 'assigns @article' do 1152 | post :create 1153 | assigns[:article].should be_eql(article) 1154 | end 1155 | 1156 | it 're-renders the "new" template' do 1157 | post :create 1158 | response.should render_template('new') 1159 | end 1160 | end 1161 | end 1162 | end 1163 | ``` 1164 | 1165 | ### 資料模型 1166 | 1167 | * 不要在資料模型自己的測試裡 mock 該資料模型。 1168 | * 使用 fabrication 來建立實際的物件 1169 | * 可以 mock 別的資料模型或子物件。 1170 | * 為避免重覆,請在測試裡建立可以給所有測試案例使用的資料模型。 1171 | 1172 | ```Ruby 1173 | describe Article do 1174 | let(:article) { Fabricate(:article) } 1175 | end 1176 | ``` 1177 | 1178 | * 新增一個測試案例來確保 fabrication 做出來的資料模型是可以用的。 1179 | 1180 | ```Ruby 1181 | describe Article do 1182 | it 'is valid with valid attributes' do 1183 | article.should be_valid 1184 | end 1185 | end 1186 | ``` 1187 | 1188 | * 寫跟驗證程序有關的測試案例時,請使用 `have(x).errors_on` 來指定要被驗證的屬性。使用 `be_valid` 並不能保證問題一定會發生在要被驗證的屬性。 1189 | 1190 | ```Ruby 1191 | # 劣 1192 | describe '#title' do 1193 | it 'is required' do 1194 | article.title = nil 1195 | article.should_not be_valid 1196 | end 1197 | end 1198 | 1199 | # 推薦使用 1200 | describe '#title' do 1201 | it 'is required' do 1202 | article.title = nil 1203 | article.should have(1).error_on(:title) 1204 | end 1205 | end 1206 | ``` 1207 | 1208 | * 替每個有驗證程序的屬性,另外加另一個 `describe`。 1209 | 1210 | ```Ruby 1211 | describe Article do 1212 | describe '#title' 1213 | it 'is required' do 1214 | article.title = nil 1215 | article.should have(1).error_on(:title) 1216 | end 1217 | end 1218 | end 1219 | ``` 1220 | 1221 | * 當測試資料模型屬性的唯一性時,將另一個重覆的物件命名為 `another_object`。 1222 | 1223 | ```Ruby 1224 | describe Article do 1225 | describe '#title' do 1226 | it 'is unique' do 1227 | another_article = Fabricate.build(:article, title: article.title) 1228 | article.should have(1).error_on(:title) 1229 | end 1230 | end 1231 | end 1232 | ``` 1233 | 1234 | ### Mailers 1235 | 1236 | * 在 Mailer 測試的資料模型應該要被 mock 掉。 Mailer 不應依賴資料模型的建立。 1237 | * Mailer 的測試應該要檢驗這些: 1238 | * 主旨正確 1239 | * 收件人 e-mail 正確 1240 | * e-mail 有寄送至正確的 e-mail 地址 1241 | * e-mail 有包含所要寄送的訊息 1242 | 1243 | ```Ruby 1244 | describe SubscriberMailer do 1245 | let(:subscriber) { mock_model(Subscription, email: 'johndoe@test.com', name: 'John Doe') } 1246 | 1247 | describe 'successful registration email' do 1248 | subject { SubscriptionMailer.successful_registration_email(subscriber) } 1249 | 1250 | its(:subject) { should == 'Successful Registration!' } 1251 | its(:from) { should == ['info@your_site.com'] } 1252 | its(:to) { should == [subscriber.email] } 1253 | 1254 | it 'contains the subscriber name' do 1255 | subject.body.encoded.should match(subscriber.name) 1256 | end 1257 | end 1258 | end 1259 | ``` 1260 | 1261 | ### Uploaders 1262 | 1263 | * 我們可以測試上傳的圖片是否有正確產生縮圖。以下是 [carrierwave](https://github.com/jnicklas/carrierwave) 圖片上傳器的範例 spec: 1264 | 1265 | ```Ruby 1266 | # rspec/uploaders/person_avatar_uploader_spec.rb 1267 | require 'spec_helper' 1268 | require 'carrierwave/test/matchers' 1269 | 1270 | describe PersonAvatarUploader do 1271 | include CarrierWave::Test::Matchers 1272 | 1273 | # 在執行測試案例之前,先打開圖片處理 1274 | before(:all) do 1275 | UserAvatarUploader.enable_processing = true 1276 | end 1277 | 1278 | # 建立一個新的 uploader。要把資料模型給 mock 掉,使上傳及縮圖的時候不會依賴於資料模型的建立。 1279 | before(:each) do 1280 | @uploader = PersonAvatarUploader.new(mock_model(Person).as_null_object) 1281 | @uploader.store!(File.open(path_to_file)) 1282 | end 1283 | 1284 | # 執行完測試案例時,關閉圖片處理 1285 | after(:all) do 1286 | UserAvatarUploader.enable_processing = false 1287 | end 1288 | 1289 | # 測試縮圖是否不比給定的尺寸大 1290 | context 'the default version' do 1291 | it 'scales down an image to be no larger than 256 by 256 pixels' do 1292 | @uploader.should be_no_larger_than(256, 256) 1293 | end 1294 | end 1295 | 1296 | # 測試縮圖是否有完全一致的尺寸 1297 | context 'the thumb version' do 1298 | it 'scales down an image to be exactly 64 by 64 pixels' do 1299 | @uploader.thumb.should have_dimensions(64, 64) 1300 | end 1301 | end 1302 | end 1303 | ``` 1304 | 1305 | # 延伸閱讀 1306 | 1307 | 有幾個絕妙講述 Rails 風格的資源,若有閒暇時應當考慮閱讀之: 1308 | 1309 | * [The Rails 3 Way](http://www.amazon.com/Rails-Way-Addison-Wesley-Professional-Ruby/dp/0321601661) 1310 | * [Ruby on Rails Guides](http://guides.rubyonrails.org/) 1311 | * [The RSpec Book](http://pragprog.com/book/achbd/the-rspec-book) 1312 | * [The Cucumber Book](http://pragprog.com/book/hwcuc/the-cucumber-book) 1313 | * [Everyday Rails Testing with RSpec](https://leanpub.com/everydayrailsrspec) 1314 | 1315 | # 貢獻 1316 | 1317 | 在本指南所寫的每個東西都不是定案。這只是我渴望想與同樣對 Rails 程式設計風格有興趣的大家一起工作,這樣子最終我們可以創造出對整個 Ruby 社群都有益的資源。 1318 | 1319 | 歡迎開票或發送一個帶有改進的 Pull Request。在此提前感謝你的幫助! 1320 | 1321 | # 授權 1322 | 1323 | ![Creative Commons License](http://i.creativecommons.org/l/by/3.0/88x31.png) 1324 | This work is licensed under a [Creative Commons Attribution 3.0 Unported License](http://creativecommons.org/licenses/by/3.0/deed.zh_TW) 1325 | 1326 | # 口耳相傳 1327 | 1328 | 一份社群驅動的風格指南,對於沒聽過這份指南的其他社群人士來說,幾乎沒什麼用。請上 Twitter 轉貼這份指南,分享給你的朋友或同事。我們得到的每個註解、建議或意見都可以讓這份指南變得更好一點。而我們都想要有最好的指南,對吧? 1329 | 1330 | 共勉之,
1331 | [Bozhidar](https://twitter.com/bbatsov) 1332 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Prelude 2 | 3 | > Role models are important.
4 | > -- Officer Alex J. Murphy / RoboCop 5 | 6 | The goal of this guide is to present a set of best practices and style 7 | prescriptions for Ruby on Rails 4 development. It's a 8 | complementary guide to the already existing community-driven 9 | [Ruby coding style guide](https://github.com/bbatsov/ruby-style-guide). 10 | 11 | Some of the advice here is applicable only to Rails 4.0+. 12 | 13 | You can generate a PDF or an HTML copy of this guide using 14 | [Pandoc](http://pandoc.org/). 15 | 16 | Translations of the guide are available in the following languages: 17 | 18 | * [Chinese Simplified](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhCN.md) 19 | * [Chinese Traditional](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhTW.md) 20 | * [German](https://github.com/arbox/de-rails-style-guide/blob/master/README-deDE.md) 21 | * [Japanese](https://github.com/satour/rails-style-guide/blob/master/README-jaJA.md) 22 | * [Russian](https://github.com/arbox/rails-style-guide/blob/master/README-ruRU.md) 23 | * [Turkish](https://github.com/tolgaavci/rails-style-guide/blob/master/README-trTR.md) 24 | * [Korean](https://github.com/pureugong/rails-style-guide/blob/master/README-koKR.md) 25 | * [Vietnamese](https://github.com/CQBinh/rails-style-guide/blob/master/README-viVN.md) 26 | 27 | # The Rails Style Guide 28 | 29 | This Rails style guide recommends best practices so that real-world Rails 30 | programmers can write code that can be maintained by other real-world Rails 31 | programmers. A style guide that reflects real-world usage gets used, and a 32 | style guide that holds to an ideal that has been rejected by the people it 33 | is supposed to help risks not getting used at all – no matter how good 34 | it is. 35 | 36 | The guide is separated into several sections of related rules. I've tried to add 37 | the rationale behind the rules (if it's omitted I've assumed it's pretty 38 | obvious). 39 | 40 | I didn't come up with all the rules out of nowhere - they are mostly based on my 41 | extensive career as a professional software engineer, feedback and suggestions 42 | from members of the Rails community and various highly regarded Rails 43 | programming resources. 44 | 45 | ## Table of Contents 46 | 47 | * [Configuration](#configuration) 48 | * [Routing](#routing) 49 | * [Controllers](#controllers) 50 | * [Rendering](#rendering) 51 | * [Models](#models) 52 | * [ActiveRecord](#activerecord) 53 | * [ActiveRecord Queries](#activerecord-queries) 54 | * [Migrations](#migrations) 55 | * [Views](#views) 56 | * [Internationalization](#internationalization) 57 | * [Assets](#assets) 58 | * [Mailers](#mailers) 59 | * [Active Support Core Extensions](#active-support-core-extensions) 60 | * [Time](#time) 61 | * [Bundler](#bundler) 62 | * [Managing processes](#managing-processes) 63 | 64 | ## Configuration 65 | 66 | * 67 | Put custom initialization code in `config/initializers`. The code in 68 | initializers executes on application startup. 69 | [[link](#config-initializers)] 70 | 71 | * 72 | Keep initialization code for each gem in a separate file with the same name 73 | as the gem, for example `carrierwave.rb`, `active_admin.rb`, etc. 74 | [[link](#gem-initializers)] 75 | 76 | * 77 | Adjust accordingly the settings for development, test and production 78 | environment (in the corresponding files under `config/environments/`) 79 | [[link](#dev-test-prod-configs)] 80 | 81 | * Mark additional assets for precompilation (if any): 82 | 83 | ```Ruby 84 | # config/environments/production.rb 85 | # Precompile additional assets (application.js, application.css, 86 | #and all non-JS/CSS are already added) 87 | config.assets.precompile += %w( rails_admin/rails_admin.css rails_admin/rails_admin.js ) 88 | ``` 89 | 90 | * 91 | Keep configuration that's applicable to all environments in the 92 | `config/application.rb` file. 93 | [[link](#app-config)] 94 | 95 | * 96 | Create an additional `staging` environment that closely resembles the 97 | `production` one. 98 | [[link](#staging-like-prod)] 99 | 100 | * 101 | Keep any additional configuration in YAML files under the `config/` directory. 102 | [[link](#yaml-config)] 103 | 104 | Since Rails 4.2 YAML configuration files can be easily loaded with the new `config_for` method: 105 | 106 | ```Ruby 107 | Rails::Application.config_for(:yaml_file) 108 | ``` 109 | 110 | ## Routing 111 | 112 | * 113 | When you need to add more actions to a RESTful resource (do you really need 114 | them at all?) use `member` and `collection` routes. 115 | [[link](#member-collection-routes)] 116 | 117 | ```Ruby 118 | # bad 119 | get 'subscriptions/:id/unsubscribe' 120 | resources :subscriptions 121 | 122 | # good 123 | resources :subscriptions do 124 | get 'unsubscribe', on: :member 125 | end 126 | 127 | # bad 128 | get 'photos/search' 129 | resources :photos 130 | 131 | # good 132 | resources :photos do 133 | get 'search', on: :collection 134 | end 135 | ``` 136 | 137 | * 138 | If you need to define multiple `member/collection` routes use the 139 | alternative block syntax. 140 | [[link](#many-member-collection-routes)] 141 | 142 | ```Ruby 143 | resources :subscriptions do 144 | member do 145 | get 'unsubscribe' 146 | # more routes 147 | end 148 | end 149 | 150 | resources :photos do 151 | collection do 152 | get 'search' 153 | # more routes 154 | end 155 | end 156 | ``` 157 | 158 | * 159 | Use nested routes to express better the relationship between ActiveRecord 160 | models. 161 | [[link](#nested-routes)] 162 | 163 | ```Ruby 164 | class Post < ActiveRecord::Base 165 | has_many :comments 166 | end 167 | 168 | class Comments < ActiveRecord::Base 169 | belongs_to :post 170 | end 171 | 172 | # routes.rb 173 | resources :posts do 174 | resources :comments 175 | end 176 | ``` 177 | 178 | * 179 | If you need to nest routes more than 1 level deep then use the `shallow: true` option. This will save user from long urls `posts/1/comments/5/versions/7/edit` and you from long url helpers `edit_post_comment_version`. 180 | 181 | ```Ruby 182 | resources :posts, shallow: true do 183 | resources :comments do 184 | resources :versions 185 | end 186 | end 187 | ``` 188 | 189 | * 190 | Use namespaced routes to group related actions. 191 | [[link](#namespaced-routes)] 192 | 193 | ```Ruby 194 | namespace :admin do 195 | # Directs /admin/products/* to Admin::ProductsController 196 | # (app/controllers/admin/products_controller.rb) 197 | resources :products 198 | end 199 | ``` 200 | 201 | * 202 | Never use the legacy wild controller route. This route will make all actions 203 | in every controller accessible via GET requests. 204 | [[link](#no-wild-routes)] 205 | 206 | ```Ruby 207 | # very bad 208 | match ':controller(/:action(/:id(.:format)))' 209 | ``` 210 | 211 | * 212 | Don't use `match` to define any routes unless there is need to map multiple request types among `[:get, :post, :patch, :put, :delete]` to a single action using `:via` option. 213 | [[link](#no-match-routes)] 214 | 215 | ## Controllers 216 | 217 | * 218 | Keep the controllers skinny - they should only retrieve data for the view 219 | layer and shouldn't contain any business logic (all the business logic 220 | should naturally reside in the model). 221 | [[link](#skinny-controllers)] 222 | 223 | * 224 | Each controller action should (ideally) invoke only one method other than an 225 | initial find or new. 226 | [[link](#one-method)] 227 | 228 | * 229 | Share no more than two instance variables between a controller and a view. 230 | [[link](#shared-instance-variables)] 231 | 232 | 233 | ### Rendering 234 | 235 | * 236 | Prefer using a template over inline rendering. 237 | [[link](#inline-rendering)] 238 | 239 | ```Ruby 240 | # very bad 241 | class ProductsController < ApplicationController 242 | def index 243 | render inline: "<% products.each do |p| %>

<%= p.name %>

<% end %>", type: :erb 244 | end 245 | end 246 | 247 | # good 248 | ## app/views/products/index.html.erb 249 | <%= render partial: 'product', collection: products %> 250 | 251 | ## app/views/products/_product.html.erb 252 |

<%= product.name %>

253 |

<%= product.price %>

254 | 255 | ## app/controllers/foo_controller.rb 256 | class ProductsController < ApplicationController 257 | def index 258 | render :index 259 | end 260 | end 261 | ``` 262 | 263 | * 264 | Prefer `render plain:` over `render text:`. 265 | [[link](#plain-text-rendering)] 266 | 267 | ```Ruby 268 | # bad - sets MIME type to `text/html` 269 | ... 270 | render text: 'Ruby!' 271 | ... 272 | 273 | # bad - requires explicit MIME type declaration 274 | ... 275 | render text: 'Ruby!', content_type: 'text/plain' 276 | ... 277 | 278 | # good - short and precise 279 | ... 280 | render plain: 'Ruby!' 281 | ... 282 | ``` 283 | 284 | * 285 | Prefer [corresponding symbols](https://gist.github.com/mlanett/a31c340b132ddefa9cca) to numeric HTTP status codes. They are meaningful and do not look like "magic" numbers for less known HTTP status codes. 286 | [[link](#http-status-code-symbols)] 287 | 288 | ```Ruby 289 | # bad 290 | ... 291 | render status: 500 292 | ... 293 | 294 | # good 295 | ... 296 | render status: :forbidden 297 | ... 298 | ``` 299 | 300 | ## Models 301 | 302 | * 303 | Introduce non-ActiveRecord model classes freely. 304 | [[link](#model-classes)] 305 | 306 | * 307 | Name the models with meaningful (but short) names without abbreviations. 308 | [[link](#meaningful-model-names)] 309 | 310 | * 311 | If you need model objects that support ActiveRecord behavior (like validation) 312 | without the ActiveRecord database functionality use the 313 | [ActiveAttr](https://github.com/cgriego/active_attr) gem. 314 | [[link](#activeattr-gem)] 315 | 316 | ```Ruby 317 | class Message 318 | include ActiveAttr::Model 319 | 320 | attribute :name 321 | attribute :email 322 | attribute :content 323 | attribute :priority 324 | 325 | attr_accessible :name, :email, :content 326 | 327 | validates :name, presence: true 328 | validates :email, format: { with: /\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}\z/i } 329 | validates :content, length: { maximum: 500 } 330 | end 331 | ``` 332 | 333 | For a more complete example refer to the 334 | [RailsCast on the subject](http://railscasts.com/episodes/326-activeattr). 335 | 336 | * 337 | Unless they have some meaning in the business domain, don't put methods in 338 | your model that just format your data (like code generating HTML). These 339 | methods are most likely going to be called from the view layer only, so their 340 | place is in helpers. Keep your models for business logic and data-persistence 341 | only. 342 | [[link](#model-business-logic)] 343 | 344 | ### ActiveRecord 345 | 346 | * 347 | Avoid altering ActiveRecord defaults (table names, primary key, etc) unless 348 | you have a very good reason (like a database that's not under your control). 349 | [[link](#keep-ar-defaults)] 350 | 351 | ```Ruby 352 | # bad - don't do this if you can modify the schema 353 | class Transaction < ActiveRecord::Base 354 | self.table_name = 'order' 355 | ... 356 | end 357 | ``` 358 | 359 | * 360 | Group macro-style methods (`has_many`, `validates`, etc) in the beginning of 361 | the class definition. 362 | [[link](#macro-style-methods)] 363 | 364 | ```Ruby 365 | class User < ActiveRecord::Base 366 | # keep the default scope first (if any) 367 | default_scope { where(active: true) } 368 | 369 | # constants come up next 370 | COLORS = %w(red green blue) 371 | 372 | # afterwards we put attr related macros 373 | attr_accessor :formatted_date_of_birth 374 | 375 | attr_accessible :login, :first_name, :last_name, :email, :password 376 | 377 | # Rails4+ enums after attr macros, prefer the hash syntax 378 | enum gender: { female: 0, male: 1 } 379 | 380 | # followed by association macros 381 | belongs_to :country 382 | 383 | has_many :authentications, dependent: :destroy 384 | 385 | # and validation macros 386 | validates :email, presence: true 387 | validates :username, presence: true 388 | validates :username, uniqueness: { case_sensitive: false } 389 | validates :username, format: { with: /\A[A-Za-z][A-Za-z0-9._-]{2,19}\z/ } 390 | validates :password, format: { with: /\A\S{8,128}\z/, allow_nil: true } 391 | 392 | # next we have callbacks 393 | before_save :cook 394 | before_save :update_username_lower 395 | 396 | # other macros (like devise's) should be placed after the callbacks 397 | 398 | ... 399 | end 400 | ``` 401 | 402 | * 403 | Prefer `has_many :through` to `has_and_belongs_to_many`. Using `has_many 404 | :through` allows additional attributes and validations on the join model. 405 | [[link](#has-many-through)] 406 | 407 | ```Ruby 408 | # not so good - using has_and_belongs_to_many 409 | class User < ActiveRecord::Base 410 | has_and_belongs_to_many :groups 411 | end 412 | 413 | class Group < ActiveRecord::Base 414 | has_and_belongs_to_many :users 415 | end 416 | 417 | # preferred way - using has_many :through 418 | class User < ActiveRecord::Base 419 | has_many :memberships 420 | has_many :groups, through: :memberships 421 | end 422 | 423 | class Membership < ActiveRecord::Base 424 | belongs_to :user 425 | belongs_to :group 426 | end 427 | 428 | class Group < ActiveRecord::Base 429 | has_many :memberships 430 | has_many :users, through: :memberships 431 | end 432 | ``` 433 | 434 | * 435 | Prefer `self[:attribute]` over `read_attribute(:attribute)`. 436 | [[link](#read-attribute)] 437 | 438 | ```Ruby 439 | # bad 440 | def amount 441 | read_attribute(:amount) * 100 442 | end 443 | 444 | # good 445 | def amount 446 | self[:amount] * 100 447 | end 448 | ``` 449 | 450 | * 451 | Prefer `self[:attribute] = value` over `write_attribute(:attribute, value)`. 452 | [[link](#write-attribute)] 453 | 454 | ```Ruby 455 | # bad 456 | def amount 457 | write_attribute(:amount, 100) 458 | end 459 | 460 | # good 461 | def amount 462 | self[:amount] = 100 463 | end 464 | ``` 465 | 466 | * 467 | Always use the new ["sexy" 468 | validations](http://thelucid.com/2010/01/08/sexy-validation-in-edge-rails-rails-3/). 469 | [[link](#sexy-validations)] 470 | 471 | ```Ruby 472 | # bad 473 | validates_presence_of :email 474 | validates_length_of :email, maximum: 100 475 | 476 | # good 477 | validates :email, presence: true, length: { maximum: 100 } 478 | ``` 479 | 480 | * 481 | When a custom validation is used more than once or the validation is some 482 | regular expression mapping, create a custom validator file. 483 | [[link](#custom-validator-file)] 484 | 485 | ```Ruby 486 | # bad 487 | class Person 488 | validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i } 489 | end 490 | 491 | # good 492 | class EmailValidator < ActiveModel::EachValidator 493 | def validate_each(record, attribute, value) 494 | record.errors[attribute] << (options[:message] || 'is not a valid email') unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i 495 | end 496 | end 497 | 498 | class Person 499 | validates :email, email: true 500 | end 501 | ``` 502 | 503 | * 504 | Keep custom validators under `app/validators`. 505 | [[link](#app-validators)] 506 | 507 | * 508 | Consider extracting custom validators to a shared gem if you're maintaining 509 | several related apps or the validators are generic enough. 510 | [[link](#custom-validators-gem)] 511 | 512 | * 513 | Use named scopes freely. 514 | [[link](#named-scopes)] 515 | 516 | ```Ruby 517 | class User < ActiveRecord::Base 518 | scope :active, -> { where(active: true) } 519 | scope :inactive, -> { where(active: false) } 520 | 521 | scope :with_orders, -> { joins(:orders).select('distinct(users.id)') } 522 | end 523 | ``` 524 | 525 | * 526 | When a named scope defined with a lambda and parameters becomes too 527 | complicated, it is preferable to make a class method instead which serves the 528 | same purpose of the named scope and returns an `ActiveRecord::Relation` 529 | object. Arguably you can define even simpler scopes like this. 530 | [[link](#named-scope-class)] 531 | 532 | ```Ruby 533 | class User < ActiveRecord::Base 534 | def self.with_orders 535 | joins(:orders).select('distinct(users.id)') 536 | end 537 | end 538 | ``` 539 | 540 | * 541 | Beware of the behavior of the 542 | [following](http://guides.rubyonrails.org/active_record_validations.html#skipping-validations) 543 | methods. They do not run the model validations and 544 | could easily corrupt the model state. 545 | [[link](#beware-skip-model-validations)] 546 | 547 | ```Ruby 548 | # bad 549 | Article.first.decrement!(:view_count) 550 | DiscussionBoard.decrement_counter(:post_count, 5) 551 | Article.first.increment!(:view_count) 552 | DiscussionBoard.increment_counter(:post_count, 5) 553 | person.toggle :active 554 | product.touch 555 | Billing.update_all("category = 'authorized', author = 'David'") 556 | user.update_attribute(:website, 'example.com') 557 | user.update_columns(last_request_at: Time.current) 558 | Post.update_counters 5, comment_count: -1, action_count: 1 559 | 560 | # good 561 | user.update_attributes(website: 'example.com') 562 | ``` 563 | 564 | * 565 | Use user-friendly URLs. Show some descriptive attribute of the model in the URL 566 | rather than its `id`. There is more than one way to achieve this: 567 | [[link](#user-friendly-urls)] 568 | 569 | * Override the `to_param` method of the model. This method is used by Rails 570 | for constructing a URL to the object. The default implementation returns 571 | the `id` of the record as a String. It could be overridden to include another 572 | human-readable attribute. 573 | 574 | ```Ruby 575 | class Person 576 | def to_param 577 | "#{id} #{name}".parameterize 578 | end 579 | end 580 | ``` 581 | 582 | In order to convert this to a URL-friendly value, `parameterize` should be 583 | called on the string. The `id` of the object needs to be at the beginning so 584 | that it can be found by the `find` method of ActiveRecord. 585 | 586 | * Use the `friendly_id` gem. It allows creation of human-readable URLs by 587 | using some descriptive attribute of the model instead of its `id`. 588 | 589 | ```Ruby 590 | class Person 591 | extend FriendlyId 592 | friendly_id :name, use: :slugged 593 | end 594 | ``` 595 | 596 | Check the [gem documentation](https://github.com/norman/friendly_id) for more 597 | information about its usage. 598 | 599 | * 600 | Use `find_each` to iterate over a collection of AR objects. Looping through a 601 | collection of records from the database (using the `all` method, for example) 602 | is very inefficient since it will try to instantiate all the objects at once. 603 | In that case, batch processing methods allow you to work with the records in 604 | batches, thereby greatly reducing memory consumption. 605 | [[link](#find-each)] 606 | 607 | 608 | ```Ruby 609 | # bad 610 | Person.all.each do |person| 611 | person.do_awesome_stuff 612 | end 613 | 614 | Person.where('age > 21').each do |person| 615 | person.party_all_night! 616 | end 617 | 618 | # good 619 | Person.find_each do |person| 620 | person.do_awesome_stuff 621 | end 622 | 623 | Person.where('age > 21').find_each do |person| 624 | person.party_all_night! 625 | end 626 | ``` 627 | 628 | * 629 | Since [Rails creates callbacks for dependent 630 | associations](https://github.com/rails/rails/issues/3458), always call 631 | `before_destroy` callbacks that perform validation with `prepend: true`. 632 | [[link](#before_destroy)] 633 | 634 | ```Ruby 635 | # bad (roles will be deleted automatically even if super_admin? is true) 636 | has_many :roles, dependent: :destroy 637 | 638 | before_destroy :ensure_deletable 639 | 640 | def ensure_deletable 641 | fail "Cannot delete super admin." if super_admin? 642 | end 643 | 644 | # good 645 | has_many :roles, dependent: :destroy 646 | 647 | before_destroy :ensure_deletable, prepend: true 648 | 649 | def ensure_deletable 650 | fail "Cannot delete super admin." if super_admin? 651 | end 652 | ``` 653 | 654 | * 655 | Define the `dependent` option to the `has_many` and `has_one` associations. 656 | [[link](#has_many-has_one-dependent-option)] 657 | 658 | ```Ruby 659 | # bad 660 | class Post < ActiveRecord::Base 661 | has_many :comments 662 | end 663 | 664 | # good 665 | class Post < ActiveRecord::Base 666 | has_many :comments, dependent: :destroy 667 | end 668 | ``` 669 | 670 | * 671 | When persisting AR objects always use the exception raising bang! method or handle the method return value. 672 | This applies to `create`, `save`, `update`, `destroy`, `first_or_create` and `find_or_create_by`. 673 | [[link](#save-bang)] 674 | 675 | ```Ruby 676 | # bad 677 | user.create(name: 'Bruce') 678 | 679 | # bad 680 | user.save 681 | 682 | # good 683 | user.create!(name: 'Bruce') 684 | # or 685 | bruce = user.create(name: 'Bruce') 686 | if bruce.persisted? 687 | ... 688 | else 689 | ... 690 | end 691 | 692 | # good 693 | user.save! 694 | # or 695 | if user.save 696 | ... 697 | else 698 | ... 699 | end 700 | ``` 701 | 702 | ### ActiveRecord Queries 703 | 704 | * 705 | Avoid string interpolation in 706 | queries, as it will make your code susceptible to SQL injection 707 | attacks. 708 | [[link](#avoid-interpolation)] 709 | 710 | ```Ruby 711 | # bad - param will be interpolated unescaped 712 | Client.where("orders_count = #{params[:orders]}") 713 | 714 | # good - param will be properly escaped 715 | Client.where('orders_count = ?', params[:orders]) 716 | ``` 717 | 718 | * 719 | Consider using named placeholders instead of positional placeholders 720 | when you have more than 1 placeholder in your query. 721 | [[link](#named-placeholder)] 722 | 723 | ```Ruby 724 | # okish 725 | Client.where( 726 | 'created_at >= ? AND created_at <= ?', 727 | params[:start_date], params[:end_date] 728 | ) 729 | 730 | # good 731 | Client.where( 732 | 'created_at >= :start_date AND created_at <= :end_date', 733 | start_date: params[:start_date], end_date: params[:end_date] 734 | ) 735 | ``` 736 | 737 | * 738 | Favor the use of `find` over `where` 739 | when you need to retrieve a single record by id. 740 | [[link](#find)] 741 | 742 | ```Ruby 743 | # bad 744 | User.where(id: id).take 745 | 746 | # good 747 | User.find(id) 748 | ``` 749 | 750 | * 751 | Favor the use of `find_by` over `where` and `find_by_attribute` 752 | when you need to retrieve a single record by some attributes. 753 | [[link](#find_by)] 754 | 755 | ```Ruby 756 | # bad 757 | User.where(first_name: 'Bruce', last_name: 'Wayne').first 758 | 759 | # bad 760 | User.find_by_first_name_and_last_name('Bruce', 'Wayne') 761 | 762 | # good 763 | User.find_by(first_name: 'Bruce', last_name: 'Wayne') 764 | ``` 765 | 766 | * 767 | Favor the use of `where.not` over SQL. 768 | [[link](#where-not)] 769 | 770 | ```Ruby 771 | # bad 772 | User.where("id != ?", id) 773 | 774 | # good 775 | User.where.not(id: id) 776 | ``` 777 | * 778 | When specifying an explicit query in a method such as `find_by_sql`, use 779 | heredocs with `squish`. This allows you to legibly format the SQL with 780 | line breaks and indentations, while supporting syntax highlighting in many 781 | tools (including GitHub, Atom, and RubyMine). 782 | [[link](#squished-heredocs)] 783 | 784 | ```Ruby 785 | User.find_by_sql(<<-SQL.squish) 786 | SELECT 787 | users.id, accounts.plan 788 | FROM 789 | users 790 | INNER JOIN 791 | accounts 792 | ON 793 | accounts.user_id = users.id 794 | # further complexities... 795 | SQL 796 | ``` 797 | 798 | [`String#squish`](http://apidock.com/rails/String/squish) removes the indentation and newline characters so that your server 799 | log shows a fluid string of SQL rather than something like this: 800 | 801 | ``` 802 | SELECT\n users.id, accounts.plan\n FROM\n users\n INNER JOIN\n acounts\n ON\n accounts.user_id = users.id 803 | ``` 804 | 805 | ## Migrations 806 | 807 | * 808 | Keep the `schema.rb` (or `structure.sql`) under version control. 809 | [[link](#schema-version)] 810 | 811 | * 812 | Use `rake db:schema:load` instead of `rake db:migrate` to initialize an empty 813 | database. 814 | [[link](#db-schema-load)] 815 | 816 | * 817 | Enforce default values in the migrations themselves instead of in the 818 | application layer. 819 | [[link](#default-migration-values)] 820 | 821 | ```Ruby 822 | # bad - application enforced default value 823 | class Product < ActiveRecord::Base 824 | def amount 825 | self[:amount] || 0 826 | end 827 | end 828 | 829 | # good - database enforced 830 | class AddDefaultAmountToProducts < ActiveRecord::Migration 831 | def change 832 | change_column_default :products, :amount, 0 833 | end 834 | end 835 | ``` 836 | 837 | While enforcing table defaults only in Rails is suggested by many 838 | Rails developers, it's an extremely brittle approach that 839 | leaves your data vulnerable to many application bugs. And you'll 840 | have to consider the fact that most non-trivial apps share a 841 | database with other applications, so imposing data integrity from 842 | the Rails app is impossible. 843 | 844 | * 845 | Enforce foreign-key constraints. As of Rails 4.2, ActiveRecord 846 | supports foreign key constraints natively. 847 | [[link](#foreign-key-constraints)] 848 | 849 | * 850 | When writing constructive migrations (adding tables or columns), 851 | use the `change` method instead of `up` and `down` methods. 852 | [[link](#change-vs-up-down)] 853 | 854 | ```Ruby 855 | # the old way 856 | class AddNameToPeople < ActiveRecord::Migration 857 | def up 858 | add_column :people, :name, :string 859 | end 860 | 861 | def down 862 | remove_column :people, :name 863 | end 864 | end 865 | 866 | # the new preferred way 867 | class AddNameToPeople < ActiveRecord::Migration 868 | def change 869 | add_column :people, :name, :string 870 | end 871 | end 872 | ``` 873 | 874 | * 875 | If you have to use models in migrations, make sure you define them 876 | so that you don't end up with broken migrations in the future 877 | [[link](#define-model-class-migrations)] 878 | 879 | ```Ruby 880 | # db/migrate/.rb 881 | # frozen_string_literal: true 882 | 883 | # bad 884 | class ModifyDefaultStatusForProducts < ActiveRecord::Migration 885 | def change 886 | old_status = 'pending_manual_approval' 887 | new_status = 'pending_approval' 888 | 889 | reversible do |dir| 890 | dir.up do 891 | Product.where(status: old_status).update_all(status: new_status) 892 | change_column :products, :status, :string, default: new_status 893 | end 894 | 895 | dir.down do 896 | Product.where(status: new_status).update_all(status: old_status) 897 | change_column :products, :status, :string, default: old_status 898 | end 899 | end 900 | end 901 | end 902 | 903 | # good 904 | # Define `table_name` in a custom named class to make sure that 905 | # you run on the same table you had during the creation of the migration. 906 | # In future if you override the `Product` class 907 | # and change the `table_name`, it won't break 908 | # the migration or cause serious data corruption. 909 | class MigrationProduct < ActiveRecord::Base 910 | self.table_name = :products 911 | end 912 | 913 | class ModifyDefaultStatusForProducts < ActiveRecord::Migration 914 | def change 915 | old_status = 'pending_manual_approval' 916 | new_status = 'pending_approval' 917 | 918 | reversible do |dir| 919 | dir.up do 920 | MigrationProduct.where(status: old_status).update_all(status: new_status) 921 | change_column :products, :status, :string, default: new_status 922 | end 923 | 924 | dir.down do 925 | MigrationProduct.where(status: new_status).update_all(status: old_status) 926 | change_column :products, :status, :string, default: old_status 927 | end 928 | end 929 | end 930 | end 931 | ``` 932 | 933 | * 934 | Name your foreign keys explicitly instead of relying on Rails auto-generated 935 | FK names. (http://guides.rubyonrails.org/active_record_migrations.html#foreign-keys) 936 | [[link](#meaningful-foreign-key-naming)] 937 | 938 | ```Ruby 939 | # bad 940 | class AddFkArticlesToAuthors < ActiveRecord::Migration 941 | def change 942 | add_foreign_key :articles, :authors 943 | end 944 | end 945 | 946 | # good 947 | class AddFkArticlesToAuthors < ActiveRecord::Migration 948 | def change 949 | add_foreign_key :articles, :authors, name: :articles_author_id_fk 950 | end 951 | end 952 | ``` 953 | 954 | * 955 | Don't use non-reversible migration commands in the `change` method. 956 | Reversible migration commands are listed below. 957 | [ActiveRecord::Migration::CommandRecorder](http://api.rubyonrails.org/classes/ActiveRecord/Migration/CommandRecorder.html) 958 | [[link](#reversible-migration)] 959 | 960 | ```ruby 961 | # bad 962 | class DropUsers < ActiveRecord::Migration 963 | def change 964 | drop_table :users 965 | end 966 | end 967 | 968 | # good 969 | class DropUsers < ActiveRecord::Migration 970 | def up 971 | drop_table :users 972 | end 973 | 974 | def down 975 | create_table :users do |t| 976 | t.string :name 977 | end 978 | end 979 | end 980 | 981 | # good 982 | # In this case, block will be used by create_table in rollback 983 | # http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters.html#method-i-drop_table 984 | class DropUsers < ActiveRecord::Migration 985 | def change 986 | drop_table :users do |t| 987 | t.string :name 988 | end 989 | end 990 | end 991 | ``` 992 | 993 | ## Views 994 | 995 | * 996 | Never call the model layer directly from a view. 997 | [[link](#no-direct-model-view)] 998 | 999 | * 1000 | Never make complex formatting in the views, export the formatting to a method 1001 | in the view helper or the model. 1002 | [[link](#no-complex-view-formatting)] 1003 | 1004 | * 1005 | Mitigate code duplication by using partial templates and layouts. 1006 | [[link](#partials)] 1007 | 1008 | ## Internationalization 1009 | 1010 | * 1011 | No strings or other locale specific settings should be used in the views, 1012 | models and controllers. These texts should be moved to the locale files in the 1013 | `config/locales` directory. 1014 | [[link](#locale-texts)] 1015 | 1016 | * 1017 | When the labels of an ActiveRecord model need to be translated, use the 1018 | `activerecord` scope: 1019 | [[link](#translated-labels)] 1020 | 1021 | ``` 1022 | en: 1023 | activerecord: 1024 | models: 1025 | user: Member 1026 | attributes: 1027 | user: 1028 | name: 'Full name' 1029 | ``` 1030 | 1031 | Then `User.model_name.human` will return "Member" and 1032 | `User.human_attribute_name("name")` will return "Full name". These 1033 | translations of the attributes will be used as labels in the views. 1034 | 1035 | 1036 | * 1037 | Separate the texts used in the views from translations of ActiveRecord 1038 | attributes. Place the locale files for the models in a folder `locales/models` and the 1039 | texts used in the views in folder `locales/views`. 1040 | [[link](#organize-locale-files)] 1041 | 1042 | * When organization of the locale files is done with additional directories, 1043 | these directories must be described in the `application.rb` file in order 1044 | to be loaded. 1045 | 1046 | ```Ruby 1047 | # config/application.rb 1048 | config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')] 1049 | ``` 1050 | 1051 | * 1052 | Place the shared localization options, such as date or currency formats, in 1053 | files under the root of the `locales` directory. 1054 | [[link](#shared-localization)] 1055 | 1056 | * 1057 | Use the short form of the I18n methods: `I18n.t` instead of `I18n.translate` 1058 | and `I18n.l` instead of `I18n.localize`. 1059 | [[link](#short-i18n)] 1060 | 1061 | * 1062 | Use "lazy" lookup for the texts used in views. Let's say we have the following 1063 | structure: 1064 | [[link](#lazy-lookup)] 1065 | 1066 | ``` 1067 | en: 1068 | users: 1069 | show: 1070 | title: 'User details page' 1071 | ``` 1072 | 1073 | The value for `users.show.title` can be looked up in the template 1074 | `app/views/users/show.html.haml` like this: 1075 | 1076 | ```Ruby 1077 | = t '.title' 1078 | ``` 1079 | 1080 | * 1081 | Use the dot-separated keys in the controllers and models instead of specifying 1082 | the `:scope` option. The dot-separated call is easier to read and trace the 1083 | hierarchy. 1084 | [[link](#dot-separated-keys)] 1085 | 1086 | ```Ruby 1087 | # bad 1088 | I18n.t :record_invalid, scope: [:activerecord, :errors, :messages] 1089 | 1090 | # good 1091 | I18n.t 'activerecord.errors.messages.record_invalid' 1092 | ``` 1093 | 1094 | * 1095 | More detailed information about the Rails I18n can be found in the [Rails 1096 | Guides](http://guides.rubyonrails.org/i18n.html) 1097 | [[link](#i18n-guides)] 1098 | 1099 | ## Assets 1100 | 1101 | Use the [assets pipeline](http://guides.rubyonrails.org/asset_pipeline.html) to leverage organization within 1102 | your application. 1103 | 1104 | * 1105 | Reserve `app/assets` for custom stylesheets, javascripts, or images. 1106 | [[link](#reserve-app-assets)] 1107 | 1108 | * 1109 | Use `lib/assets` for your own libraries that don’t really fit into the 1110 | scope of the application. 1111 | [[link](#lib-assets)] 1112 | 1113 | * 1114 | Third party code such as [jQuery](http://jquery.com/) or 1115 | [bootstrap](http://twitter.github.com/bootstrap/) should be placed in 1116 | `vendor/assets`. 1117 | [[link](#vendor-assets)] 1118 | 1119 | * 1120 | When possible, use gemified versions of assets (e.g. 1121 | [jquery-rails](https://github.com/rails/jquery-rails), 1122 | [jquery-ui-rails](https://github.com/joliss/jquery-ui-rails), 1123 | [bootstrap-sass](https://github.com/twbs/bootstrap-sass), 1124 | [zurb-foundation](https://github.com/zurb/foundation)). 1125 | [[link](#gem-assets)] 1126 | 1127 | ## Mailers 1128 | 1129 | * 1130 | Name the mailers `SomethingMailer`. Without the Mailer suffix it isn't 1131 | immediately apparent what's a mailer and which views are related to the 1132 | mailer. 1133 | [[link](#mailer-name)] 1134 | 1135 | * 1136 | Provide both HTML and plain-text view templates. 1137 | [[link](#html-plain-email)] 1138 | 1139 | * 1140 | Enable errors raised on failed mail delivery in your development environment. 1141 | The errors are disabled by default. 1142 | [[link](#enable-delivery-errors)] 1143 | 1144 | ```Ruby 1145 | # config/environments/development.rb 1146 | 1147 | config.action_mailer.raise_delivery_errors = true 1148 | ``` 1149 | 1150 | * 1151 | Use a local SMTP server like 1152 | [Mailcatcher](https://github.com/sj26/mailcatcher) in the development 1153 | environment. 1154 | [[link](#local-smtp)] 1155 | 1156 | ```Ruby 1157 | # config/environments/development.rb 1158 | 1159 | config.action_mailer.smtp_settings = { 1160 | address: 'localhost', 1161 | port: 1025, 1162 | # more settings 1163 | } 1164 | ``` 1165 | 1166 | * 1167 | Provide default settings for the host name. 1168 | [[link](#default-hostname)] 1169 | 1170 | ```Ruby 1171 | # config/environments/development.rb 1172 | config.action_mailer.default_url_options = { host: "#{local_ip}:3000" } 1173 | 1174 | # config/environments/production.rb 1175 | config.action_mailer.default_url_options = { host: 'your_site.com' } 1176 | 1177 | # in your mailer class 1178 | default_url_options[:host] = 'your_site.com' 1179 | ``` 1180 | 1181 | * 1182 | If you need to use a link to your site in an email, always use the `_url`, not 1183 | `_path` methods. The `_url` methods include the host name and the `_path` 1184 | methods don't. 1185 | [[link](#url-not-path-in-email)] 1186 | 1187 | ```Ruby 1188 | # bad 1189 | You can always find more info about this course 1190 | <%= link_to 'here', course_path(@course) %> 1191 | 1192 | # good 1193 | You can always find more info about this course 1194 | <%= link_to 'here', course_url(@course) %> 1195 | ``` 1196 | 1197 | * 1198 | Format the from and to addresses properly. Use the following format: 1199 | [[link](#email-addresses)] 1200 | 1201 | ```Ruby 1202 | # in your mailer class 1203 | default from: 'Your Name ' 1204 | ``` 1205 | 1206 | * 1207 | Make sure that the e-mail delivery method for your test environment is set to 1208 | `test`: 1209 | [[link](#delivery-method-test)] 1210 | 1211 | ```Ruby 1212 | # config/environments/test.rb 1213 | 1214 | config.action_mailer.delivery_method = :test 1215 | ``` 1216 | 1217 | * 1218 | The delivery method for development and production should be `smtp`: 1219 | [[link](#delivery-method-smtp)] 1220 | 1221 | ```Ruby 1222 | # config/environments/development.rb, config/environments/production.rb 1223 | 1224 | config.action_mailer.delivery_method = :smtp 1225 | ``` 1226 | 1227 | * 1228 | When sending html emails all styles should be inline, as some mail clients 1229 | have problems with external styles. This however makes them harder to maintain 1230 | and leads to code duplication. There are two similar gems that transform the 1231 | styles and put them in the corresponding html tags: 1232 | [premailer-rails](https://github.com/fphilipe/premailer-rails) and 1233 | [roadie](https://github.com/Mange/roadie). 1234 | [[link](#inline-email-styles)] 1235 | 1236 | * 1237 | Sending emails while generating page response should be avoided. It causes 1238 | delays in loading of the page and request can timeout if multiple email are 1239 | sent. To overcome this emails can be sent in background process with the help 1240 | of [sidekiq](https://github.com/mperham/sidekiq) gem. 1241 | [[link](#background-email)] 1242 | 1243 | 1244 | ## Active Support Core Extensions 1245 | 1246 | * 1247 | Prefer Ruby 2.3's safe navigation operator `&.` over `ActiveSupport#try!`. 1248 | [[link](#try-bang)] 1249 | 1250 | ```ruby 1251 | # bad 1252 | obj.try! :fly 1253 | 1254 | # good 1255 | obj&.fly 1256 | ``` 1257 | 1258 | * 1259 | Prefer Ruby's Standard Library methods over `ActiveSupport` aliases. 1260 | [[link](#active_support_aliases)] 1261 | 1262 | ```ruby 1263 | # bad 1264 | 'the day'.starts_with? 'th' 1265 | 'the day'.ends_with? 'ay' 1266 | 1267 | # good 1268 | 'the day'.start_with? 'th' 1269 | 'the day'.end_with? 'ay' 1270 | ``` 1271 | 1272 | * 1273 | Prefer Ruby's Standard Library over uncommon ActiveSupport extensions. 1274 | [[link](#active_support_extensions)] 1275 | 1276 | ```ruby 1277 | # bad 1278 | (1..50).to_a.forty_two 1279 | 1.in? [1, 2] 1280 | 'day'.in? 'the day' 1281 | 1282 | # good 1283 | (1..50).to_a[41] 1284 | [1, 2].include? 1 1285 | 'the day'.include? 'day' 1286 | ``` 1287 | 1288 | * 1289 | Prefer Ruby's comparison operators over ActiveSupport's `Array#inquiry`, `Numeric#inquiry` and `String#inquiry`. 1290 | [[link](#inquiry)] 1291 | 1292 | ```ruby 1293 | # bad - String#inquiry 1294 | ruby = 'two'.inquiry 1295 | ruby.two? 1296 | 1297 | # good 1298 | ruby = 'two' 1299 | ruby == 'two' 1300 | 1301 | # bad - Array#inquiry 1302 | pets = %w(cat dog).inquiry 1303 | pets.gopher? 1304 | 1305 | # good 1306 | pets = %w(cat dog) 1307 | pets.include? 'cat' 1308 | 1309 | # bad - Numeric#inquiry 1310 | 0.positive? 1311 | 0.negative? 1312 | 1313 | # good 1314 | 0 > 0 1315 | 0 < 0 1316 | ``` 1317 | 1318 | ## Time 1319 | 1320 | * 1321 | Config your timezone accordingly in `application.rb`. 1322 | [[link](#tz-config)] 1323 | 1324 | ```Ruby 1325 | config.time_zone = 'Eastern European Time' 1326 | # optional - note it can be only :utc or :local (default is :utc) 1327 | config.active_record.default_timezone = :local 1328 | ``` 1329 | 1330 | * 1331 | Don't use `Time.parse`. 1332 | [[link](#time-parse)] 1333 | 1334 | ```Ruby 1335 | # bad 1336 | Time.parse('2015-03-02 19:05:37') # => Will assume time string given is in the system's time zone. 1337 | 1338 | # good 1339 | Time.zone.parse('2015-03-02 19:05:37') # => Mon, 02 Mar 2015 19:05:37 EET +02:00 1340 | ``` 1341 | 1342 | * 1343 | Don't use `Time.now`. 1344 | [[link](#time-now)] 1345 | 1346 | ```Ruby 1347 | # bad 1348 | Time.now # => Returns system time and ignores your configured time zone. 1349 | 1350 | # good 1351 | Time.zone.now # => Fri, 12 Mar 2014 22:04:47 EET +02:00 1352 | Time.current # Same thing but shorter. 1353 | ``` 1354 | 1355 | ## Bundler 1356 | 1357 | * 1358 | Put gems used only for development or testing in the appropriate group in the 1359 | Gemfile. 1360 | [[link](#dev-test-gems)] 1361 | 1362 | * 1363 | Use only established gems in your projects. If you're contemplating on 1364 | including some little-known gem you should do a careful review of its source 1365 | code first. 1366 | [[link](#only-good-gems)] 1367 | 1368 | * 1369 | OS-specific gems will by default result in a constantly changing 1370 | `Gemfile.lock` for projects with multiple developers using different operating 1371 | systems. Add all OS X specific gems to a `darwin` group in the Gemfile, and 1372 | all Linux specific gems to a `linux` group: 1373 | [[link](#os-specific-gemfile-locks)] 1374 | 1375 | ```Ruby 1376 | # Gemfile 1377 | group :darwin do 1378 | gem 'rb-fsevent' 1379 | gem 'growl' 1380 | end 1381 | 1382 | group :linux do 1383 | gem 'rb-inotify' 1384 | end 1385 | ``` 1386 | 1387 | To require the appropriate gems in the right environment, add the 1388 | following to `config/application.rb`: 1389 | 1390 | ```Ruby 1391 | platform = RUBY_PLATFORM.match(/(linux|darwin)/)[0].to_sym 1392 | Bundler.require(platform) 1393 | ``` 1394 | 1395 | * 1396 | Do not remove the `Gemfile.lock` from version control. This is not some 1397 | randomly generated file - it makes sure that all of your team members get the 1398 | same gem versions when they do a `bundle install`. 1399 | [[link](#gemfile-lock)] 1400 | 1401 | ## Managing processes 1402 | 1403 | * 1404 | If your projects depends on various external processes use 1405 | [foreman](https://github.com/ddollar/foreman) to manage them. 1406 | [[link](#foreman)] 1407 | 1408 | # Further Reading 1409 | 1410 | There are a few excellent resources on Rails style, that you should consider if 1411 | you have time to spare: 1412 | 1413 | * [The Rails 4 Way](http://www.amazon.com/The-Rails-Addison-Wesley-Professional-Ruby/dp/0321944275) 1414 | * [Ruby on Rails Guides](http://guides.rubyonrails.org/) 1415 | * [The RSpec Book](https://pragprog.com/book/achbd/the-rspec-book) 1416 | * [The Cucumber Book](https://pragprog.com/book/hwcuc/the-cucumber-book) 1417 | * [Everyday Rails Testing with RSpec](https://leanpub.com/everydayrailsrspec) 1418 | * [Rails 4 Test Prescriptions](https://pragprog.com/book/nrtest2/rails-4-test-prescriptions) 1419 | * [Better Specs for RSpec](http://betterspecs.org) 1420 | 1421 | # Contributing 1422 | 1423 | Nothing written in this guide is set in stone. It's my desire to work together 1424 | with everyone interested in Rails coding style, so that we could ultimately 1425 | create a resource that will be beneficial to the entire Ruby community. 1426 | 1427 | Feel free to open tickets or send pull requests with improvements. Thanks in 1428 | advance for your help! 1429 | 1430 | You can also support the project (and RuboCop) with financial contributions via 1431 | [gittip](https://gratipay.com/~bbatsov/). 1432 | 1433 | [![Support via Gittip](https://rawgithub.com/twolfson/gittip-badge/0.2.0/dist/gittip.png)](https://gratipay.com/~bbatsov/) 1434 | 1435 | ## How to Contribute? 1436 | 1437 | It's easy, just follow the [contribution guidelines](https://github.com/bbatsov/rails-style-guide/blob/master/CONTRIBUTING.md). 1438 | 1439 | # License 1440 | 1441 | ![Creative Commons License](http://i.creativecommons.org/l/by/3.0/88x31.png) 1442 | This work is licensed under a [Creative Commons Attribution 3.0 Unported 1443 | License](http://creativecommons.org/licenses/by/3.0/deed.en_US) 1444 | 1445 | # Spread the Word 1446 | 1447 | A community-driven style guide is of little use to a community that doesn't know 1448 | about its existence. Tweet about the guide, share it with your friends and 1449 | colleagues. Every comment, suggestion or opinion we get makes the guide just a 1450 | little bit better. And we want to have the best possible guide, don't we? 1451 | 1452 | Cheers,
1453 | [Bozhidar](https://twitter.com/bbatsov) 1454 | --------------------------------------------------------------------------------