mcpField
A
action-text

action-text

github.com/thinkoodle开发7187 次安装8 周前更新
代码仓库

关于 action-text

为 Rails 开发者提供 Action Text 实现指导,涵盖富文本字段、编辑器、附件、安全渲染和测试。根据 Rails 版本介绍 Lexxy 与 Trix,并说明预加载富文本及嵌入内容以避免 N+1 查询。

主要功能

  1. 01安装并配置 Action Text
  2. 02使用 Lexxy 或 Trix 编辑富文本
  3. 03安全渲染及提取纯文本
  4. 04通过 Active Storage 管理附件
  5. 05嵌入自定义对象
  6. 06预加载内容以避免 N+1 查询

适用场景

  1. 01为 Rails 模型添加富文本字段
  2. 02在表单中集成富文本编辑器
  3. 03在文章中展示图片或文件附件
  4. 04优化列表页面的富文本查询

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/thinkoodle/rails-skills --skill action-text

安装后在 Claude Code 中输入/action-text确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
attachables.md
editors.md
security.md
styling.md
testing.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_processing gem

自定义附件渲染:

<%# 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.cssAction Text 默认样式

应避免的反模式

  1. 跳过安装生成器 — 未运行 action_text:install 就使用 has_rich_text,会导致缺少数据表、JS 和局部模板
  2. 没有内容样式 — 原始 Action Text 输出需要为 .trix-content/.lexxy-content 添加 CSS
  3. 忽略 N+1 — 在列表视图中始终使用 with_rich_text_X_and_embeds
  4. 给模型添加列 — 富文本存储在 action_text_rich_texts 中,而非你的数据表
  5. 手动清理 HTML — Action Text 会处理此操作;重复清理会破坏内容
  6. Lexxy 样式表顺序错误 — Lexxy CSS 必须在应用 CSS 之前加载
  7. 缺少 Active Storage 依赖项 — 没有 libvips 会导致图片渲染异常
  8. 未处理缺失的可嵌入对象 — 没有 to_missing_attachable_partial_path 时,已删除的记录会渲染为空白框

评论

支持 Markdown