关于 rails-conventions
这是一套面向 Rails 应用开发与维护的代码和测试约定,强调简单、易测试的结构,并涵盖控制器、服务对象、Turbo Streams、JavaScript 导入和性能。
主要功能
- 01控制器与模型职责划分建议
- 02服务对象提取规则
- 03内联 Turbo Streams 模式
- 04基于 Minitest 与 fixtures 的测试
- 05使用 importmap 管理 JavaScript 导入
- 06性能与命名实践
适用场景
- 01组织 Rails 控制器与业务逻辑
- 02判断何时提取服务对象
- 03使用 fixtures 和外部依赖桩测试行为
- 04区分公共与管理端 JavaScript 依赖
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ majesticlabs-dev/ majestic-marketplace --skill rails-conventions安装后在 Claude Code 中输入/rails-conventions确认已加载。
详细介绍
Rails 约定
注重实践的 Rails 模式,助力编写整洁、易维护的代码。
核心理念
-
重复 > 复杂:简单的重复代码胜过复杂的 DRY 抽象
- “我宁愿有四个简单的控制器,也不愿有三个复杂的控制器”
-
可测试性 = 质量:如果代码难以测试,就需要重构其结构
-
增加控制器永远不是坏事。让控制器变复杂才是坏事。
Turbo Streams
简单的 Turbo Streams 必须直接写在控制器中:
# FAIL: Separate .turbo_stream.erb files for simple operations
render "posts/update"
# PASS: Inline array
render turbo_stream: [
turbo_stream.replace("post_#{@post.id}", partial: "posts/post", locals: { post: @post }),
turbo_stream.remove("flash")
]
控制器与 Concerns
业务逻辑应放在模型或 Concern 中,而不是控制器中。
# Concern structure
module Dispatchable
extend ActiveSupport::Concern
included do
scope :available, -> { where(status: "pending") }
end
class_methods do
def claim!(batch_size)
# class-level behavior
end
end
end
提取服务
遇到以下多项情况时进行提取:
- 复杂的业务规则(不只是“代码很长”)
- 协调多个模型
- 与外部 API 交互
- 可跨控制器复用的逻辑
服务结构:
- 只有一个公共方法
- 按职责命名空间组织(
Extraction::RegexExtractor) - 构造函数接收依赖项
- 返回数据结构,而不是领域对象
现代 Ruby 风格
# Hash shorthand
{ id:, slug:, doc_type: kind }
# Safe navigation
created_at&.iso8601
@setting ||= SlugSetting.active.find_by!(slug:)
# Keyword arguments
def extract(document_type:, subject:, filename:)
def process!(strategy: nil)
枚举模式
# Frozen arrays with validation
STATUSES = %w[processed needs_review].freeze
enum :status, STATUSES.index_by(&:itself), validate: true
Scope 模式
# Guard with .present?, chainable design
scope :by_slug, ->(slug) { where(slug:) if slug.present? }
scope :from_date, ->(date) { where(created_at: Date.parse(date).beginning_of_day..) if date.present? }
def self.filtered(params)
all.by_slug(params[:slug]).by_kind(params[:kind])
rescue ArgumentError
all
end
错误处理
# Domain-specific errors
class InactiveSlug < StandardError; end
# Log with context, re-raise for upstream
def handle_exception!(error:)
log_error("Exception #{error.class}: #{error.message}", error:)
mark_failed!(error.message)
raise
end
测试(Minitest + Fixtures)
test "describes expected behavior" do
email = emails(:two)
email.process
email.reload
assert_equal "finished", email.processing_status
end
原则:
- 行为驱动:测试做什么,而不是如何实现
- 基于 Fixture:使用
emails(:two)进行设置 - 模拟外部依赖:对 S3、API、PDF 使用存根
- 状态验证:操作后调用
.reload - 辅助方法:
build_valid_email、with_stubbed_download
命名(5 秒规则)
如果你无法在 5 秒内理解:
# FAIL
show_in_frame
process_stuff
# PASS
fact_check_modal
_fact_frame
JavaScript 与 Importmap
Rails 7+ 使用 importmap 管理 JS 依赖。将依赖限定在范围最小的入口点中。
多个入口点
# config/importmap.rb
pin "application" # Public entrypoint
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
# Admin-only dependencies - pinned but only imported in admin entrypoint
pin "chartkick", to: "chartkick.js"
pin "Chart.bundle", to: "Chart.bundle.js"
// app/javascript/application.js — public pages only
import "@hotwired/turbo-rails"
import "@hotwired/stimulus"
import "controllers"
// app/javascript/admin.js — imports public base + admin-only libs
import "application"
import "chartkick"
import "Chart.bundle"
<%# Admin layout %>
<%= javascript_importmap_tags("admin") %>
<%# All other layouts %>
<%= javascript_importmap_tags %>
添加新的 JS 依赖
- 确定适用范围:面向公众还是仅限管理端?
- 在
config/importmap.rb中 pin - 在正确的入口点中导入
- 绝不要将仅限管理端的库添加到
application.js
性能
- 考虑规模带来的影响
- 不要过早缓存
- KISS——保持简单
- 索引会拖慢写入,只在需要时添加
评论
支持 Markdown