首页
/ Rails Action Text 全面指南:富文本编辑、附件管理与安全渲染

Rails Action Text 全面指南:富文本编辑、附件管理与安全渲染

2026-09-07 09:07:40作者:何将鹤

Action Text 是 Ruby on Rails 内置的富文本处理框架,它把所见即所得(WYSIWYG)编辑器 Trix、ActionText::RichText 数据模型与 Active Storage 附件系统整合在一起,让你可以用一个 has_rich_text 声明为任意 Active Record 模型添加格式化正文(粗体、斜体、链接、图片、引用等)能力,并在保存、渲染、安全消毒与附件展示全链路上获得开箱即用的实现。读完本指南,你将掌握 Action Text 的安装配置、富文本创建与渲染、Trix 编辑器样式定制,以及 Active Storage 直传与 Signed GlobalID 两种附件处理方案的完整实战方法。本指南以仓库文档 action_text_overview.md 为核心骨架,并结合本仓库中 actiontext 组件的源码实现进行纵深佐证。

什么是 Action Text?

Action Text 用于便捷地创建、存储与展示富文本内容。所谓富文本,是指带有格式元素(如粗体、斜体、颜色、超链接)的文本,相比纯文本拥有更强的视觉表现与结构化信息。它允许我们把富文本内容创建出来、存入数据表,再把它挂到任意模型上。

Action Text 内置了一个名为 Trix 的 WYSIWYG 编辑器,用于在 Web 应用中给用户提供友好、易用的富文本创建与编辑界面。Trix 负责从文本格式化、添加链接或引用,到嵌入图片等一系列丰富的编辑能力。

由 Trix 编辑器产出的富文本会保存在独立的 RichText 模型中,该模型可与应用中的任意 Active Record 模型建立关联。与此同时,正文中嵌入的图片(或其他附件)会自动通过 Active Storage(Action Text 将其作为依赖引入)存储,并与这条 RichText 记录关联。渲染时,Action Text 会先对内容做安全消毒(sanitizing),使其能够安全地直接嵌入页面 HTML——这正是富文本内容可直接输出的关键前提。

为什么是 Trix,而不是 contenteditable

大多数 WYSIWYG 编辑器都只是对 HTML 的 contenteditableexecCommand API 的封装。这两个 API 最初由微软为 Internet Explorer 5.5 的网页实时编辑设计,之后被其他浏览器逆向并复制。它们从未被完整地规范与文档化,而 WYSIWYG HTML 编辑器的范围又极其庞大,因此每个浏览器的实现都各有各的 bug 与怪癖,JavaScript 开发者常常不得不手工处理这些不一致。

Trix 规避这些不一致的方式是:contenteditable 当作一个 I/O 设备——当输入进入编辑器时,Trix 将其转换为对自己内部文档模型的一次编辑操作,然后再把该文档重新渲染回编辑器。这让 Trix 能够完全掌控每一次按键之后发生的一切,从而彻底绕开 execCommand 及其带来的一系列浏览器差异问题。

安装与配置 Action Text

运行安装命令

要安装 Action Text 并开始使用富文本,在应用根目录执行:

$ bin/rails action_text:install

以当前仓库中的安装生成器 install_generator.rb 为参考,该命令会完成以下几件事:

  • 安装 JS 依赖并接入打包器:安装 trix@rails/actiontext 两个 JavaScript 包,并自动把它们 importapplication.js(若项目使用 importmap,则会在 config/importmap.rb 中追加 pin 声明)。仓库中生成器还支持 --editor 选项(默认 "trix"),Action Text 已做成可插拔的编辑器体系(见 engine.rbconfig.action_text.editorsconfig.action_text.editor 配置)。
  • 添加 image_processing gem:用于对嵌入图片及其他附件执行 Active Storage 的分析与变换。更多细节参见 Active Storage Overview
  • 添加迁移:创建存储富文本与附件的表——action_text_rich_textsactive_storage_blobsactive_storage_attachmentsactive_storage_variant_records
  • 创建 actiontext.css:包含全部 Trix 样式及 Action Text 所需的覆盖样式。
  • 添加默认视图 partial:生成渲染 Action Text 内容与 Active Storage 附件(即 blob)的默认 partial _content.html_blob.html

之后执行数据库迁移,新的 action_text_*active_storage_* 表就会进入你的应用:

$ bin/rails db:migrate

action_text_rich_texts 表与多态关联

