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 的
contenteditable与execCommandAPI 的封装。这两个 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 包,并自动把它们import进application.js(若项目使用 importmap,则会在config/importmap.rb中追加pin声明)。仓库中生成器还支持--editor选项(默认"trix"),Action Text 已做成可插拔的编辑器体系(见 engine.rb 中config.action_text.editors与config.action_text.editor配置)。 - 添加
image_processinggem:用于对嵌入图片及其他附件执行 Active Storage 的分析与变换。更多细节参见 Active Storage Overview。 - 添加迁移:创建存储富文本与附件的表——
action_text_rich_texts、active_storage_blobs、active_storage_attachments、active_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:migrateaction_text_rich_texts表与多态关联
当 Action Text 安装创建action_text_rich_texts表时,它使用了多态关联(polymorphic association),以便多个模型都能添加富文本属性。表结构中的record_type与record_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_s→to_rendered_html_with_layout链路,最终套上默认布局 partial 输出)。在 rich_text.rb 中可以看到,RichText通过serialize :body, coder: ActionText::Content把body序列化为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 解析、ContentAttachment、RemoteImage三种来源,全部失败则返回一个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_text、app/models/action_text、app/helpers/action_text与app/views各目录展开)。
【免费下载链接】railsRuby on Rails项目地址: https://gitcode.com/GitHub_Trending/rai/rails
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考