news 2026/9/8 23:42:40

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Rails Action Text 全面指南:富文本编辑、附件管理与安全渲染

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

【免费下载链接】railsRuby on Rails项目地址: https://gitcode.com/GitHub_Trending/rai/rails

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 的contenteditableexecCommandAPI 的封装。这两个 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.rb 中config.action_text.editorsconfig.action_text.editor配置)。
  • 添加image_processinggem:用于对嵌入图片及其他附件执行 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.rb:rich_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.rb 的to_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::Identificationconcern,因此它们都可以被 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.rb,with_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 前端的架构,都可以顺着上文各节的代码路径在仓库源码中继续深入钻研(入口可从 actiontext 的lib/action_textapp/models/action_textapp/helpers/action_textapp/views各目录展开)。

【免费下载链接】railsRuby on Rails项目地址: https://gitcode.com/GitHub_Trending/rai/rails

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 23:42:10

基于深度学习与OCR的银行卡号识别技术解析

简介&#xff1a;一套基于深度学习的银行卡号识别项目&#xff0c;以卷积神经网络&#xff08;CNN&#xff09;和TensorFlow为核心&#xff0c;配图形界面&#xff0c;面向金融科技开发者、高校学生及计算机视觉入门者&#xff0c;解决银行卡号自动定位与识别的实操训练问题。压…

作者头像 李华
网站建设 2026/9/8 23:39:22

黑盒通信协议逆向实战:从物理层波形到单片机插桩解析

1. 整体思路拆解&#xff1a;黑盒逆向不是玄学&#xff0c;是一套方法论 我做了这么多年嵌入式开发&#xff0c;接到过不少“只有一块板子&#xff0c;没有原理图、没有协议文档、没有固件源码”的项目。说白了就是纯黑盒逆向。以前带新人的时候&#xff0c;我经常跟他们讲一句…

作者头像 李华
网站建设 2026/9/8 23:33:20

STM32F103 AB双分区OTA升级方案详解:从Bootloader到App完整实现

前阵子有个客户现场的设备需要修一个逻辑bug&#xff0c;设备装在十几公里外的农田排灌站里&#xff0c;跑一趟光高速费就够喝一壶&#xff0c;从那时起我意识到&#xff1a; OTA升级 是嵌入式产品绕不开的必修课。于是我用最经典的 STM32F103 做了一套完整的 AB 双分区 …

作者头像 李华
网站建设 2026/9/8 23:31:33

Type II补偿网络调参困局:穿越频率与相位裕量为何联动?

网络分析仪探头刚夹上去&#xff0c;拧了一圈R2&#xff0c;屏幕上那两个数字——穿越频率和相位裕量——像商量好似的&#xff0c;一起开始漂移。这是几乎所有调过环路补偿的人都会撞上的场面。前几篇我把功率级传递函数、Type I积分补偿的基本功都铺垫完了&#xff0c;这一篇…

作者头像 李华