当 Action Text 安装创建 action_text_rich_texts 表时,它使用了多态关联(polymorphic association),以便多个模型都能添加富文本属性。表结构中的 record_typerecord_id 两列分别存存储拥有富文本的模型的类名(ClassName)与记录 ID。

借助多态关联,一个模型可以通过单条关联同时属于多个其他模型。可参见仓库中实际的迁移文件 20180528164100_create_action_text_tables.rb

create_table :action_text_rich_texts, id: primary_key_type do |t|
  t.string     :name, null: false
  t.text       :body, size: :long
  t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
  t.timestamps

  t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true
end

除多态列外,name 列记录该富文本所属的 has_rich_text 属性名,body 列以 long 文本保存 Trix 序列化后的正文,而 (record_type, record_id, name) 上的唯一索引保证每个模型实例的每个富文本属性只有一条记录。需要指出的是,仓库迁移中的主键/外键类型并非写死:primary_and_foreign_key_types 会读取 Rails.configuration.generators 下 ORM 的 primary_key_type 配置(默认主键 primary_key、外键 bigint),若应用整体使用 UUID 主键,该配置会一并生效。

使用 UUID 主键时的注意事项

如果包含 Action Text 内容的模型使用 UUID 作为标识符,那么所有使用 Action Text 属性的模型都必须统一使用 UUID 主键。同时,由于多态外键类型跟随上述全局配置推导,生成的迁移可能仍按 bigint 处理 record 引用;为保证一致,通常仍需手动修改 Action Text 生成的迁移,把 references 行明确为 type: :uuid

t.references :record, null: false, polymorphic: true, index: false, type: :uuid

创建富文本内容

本节介绍为模型添加富文本所需的配置步骤。

核心机制:RichText 记录与 has_rich_text

RichText 记录把 Trix 编辑器产出的内容保存在一个经过序列化的 body 属性中,同时持有所有通过 Active Storage 存储的嵌入文件的引用。这条记录会与"想要富文本内容的 Active Record 模型"关联起来,关联方式就是在该模型上调用 has_rich_text 类方法:

# app/models/article.rb
class Article < ApplicationRecord
  has_rich_text :content
end

注意:不需要在 Article 表中添加 content 列。has_rich_text 会把 content 关联到已创建的 action_text_rich_texts 表并回链到你的模型。属性名也可以自定义为 content 以外的任意名字。

从仓库实现 attribute.rb 看,has_rich_text 会在模型上动态生成一组方法:

  • content——懒加载并返回(必要时构建)对应的 RichText 记录,例如 article.content.to_s
  • content?——判断是否存在非空的富文本正文(rich_text_content.present?);
  • content=——接收来自 Trix 的 HTML 字符串写入正文。

其底层通过一条多态 has_one 关联实现:has_one :rich_text_content, as: :record, inverse_of: :record, autosave: true, dependent: :destroy,并绑定 where(name: name) 过滤,使同一模型可持有多个不同名字的富文本字段。除了文档中常规用法,该方法签名还支持(默认值取自源码 attribute.rb):

  • encrypted: false——置为 true 时改用 ActionText::EncryptedRichText(非确定性加密,依赖 Active Record 加密能力);
  • strict_loading: strict_loading_by_default——是否强制 strict loading;
  • store_if_blank: true——置为 false 时,若写入空白值则不再为其创建 RichText 记录,而是标记销毁已有空记录。

在表单中使用 rich_textarea

为模型加上 has_rich_text 之后,就可以在视图中让该字段使用富文本编辑器(Trix)。做法是把表单字段声明为 rich_textarea

<%# app/views/articles/_form.html.erb %>
<%= form_with model: article do |form| %>
  <div class="field">
    <%= form.label :content %>
    <%= form.rich_textarea :content %>
  </div>
<% end %>

这会显示一个 Trix 编辑器,用于创建与更新富文本。rich_textarea 渲染出的是一对元素:一个真正的 <trix-editor> 可编辑区,加上一个隐藏的 <input>——Trix 在用户编辑时会把 HTML 写入该隐藏域,从而随表单一起正常提交。其实现位于 tag_helper.rbrich_textarea_tag / rich_textarea 默认会把编辑器容器带上 class="trix-content"(保证默认样式生效),并自动注入 data-direct-upload-url(默认 rails_direct_uploads_url)与 data-blob-url-template(默认 rails_service_blob_url(":signed_id", ":filename"))两个 data 属性,用于编辑器内的图片直传与预览;传入块时还可设置默认编辑内容。

