关于 action-text
为 Rails 开发者提供 Action Text 实现指导,涵盖富文本字段、编辑器、附件、安全渲染和测试。根据 Rails 版本介绍 Lexxy 与 Trix,并说明预加载富文本及嵌入内容以避免 N+1 查询。
主要功能
- 01安装并配置 Action Text
- 02使用 Lexxy 或 Trix 编辑富文本
- 03安全渲染及提取纯文本
- 04通过 Active Storage 管理附件
- 05嵌入自定义对象
- 06预加载内容以避免 N+1 查询
适用场景
- 01为 Rails 模型添加富文本字段
- 02在表单中集成富文本编辑器
- 03在文章中展示图片或文件附件
- 04优化列表页面的富文本查询
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ thinkoodle/ rails-skills --skill action-text安装后在 Claude Code 中输入/action-text确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/attachables.mdeditors.mdsecurity.mdstyling.mdtesting.md详细介绍
Rails Action Text 专家
在 Rails 应用中使用 Action Text 和 Lexxy 编辑器(Rails 8.1+)或 Trix(Rails 6-8.0)实现富文本编辑。
核心概念
Action Text 通过多态关联将富文本存储在单独的 action_text_rich_texts 表中——而不是模型的表中。它负责清理内容、渲染内容、处理附件(通过 Active Storage)以及处理嵌入对象(通过 Signed GlobalID)。
编辑器沿革:
- Trix — Action Text 的原始编辑器(Rails 6+)。仍可使用,仍受支持。
- Lexxy — Basecamp 推出的现代替代方案(Rails 8.1+)。暗色模式更完善、支持 CSS 自定义属性,用户体验更佳。新项目请使用 Lexxy。
何时使用此技能
- 为模型添加富文本字段
- 在新的或现有的 Rails 应用中设置 Action Text
- 配置 Lexxy 或 Trix 编辑器的外观
- 安全地渲染富文本内容
- 处理富文本中的图片/文件附件
- 创建自定义可附加对象(嵌入用户、产品等)
- 修复富文本相关的 N+1 查询
- 设置编辑器和渲染内容的样式
- 测试富文本功能
说明
步骤 1:检查是否已安装 Action Text
# Check for Action Text tables
bin/rails runner "puts ActiveRecord::Base.connection.table_exists?('action_text_rich_texts')"
# Check for Action Text config files
ls app/views/layouts/action_text/contents/_content.html.erb 2>/dev/null
ls app/views/active_storage/blobs/_blob.html.erb 2>/dev/null
# Check Gemfile for editor
grep -E "trix|lexxy" Gemfile
如果尚未安装,请先运行安装步骤(步骤 2)。如果已安装,请跳至步骤 3。
步骤 2:安装 Action Text
bin/rails action_text:install
bin/rails db:migrate
这会创建:
action_text_rich_texts表的迁移(如果缺少 Active Storage 表,也会一并创建)- 编辑器的 JavaScript 导入
app/views/layouts/action_text/contents/_content.html.erb— 内容包装器局部视图app/views/active_storage/blobs/_blob.html.erb— 附件渲染局部视图app/assets/stylesheets/actiontext.css— 默认样式
对于 Lexxy(Rails 8.1+),还需添加:
# Gemfile
gem "lexxy"
bundle install
重要:Lexxy 的样式表加载顺序:
<%# app/views/layouts/application.html.erb %>
<%# Lexxy FIRST, then app styles (so your overrides win) %>
<%= stylesheet_link_tag "lexxy", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
Agent 常见错误: 忘记运行 bin/rails action_text:install。如果没有运行,迁移、JS 导入和视图局部模板都会缺失。仅声明 has_rich_text 并不足够。
步骤 3:为模型添加富文本
# app/models/article.rb
class Article < ApplicationRecord
has_rich_text :content
end
要点:
articles表无需添加列——内容存储在action_text_rich_texts中- 属性名称可以任意指定(
:content、:body、:description等) - 一个模型可以有多个富文本属性:
has_rich_text :body和has_rich_text :summary - 每个
has_rich_text都会创建一条独立的ActionText::RichText记录
步骤 4:将编辑器添加到表单
<%# app/views/articles/_form.html.erb %>
<%= form_with model: @article do |form| %>
<div class="field">
<%= form.label :content %>
<%= form.rich_text_area :content %>
</div>
<% end %>
在控制器中允许该属性:
class ArticlesController < ApplicationController
def create
@article = Article.create!(article_params)
redirect_to @article
end
private
def article_params
params.expect(article: [:title, :content])
end
end
注意: rich_text_area(或 rich_textarea)都可以使用。富文本内容是单个字符串参数,无需特殊嵌套。
步骤 5:渲染富文本内容
<%# Safe — Action Text sanitizes content automatically %>
<%= @article.content %>
就是这样。ActionText::RichText#to_s 会返回经过清理、可直接嵌入的安全 HTML。
获取纯文本(例如摘要、元描述):
@article.content.to_plain_text
# => "Hello world. This is bold text."
# Truncated excerpt
truncate(@article.content.to_plain_text, length: 150)
检查内容是否存在:
@article.content.blank? # true if no content
@article.content.present? # true if has content
步骤 6:设置编辑器和内容的样式
Lexxy(Rails 8.1+)
编辑器尺寸:
lexxy-editor,
.lexxy-editor {
min-height: 300px;
}
渲染内容的样式(使用 .lexxy-content 包装器):
<%# app/views/layouts/action_text/contents/_content.html.erb %>
<div class="lexxy-content">
<%= yield %>
</div>
.lexxy-content {
line-height: 1.6;
overflow-wrap: break-word;
}
.lexxy-content p { margin: 0 0 1rem; }
.lexxy-content h1, .lexxy-content h2, .lexxy-content h3 {
font-weight: 600;
line-height: 1.3;
margin: 1.5rem 0 0.75rem;
}
.lexxy-content ul, .lexxy-content ol {
margin: 0 0 1rem;
padding-left: 1.5rem;
}
.lexxy-content blockquote {
border-left: 3px solid var(--color-border);
margin: 1rem 0;
padding: 0.5rem 0 0.5rem 1rem;
color: var(--color-ink-muted);
}
.lexxy-content code {
font-size: 0.875em;
background: var(--color-surface-muted);
padding: 0.125rem 0.375rem;
border-radius: 4px;
}
.lexxy-content pre {
background: var(--color-surface-muted);
border-radius: 8px;
padding: 1rem;
overflow-x: auto;
margin: 1rem 0;
}
使用 CSS 自定义属性实现暗色模式:
:root {
--lexxy-color-canvas: var(--color-surface);
--lexxy-color-text: var(--color-ink);
--lexxy-color-link: var(--color-link);
--lexxy-color-code-bg: var(--color-surface-muted);
--lexxy-focus-ring-color: var(--color-primary);
}
完整的 Lexxy CSS 变量列表请参见 references/editors.md。
Trix(Rails 6–8.0)
内容包装器使用 .trix-content:
<%# app/views/layouts/action_text/contents/_content.html.erb %>
<div class="trix-content">
<%= yield %>
</div>
在 app/assets/stylesheets/actiontext.css 中覆盖样式。
Agent 常见错误: 完全不为 Action Text 内容设置样式。原始输出看起来没有样式。始终为 .trix-content 或 .lexxy-content 提供 CSS。
步骤 7:处理附件
Action Text 使用 Active Storage 处理文件附件(图片、拖放到编辑器中的文件)。
前提条件:
- 必须安装 Active Storage(
bin/rails active_storage:install) - 用于图像处理的
libvips或ImageMagick - Gemfile 中包含
image_processinggem
自定义附件渲染:
<%# app/views/active_storage/blobs/_blob.html.erb %>
<figure class="attachment attachment--<%= blob.representable? ? "preview" : "file" %> attachment--<%= blob.filename.extension %>">
<% if blob.representable? %>
<%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [800, 600] : [1024, 768]) %>
<% end %>
<figcaption class="attachment__caption">
<% if caption = blob.try(:caption) %>
<%= caption %>
<% else %>
<span class="attachment__name"><%= blob.filename %></span>
<span class="attachment__size"><%= number_to_human_size blob.byte_size %></span>
<% end %>
</figcaption>
</figure>
步骤 8:避免 N+1 查询
这非常关键。 加载富文本会为每条记录触发额外查询。请始终预加载。
# BAD — N+1 queries (one query per article for rich text)
Article.all.each { |a| a.content.to_s }
# GOOD — Preload rich text
Article.all.with_rich_text_content
# GOOD — Preload rich text AND its embedded attachments
Article.all.with_rich_text_content_and_embeds
作用域名称是动态的: 根据你的 has_rich_text :name 声明,名称为 with_rich_text_#{name} 和 with_rich_text_#{name}_and_embeds。
# If you have: has_rich_text :body
Article.with_rich_text_body
Article.with_rich_text_body_and_embeds
# If you have: has_rich_text :description
Product.with_rich_text_description_and_embeds
Agent 常见错误: 忘记添加 _and_embeds。如果不添加,渲染附件图片时会触发额外查询。
步骤 9:自定义可嵌入对象(嵌入内容)
使用 Signed GlobalIDs 将任何 Active Record 模型嵌入富文本。
# app/models/user.rb
class User < ApplicationRecord
include ActionText::Attachable
def to_attachable_partial_path
"users/mention"
end
end
<%# app/views/users/_mention.html.erb %>
<span class="user-mention">@<%= user.name %></span>
以编程方式插入:
user = User.find(1)
html = %(<action-text-attachment sgid="#{user.attachable_sgid}"></action-text-attachment>)
article.update!(content: "Hello #{html}")
妥善处理已删除的记录:
class User < ApplicationRecord
include ActionText::Attachable
def self.to_missing_attachable_partial_path
"users/missing_mention"
end
end
<%# app/views/users/missing_mention.html.erb %>
<span class="user-mention user-mention--deleted">@deleted user</span>
步骤 10:测试富文本
模型测试:
require "test_helper"
class ArticleTest < ActiveSupport::TestCase
test "accepts rich text content" do
article = Article.new(title: "Test", content: "<h1>Hello</h1><p>World</p>")
assert article.content.present?
assert_includes article.content.to_plain_text, "Hello"
assert_includes article.content.to_plain_text, "World"
end
test "content is blank when not set" do
article = Article.new(title: "Test")
assert article.content.blank?
end
end
请求测试:
require "test_helper"
class ArticlesRequestTest < ActionDispatch::IntegrationTest
test "creates article with rich text" do
assert_difference "Article.count", 1 do
post articles_path, params: {
article: { title: "Test", content: "<p>Rich text body</p>" }
}
end
assert_equal "Rich text body", Article.last.content.to_plain_text
end
end
系统测试(用于测试编辑器交互):
require "application_system_test_case"
class ArticlesSystemTest < ApplicationSystemTestCase
test "creates article with rich text editor" do
visit new_article_path
fill_in "Title", with: "My Article"
# Fill the rich text editor
find("trix-editor").click
find("trix-editor").set("Hello from the editor")
click_on "Create Article"
assert_text "Hello from the editor"
end
end
UUID 主键
如果你的模型使用 UUID,请更新 Action Text 迁移:
# In the generated migration
t.references :record, null: false, polymorphic: true, index: false, type: :uuid
内容安全
Action Text 使用安全名单方式在渲染时清理 HTML。只有允许的标签和属性会被保留。无需手动调用 sanitize。
自定义清理(如有需要):
# config/application.rb
config.action_text.sanitizer_allowed_tags = ActionText::ContentHelper::ALLOWED_TAGS + ["iframe"]
config.action_text.sanitizer_allowed_attributes = ActionText::ContentHelper::ALLOWED_ATTRIBUTES + ["src", "frameborder"]
⚠️ 扩展允许名单时务必格外谨慎。 添加 <iframe> 或 <script> 会引入 XSS 漏洞。只有在信任所有内容作者的情况下才这样做。
快速参考
必要命令
bin/rails action_text:install # Install Action Text
bin/rails db:migrate # Run migrations
bin/rails active_storage:install # Install Active Storage (if not present)
模型声明
has_rich_text :content # Single rich text field
has_rich_text :body # Name it whatever you want
has_rich_text :content # Multiple fields OK
has_rich_text :summary # on the same model
表单辅助方法
<%= form.rich_text_area :content %>
<%= form.rich_text_area :content, placeholder: "Write something..." %>
<%= form.rich_text_area :content, data: { controller: "editor" } %>
渲染
<%= @article.content %> # Safe HTML
<%= @article.content.to_plain_text %> # Plain text
<%= truncate(@article.content.to_plain_text, length: 200) %> # Excerpt
预加载(防止 N+1)
Model.with_rich_text_fieldname # Preload text only
Model.with_rich_text_fieldname_and_embeds # Preload text + attachments
关键文件
| 文件 | 用途 |
|---|---|
app/views/layouts/action_text/contents/_content.html.erb | 内容容器(.trix-content 或 .lexxy-content) |
app/views/active_storage/blobs/_blob.html.erb | 附件渲染模板 |
app/assets/stylesheets/actiontext.css | Action Text 默认样式 |
应避免的反模式
- 跳过安装生成器 — 未运行
action_text:install就使用has_rich_text,会导致缺少数据表、JS 和局部模板 - 没有内容样式 — 原始 Action Text 输出需要为
.trix-content/.lexxy-content添加 CSS - 忽略 N+1 — 在列表视图中始终使用
with_rich_text_X_and_embeds - 给模型添加列 — 富文本存储在
action_text_rich_texts中,而非你的数据表 - 手动清理 HTML — Action Text 会处理此操作;重复清理会破坏内容
- Lexxy 样式表顺序错误 — Lexxy CSS 必须在应用 CSS 之前加载
- 缺少 Active Storage 依赖项 — 没有
libvips会导致图片渲染异常 - 未处理缺失的可嵌入对象 — 没有
to_missing_attachable_partial_path时,已删除的记录会渲染为空白框