关于 rails-components
rails-components 是一项指导开发者使用 CSS 类、partials、helpers 和 ViewComponent 构建可复用 Rails 界面组件的 Skill。它帮助选择合适且尽量简单的实现方式,并统一组件结构与约定。
主要功能
- 01从 CSS 到 ViewComponent 的选择指南
- 02可复用 partial 与 helper 模式
- 03组件命名和文件约定
- 04插槽与变体实现指导
- 05组件展示页设置
适用场景
- 01为新界面组件选择实现方式
- 02重构重复的 Rails 标记
- 03整理共享组件样式与模板
- 04创建开发用组件展示页
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ thinkoodle/ rails-skills --skill rails-components安装后在 Claude Code 中输入/rails-components确认已加载。
附带文件reference.md
详细介绍
Rails 组件模式
使用 Rails 基础构件构建可复用、自包含、可组合的 UI 组件——局部模板、CSS 类和辅助方法。
理念
核心原则:
- 从 CSS 类开始——能用类解决,就不要急着使用局部模板
- 自包含——每个组件都在一处管理自己的标记和样式
- 可组合——组件可以自然地嵌套和组合
- 一致——相同的组件 = 相同的标记 = 各处相同的外观
- 循序渐进——只有在遇到问题时,才从 CSS 升级到局部模板,再到辅助方法
组件阶梯:
ViewComponent (gem) ← Complex: encapsulated Ruby + template + tests
Helper method ← Frequent: shorthand for common partials
Partial with locals ← Structured: logic, slots, complex markup
CSS classes only ← Simple: single element, few variants
始终从最底层开始。只有遇到问题时才向上升级。
何时使用此技能
- 构建新的可复用 UI 组件(按钮、卡片、模态框等)
- 在 CSS 类、局部模板、辅助方法或 ViewComponent 之间做选择
- 创建空状态、警报、徽章或数据表格
- 将重复的标记重构为共享组件
- 搭建用于开发的组件库
- 实现变体模式(尺寸、颜色、状态)
- 组织组件文件和样式
决策框架
使用 CSS 类的情况:
- 组件是单个元素或简单的包装元素(
<button>、<span>、<div>) - 变体仅影响视觉效果(颜色、尺寸、间距)
- 不需要条件逻辑
- 除内部文本外,不需要内容插槽
示例: 按钮、徽章、简单卡片、表单输入框、分隔线
使用局部模板的情况:
- 组件包含条件区域(根据参数显示或隐藏)
- 包含多个命名内容区域(页眉、正文、页脚)
- 标记结构并不简单(5 个以上元素)
- 需要默认值或计算后的属性
示例: 空状态、模态框、复杂卡片、数据表格、带图标的警报
使用辅助方法的情况:
- 局部模板的调用频率很高(10 个以上调用处)
- 希望使用更简洁的 Ruby API:
ui_badge("Active", variant: :success) - ERB 渲染语法显得繁琐的简单组件
- 希望在 Ruby 中组合多个元素
示例: 徽章、状态指示器、图标按钮、面包屑导航
使用 ViewComponent 的情况:
- 组件需要自己的单元测试
- 复杂的状态逻辑不适合放在视图中
- 团队规模较大,组件需要严格的接口
- 正在构建包含数十个组件的设计系统
大多数 Rails 应用都不需要 ViewComponent。局部模板和辅助方法足以满足 90% 的需求。
说明
第 1 步:检查现有组件
务必先查找现有模式:
# Find existing component partials
find app/views -path "*/components/*" -o -path "*/_component*" | head -20
# Find component CSS
find app/assets -name "*component*" -o -path "*/components/*" | head -20
# Find component helpers
rg "def ui_|def component_" app/helpers/
# Check for ViewComponent usage
ls app/components/ 2>/dev/null
rg "ViewComponent" Gemfile
遵循项目现有约定。 一致性优先于个人偏好。
第 2 步:选择合适的方案
参考上面的决策框架。请自问:
- 只用 CSS 类就能实现吗?→ 那就这样做。
- 需要条件逻辑或插槽吗?→ 使用局部模板。
- 是否有 10 个以上位置以相同模式调用?→ 添加辅助方法。
- 是否需要隔离测试和复杂状态?→ 使用 ViewComponent。
第 3 步:文件组织
app/views/components/ # Shared component partials
├── _modal.html.erb
├── _empty_state.html.erb
├── _card.html.erb
├── _alert.html.erb
└── _data_table.html.erb
app/assets/stylesheets/components/ # One CSS file per component
├── buttons.css
├── badges.css
├── cards.css
├── modals.css
├── empty-states.css
├── alerts.css
└── tables.css
app/helpers/
└── component_helper.rb # Helper shortcuts for common components
命名约定:
- 局部模板:
app/views/components/中的_snake_case.html.erb - CSS 文件:
app/assets/stylesheets/components/中的kebab-case.css - 辅助方法:在
ComponentHelper中使用ui_component_name前缀 - CSS 类:
.component-name、.component-name-element、.component-name--variant
第 4 步:构建纯 CSS 组件
对于简单组件,定义 CSS 类并直接在模板中使用:
/* app/assets/stylesheets/components/buttons.css */
@layer components {
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.15s;
}
.btn-primary { background: var(--color-primary); color: white; }
.btn-secondary { background: var(--color-surface); border: 1px solid var(--color-border); }
.btn-danger { background: var(--color-danger); color: white; }
.btn-ghost { background: transparent; }
.btn-sm { padding: 0.25rem 0.75rem; font-size: 0.875rem; }
.btn-lg { padding: 0.75rem 1.5rem; }
.btn-icon { padding: 0.5rem; }
}
<%# Usage — no partial needed %>
<button class="btn btn-primary">Save</button>
<button class="btn btn-danger btn-sm">Delete</button>
<%= link_to "View", post_path(@post), class: "btn btn-secondary" %>
第 5 步:构建基于局部模板的组件
每个局部模板都必须包含注释块,说明其接口:
<%# app/views/components/_empty_state.html.erb %>
<%#
Empty State Component
Arguments:
icon: (String) Icon name — required
title: (String) Main heading — required
description: (String, optional) Supporting text
class: (String, optional) Additional CSS classes
Block: Optional action buttons
%>
<div class="empty-state <%= local_assigns[:class] %>">
<div class="empty-state-icon">
<%= lucide_icon icon, size: 48 %>
</div>
<h3 class="empty-state-title"><%= title %></h3>
<% if local_assigns[:description] %>
<p class="empty-state-description"><%= description %></p>
<% end %>
<% if block_given? %>
<div class="empty-state-actions">
<%= yield %>
</div>
<% end %>
</div>
局部模板的关键规则:
- 使用
local_assigns[:key]检查可选参数——不要使用defined?或if key - 当参数需要后备值时,使用
||=设置默认值 - 使用
block_given?检查是否传入了代码块 - 使用
yield提供主要内容插槽 - 使用
capture提供其他命名插槽
第 6 步:使用 Capture 实现插槽
对于包含多个内容区域的组件:
<%# app/views/components/_card.html.erb %>
<%#
Card Component
Arguments:
header: (String|HTML, optional) Header content — use capture for complex HTML
footer: (String|HTML, optional) Footer content — use capture for complex HTML
class: (String, optional) Additional CSS classes
variant: (Symbol, optional) Card variant — :default, :stat, :interactive
Block: Card body content (required)
%>
<%
variant ||= :default
css_class = ["card", ("card-#{variant}" unless variant == :default), local_assigns[:class]].compact.join(" ")
%>
<div class="<%= css_class %>">
<% if local_assigns[:header] %>
<div class="card-header"><%= header %></div>
<% end %>
<div class="card-body">
<%= yield %>
</div>
<% if local_assigns[:footer] %>
<div class="card-footer"><%= footer %></div>
<% end %>
</div>
调用方使用 capture 提供富内容插槽:
<%= render "components/card",
header: capture { tag.h3("Settings", class: "card-title") },
footer: capture {
link_to("Cancel", "#", class: "btn btn-secondary") +
link_to("Save", "#", class: "btn btn-primary")
} do %>
<p>Card body content here.</p>
<% end %>
第 7 步:添加辅助快捷方式
对于经常使用的组件,创建辅助方法:
# app/helpers/component_helper.rb
module ComponentHelper
def ui_badge(text, variant: :muted)
tag.span(text, class: "badge badge-#{variant}")
end
def ui_empty_state(title:, icon: nil, description: nil, &block)
render("components/empty_state",
title: title,
icon: icon,
description: description,
&block)
end
def ui_alert(variant: :info, dismissible: false, &block)
render("components/alert",
variant: variant,
dismissible: dismissible,
&block)
end
end
使用方式会变得简洁:
<%= ui_badge "Active", variant: :success %>
<%= ui_badge "Draft" %>
<%= ui_empty_state(
icon: "inbox",
title: "No messages",
description: "Check back later") %>
第 8 步:处理变体
纯视觉改动使用 CSS 变体:
/* Size variants */
.btn-sm { padding: 0.25rem 0.75rem; font-size: 0.875rem; }
.btn-lg { padding: 0.75rem 1.5rem; }
/* State variants */
.card-interactive:hover { border-color: var(--color-border-strong); }
.card-flush .card-body { padding: 0; }
标记发生变化时使用 部分参数变体:
<%# In the partial %>
<%
variant ||= :info
icon_name = { success: "check-circle", error: "alert-circle",
warning: "alert-triangle", info: "info" }[variant.to_sym]
%>
<div class="alert alert-<%= variant %>" role="alert">
<%= lucide_icon icon_name, size: 18 %>
<span class="alert-content"><%= yield %></span>
</div>
第 9 步:创建组件展示页
这是开发的必备项。 让任何人都能浏览所有组件:
# config/routes.rb
if Rails.env.development?
get "components", to: "components#index"
end
# app/controllers/components_controller.rb
class ComponentsController < ApplicationController
layout "application"
def index
end
end
<%# app/views/components/index.html.erb %>
<div style="max-width: 48rem; margin: 2rem auto; padding: 0 1rem;">
<h1>Component Gallery</h1>
<h2>Buttons</h2>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 2rem;">
<button class="btn btn-primary">Primary</button>
<button class="btn btn-secondary">Secondary</button>
<button class="btn btn-danger">Danger</button>
<button class="btn btn-ghost">Ghost</button>
<button class="btn btn-sm">Small</button>
</div>
<h2>Badges</h2>
<div style="display: flex; gap: 0.5rem; margin-bottom: 2rem;">
<span class="badge badge-muted">Draft</span>
<span class="badge badge-success">Active</span>
<span class="badge badge-warning">Pending</span>
<span class="badge badge-danger">Failed</span>
</div>
<h2>Empty States</h2>
<%= render "components/empty_state",
icon: "inbox",
title: "No messages yet",
description: "When you receive messages, they'll appear here." do %>
<button class="btn btn-primary">Compose</button>
<% end %>
<h2>Alerts</h2>
<%= render "components/alert", variant: :info do %>Info message<% end %>
<%= render "components/alert", variant: :success do %>Success message<% end %>
<%= render "components/alert", variant: :warning do %>Warning message<% end %>
<%= render "components/alert", variant: :error do %>Error message<% end %>
</div>
反模式
- 所有内容都用 partial —
<span class="badge">不需要render "components/badge"。使用 CSS 类就够了。 - 使用
defined?(variable)— 在 partial 中始终使用local_assigns[:variable]。defined?并不可靠。 - 将全局样式用于组件专属内容 — 组件 CSS 应放在
components/中。不要污染application.css。 - 命名不一致 — 选择一种约定并坚持使用。使用
_empty_state.html.erb,而不是_emptyState.html.erb。 - 万能组件 — 如果一个 partial 接收 10 个以上的参数,就将其拆分为更小的组件。
- 缺少文档 — 每个 partial 都需要一个列出其参数的注释块。
- 将 HTML 字符串作为参数传递 — 使用
capture { }块或yield,绝不要传入原始 HTML 字符串。 - 没有展示页 — 如果无法在同一处查看所有组件,它们就会逐渐变得不一致。
CSS 约定
/* Use @layer for specificity management */
@layer components {
.component-name { /* base styles */ }
.component-name-element { /* child element */ }
.component-name--variant { /* BEM variant — OR use component-variant */ }
}
设计令牌的使用:
.component {
color: var(--color-ink); /* Text colors */
background: var(--color-surface); /* Backgrounds */
border: 1px solid var(--color-border); /* Borders */
padding: var(--space-4); /* Spacing scale */
font-size: var(--text-sm); /* Type scale */
border-radius: var(--radius-md); /* Border radius */
}
如果项目使用设计令牌/CSS 自定义属性,就使用它们。如果没有,就使用一致的原始值。
快速参考
local_assigns 速查表
<%# Check if argument was passed (even if nil) %>
<% if local_assigns.key?(:title) %>
<%# Check if argument was passed AND is truthy %>
<% if local_assigns[:title] %>
<%# Default value %>
<% variant = local_assigns[:variant] || :default %>
<%# OR %>
<% variant ||= :default %>
<%# Pass-through CSS classes %>
<div class="component <%= local_assigns[:class] %>">
Render 语法
<%# Simple — no block %>
<%= render "components/empty_state", icon: "inbox", title: "Empty" %>
<%# With block %>
<%= render "components/modal", title: "Confirm" do %>
<p>Are you sure?</p>
<% end %>
<%# With capture slots %>
<%= render "components/card",
header: capture { tag.h3("Title") },
footer: capture { tag.button("Save", class: "btn btn-primary") } do %>
<p>Body</p>
<% end %>
常见组件模板
完整模板请参见此技能目录中的 reference.md:
- 按钮、徽章(仅 CSS)
- 卡片、空状态、提示框(partial)
- 使用 Stimulus 的模态框(partial + JS)
- 数据表格(partial)
- 上述所有组件的辅助方法
评论
支持 Markdown