编辑器样式的更新方式,稍后会在「移除或添加 Trix 样式」一节详述。

控制器参数白名单

最后,为了保证能接收来自编辑器的更新,需要在对应控制器中把该属性加入允许参数:

class ArticlesController < ApplicationController
  def create
    article = Article.create! params.expect(article: [:title, :content])
    redirect_to article
  end
end

重命名模型类时的数据同步

一旦有需要重命名使用 has_rich_text 的类(例如把 Article 改名),也必须同步更新 action_text_rich_texts 表中对应行的多态类型列 record_type。由于 Action Text 依赖多态关联,而多态关联会把类名存进数据库,保持库中数据与 Ruby 代码中的类名一致至关重要,否则既存的富文本将无法正确解析回模型。这一点在仓库源码 attribute.rb 的注释中也被明确提醒。

渲染富文本内容

ActionText::RichText 实例可以直接嵌入页面,因为其内容在保存/输出阶段已做过安全消毒:

<%= @article.content %>

这里实际触发的是 ActionText::RichText#to_s:它把富文本安全地转换为 HTML 字符串(经由 content.rbto_sto_rendered_html_with_layout 链路,最终套上默认布局 partial 输出)。在 rich_text.rb 中可以看到,RichText 通过 serialize :body, coder: ActionText::Contentbody 序列化为 ActionText::Content 对象,并委托 to_s 等行为给它;Content 内部用 Nokogiri 解析 HTML fragment 并做标准化处理。

与之相对,ActionText::RichText#to_plain_text 返回的是去掉标签但保留 HTML 实体编码的纯文本,该字符串不是 HTML safe 的,未经额外消毒不应直接在浏览器中渲染

message = Message.create!(content: "<h1>Funny times!</h1>")
message.content.to_s           # => "<h1>Funny times!</h1>"
message.content.to_plain_text  # => "Funny times!"

同文件还提供了 to_markdown(attachment_links: false),可把正文转换为 Markdown;在需要编辑态预览时还可用 to_editor_html(旧名 to_trix_html 已弃用)获得在编辑器中可回填的 HTML。

安全消毒

消毒发生在保存与渲染过程中。仓库中 engine.rb 通过 config.action_text.sanitizer_vendor 允许应用替换消毒器实现,默认使用 Action View 的 safe-list sanitizer(ActionText::ContentHelper.sanitizer),据此剥离 onclick<script> 之类的危险片段,这正是 to_s 结果可以放心直接输出的原因。

注意:如果 content 字段中存在附件资源,而本机尚未安装 Active Storage 所需的第三方软件依赖,附件可能无法正常显示。

定制富文本编辑器(Trix)

当需要按自己的风格要求调整编辑器的呈现时,可以按下面的方式定制。

移除或添加 Trix 样式

默认情况下,Action Text 会把富文本内容渲染进一个带 .trix-content 类的元素中,这一行为由 app/views/layouts/action_text/contents/_content.html.erb 决定(仓库内置的默认版本见 layouts/action_text/contents/_content.html.erb,仅一行 <div class="trix-content"><%= yield %></div>),带有该类的元素再由 trix 样式表进行样式化。

如果你想调整任何 trix 样式,可在 app/assets/stylesheets/actiontext.css 中添加自定义样式——这个文件由安装器生成,同时包含 Trix 的整套样式与 Action Text 所需的覆盖(override)。

定制内容容器

想要定制富文本内容外层包裹的 HTML 容器元素,编辑安装器生成的 app/views/layouts/action_text/contents/_content.html.erb 布局文件:

<%# app/views/layouts/action_text/contents/_content.html.erb %>
<div class="trix-content">
  <%= yield %>
</div>

定制嵌入图片与附件的 HTML

要定制嵌入图片及其他附件(即 blob)渲染出的 HTML,编辑安装器生成的 app/views/active_storage/blobs/_blob.html.erb 模板:

<%# 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>

仓库中该默认模板位于 actiontext/app/views/active_storage/blobs/_blob.html.erb。它演示了几个关键点:可通过 blob.representable? 区分"图片类可预览 blob"与"普通文件 blob",从而为 <figure> 施加不同的 attachment--preview / attachment--file 及按扩展名命名的 CSS 类;可预览的 blob 用 image_tag blob.representation(...) 生成自适应缩略图,图库场景(in_gallery 为真)下缩略上限更小(800×600);图注部分优先显示 blob 的 caption,否则展示文件名与人类可读的文件大小。

附件处理

目前 Action Text 支持两类附件:通过 Active Storage 上传的附件,以及通过 Signed GlobalID 关联的附件。

通过 Active Storage 上传附件

在富文本编辑器中上传图片时,动作由 Action Text 发起,底层则使用 Active Storage。不过 Active Storage 有若干第三方依赖 并不由 Rails 提供,要使用内置的预览(preview)能力,需要安装这些库。这些库并非全部必需,具体取决于你预期在编辑器中接收的上传类型。

用户在使用 Action Text 与 Active Storage 时最常遇到的一个问题是:图片在编辑器中无法正确渲染。这通常是因为系统没有安装 libvips 依赖。

附件直传的 JavaScript 事件

Action Text 在整个文件附件生命周期内都会派发 Active Storage 的 Direct Upload 事件。除常规的 event.detail 属性之外,Action Text 额外派发的事件还会携带 event.detail.attachment 属性(对应本次文件插入所创建的 Trix attachment)。

事件名 事件目标 事件数据(event.detail 描述
direct-upload:initialize <trix-editor> {id, file, attachment} 表单提交后对每个文件派发。
direct-upload:start <trix-editor> {id, file, attachment} 一次直传开始。
direct-upload:before-blob-request <trix-editor> {id, file, xhr, attachment} 在向应用请求直传元数据之前。
direct-upload:before-storage-request <trix-editor> {id, file, xhr, attachment} 在请求存储文件之前。
direct-upload:progress <trix-editor> {id, file, progress, attachment} 文件存储请求进行中。
direct-upload:error <trix-editor> {id, file, error, attachment} 发生错误。若不取消该事件,将弹出 alert 提示。
direct-upload:end <trix-editor> {id, file, attachment} 一次直传结束。

经 Action Text 通过 Active Storage 直传的文件有可能最终并未被嵌入任何富文本内容。建议定期清理这些"无主上传"(purging unattached uploads)。相关做法同样见 Active Storage Overview

通过 Signed GlobalID 关联附件

除上传到 Active Storage 的附件之外,Action Text 还可以嵌入任何能通过 Signed GlobalID 解析的对象

Global ID 是应用级的统一 URI,用于唯一标识一个模型实例,形如 gid://YourApp/Some::Model/id。当你需要用一个标识符去引用不同类型的对象时,它非常有用。

使用这种方式时,Action Text 要求附件具有签名全局 ID(sgid)。默认情况下,Rails 应用中的全部 Active Record 模型都混入了 GlobalID::Identification concern,因此它们都可以被 sgid 解析,从而天然兼容 ActionText::Attachable

Action Text 会在保存时记录你所插入的 HTML 引用,以便之后用最新的内容重新渲染——也就是说,你可以引用某个模型,并在记录变化后始终展示其当前内容。渲染时,Action Text 会先从 global ID 加载出模型,再用默认 partial 路径渲染它。

一个 Action Text Attachment 看起来是这样的:

<action-text-attachment sgid="BAh7CEkiCG…"></action-text-attachment>

Action Text 渲染内嵌的 <action-text-attachment> 元素时,会先解析其 sgid 属性得到对象实例,再把实例交给渲染 helper;渲染出的 HTML 作为 <action-text-attachment> 元素的后代嵌入。要让对象能作为 Attachment 渲染,需要 include ActionText::Attachable 模块,该模块通过 GlobalID::Identification 实现了 #to_sgid(**options)

class Person < ApplicationRecord
  include ActionText::Attachable
end

person = Person.create! name: "Javan"
html = %Q(<action-text-attachment sgid="#{person.attachable_sgid}"></action-text-attachment>)
content = ActionText::Content.new(html)
content.attachables # => [person]

从仓库实现 attachable.rb 看,attachable_sgid 会生成一个限定用途(purpose 为 "attachable")且不过期的 sgid:to_sgid(expires_in: nil, for: LOCATOR_NAME)LOCATOR_NAME = "attachable"。也就是说,这个签名 ID 只允许被 Action Text 的附件定位器使用,无法被用于其他业务目的,是一种安全上的隔离。同时,ActionText::Content#attachables 在解析时会依次尝试 sgid 解析、ContentAttachmentRemoteImage 三种来源,全部失败则返回一个 MissingAttachable 占位对象,为"记录已删除"的场景兜底。

渲染一个 Action Text Attachment

<action-text-attachment> 的默认渲染方式是默认路径 partial。下面以 User 模型为例:

# app/models/user.rb
class User < ApplicationRecord
  has_one_attached :avatar
end

user = User.find(1)
user.to_global_id.to_s #=> gid://MyRailsApp/User/1
user.to_signed_global_id.to_s #=> BAh7CEkiCG…

我们可以把 GlobalID::Identification 混入任何带 .find(id) 类方法的模型;Active Record 模型默认自带该能力。

上述代码得到唯一标识该模型实例的 ID。接着,看一段嵌入了引用 User 实例 sgid 的 <action-text-attachment> 的富文本:

<p>Hello, <action-text-attachment sgid="BAh7CEkiCG…"></action-text-attachment>.</p>

Action Text 用 "BAh7CEkiCG…" 解析出 User 实例,然后在渲染内容时按默认 partial 路径渲染它。此处的默认 partial 就是 users/user

<%# app/views/users/_user.html.erb %>
<span><%= image_tag user.avatar %> <%= user.name %></span>

于是 Action Text 渲染出的最终 HTML 大致如下:

<p>Hello, <action-text-attachment sgid="BAh7CEkiCG…"><span><img src="..."> Jane Doe</span></action-text-attachment>.</p>

为 action-text-attachment 渲染不同的 partial

如果想为某个可附件对象渲染不同的 partial,可以定义 to_attachable_partial_path(实例方法,其默认值是 to_partial_path,见 attachable.rb):

class User < ApplicationRecord
  def to_attachable_partial_path
    "users/attachable"
  end
end

然后声明该 partial,User 实例将作为 user 局部变量可用:

<%# app/views/users/_attachable.html.erb %>
<span><%= image_tag user.avatar %> <%= user.name %></span>

为无法解析或缺失的 action-text-attachment 渲染 partial

如果 Action Text 无法解析出 User 实例(例如记录已被删除),默认会渲染一个回退 partial。仓库中默认回退视图为 actiontext/app/views/action_text/attachables/_missing_attachable.html.erb,对应默认类方法 to_missing_attachable_partial_path(见 attachable.rb)。

想渲染不同的"缺失附件" partial,定义类级方法 to_missing_attachable_partial_path

class User < ApplicationRecord
  def self.to_missing_attachable_partial_path
    "users/missing_attachable"
  end
end

然后声明该 partial:

<%# app/views/users/missing_attachable.html.erb %>
<span>Deleted user</span>

通过 API 使用 Attachable

如果你的架构并不遵循传统的 Rails 服务端渲染模式,而是一个后端 API(例如返回 JSON),那么你需要一个独立的文件上传端点。该端点负责创建一个 ActiveStorage::Blob,并返回它的 attachable_sgid

{
  "attachable_sgid": "BAh7CEkiCG…"
}

之后在前端代码中把 attachable_sgid 放进 <action-text-attachment> 标签,即可把它插入富文本内容:

<action-text-attachment sgid="BAh7CEkiCG…"></action-text-attachment>

其他建议:避免 N+1 查询

如果你希望预加载关联的 ActionText::RichText 模型(假设富文本字段名为 content),可使用 has_rich_text 自动生成的具名 scope(仓库实现见 attribute.rbwith_all_rich_text 见同文件 rich_text_association_names 相关方法):

Article.all.with_rich_text_content # 仅预加载 body,不含附件。
Article.all.with_rich_text_content_and_embeds # 同时预加载 body 与附件(含附件需连表 includes embeds_attachments: :blob)。

若模型持有多个富文本字段,还可以用 Article.all.with_all_rich_text 一次性预加载所有 rich_text_* 关联。这类预加载能显著降低渲染列表页时的查询数量。

小结

Action Text 把富文本编辑(Trix)、结构化存储(action_text_rich_texts 多态表)与文件托管(Active Storage)三件事编排为开箱即用的一条龙方案:模型侧一条 has_rich_text 即完成接线,表单侧一个 rich_textarea 即获得完整 WYSIWYG 输入,输出侧通过消毒后的 to_s 即可安全渲染;对图片等附件,Active Storage 直传 + 事件钩子承担存储,Signed GlobalID + ActionText::Attachable 则打通"在正文中引用任意模型"的高级玩法。无论是追求快速落地的常规博客/内容场景,还是需要深度定制编辑器样式、自定义附件 partial、接入纯 API 前端的架构,都可以顺着上文各节的代码路径在仓库源码中继续深入钻研(入口可从 actiontextlib/action_textapp/models/action_textapp/helpers/action_textapp/views 各目录展开)。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391