Spree 6.0 富文本读写契约:HTML 存入纯文本列,field 与 field_html 双读模型 📅 发布时间:2026/9/14 12:27:30 👁 浏览次数: Spree 6.0 富文本读写契约HTML 存入纯文本列field 与 field_html 双读模型【免费下载链接】spreeOpen Source eCommerce Platform for B2B, Marketplace, and Enterprise. REST API, TypeScript SDK, and production-ready Next.js storefront. Self-host it. Own your stack. No vendor lock-in. Zero platform fees.项目地址: https://gitcode.com/GitHub_Trending/sp/spree本篇围绕 Spree 6.0 富文本字段的读写契约展开写入参数保持不变description、internal_note等的值本身就是 HTML而读取侧统一为纯文本字段 同名_html字段的成对输出。读完你会掌握 Spree 6.0 中富文本的存储模型、序列化契约、服务端净化机制以及客户端编辑器水合、SDK 集成应当如何正确地读取与写入富文本数据。一、核心变更写契约不变读契约统一本次变更changeset 声明影响spree/admin-sdk与spree/cli两个包级别为 patch的关键事实可以浓缩为一句话富文本字段以纯文本 HTML 两种形态可读。Spree 6.0 将富文本以净化后的 HTML存储在纯文本列plain text column中取代了此前基于 ActionText 的存储方式。写入侧的参数完全未变description、internal_note等参数仍然接收原始值且该值就是 HTML不存在description_html或internal_note_html这样的写入参数——既然值在哪个名字下到达都同样是 HTML第二个 setter 只是同一次写入的第二种写法。真正改变的是读取侧internal_note_html现在可以在Order上读取同时Customer上也可读取纯文本internal_note。此前 Order 序列化器只返回纯文本、Customer 序列化器只返回 HTML如今两者都返回这对字段description返回去除标签后的纯文本原始标记则位于description_html之下。编辑器水合hydrate时应从description_html取值而不是description。由此引出一个高频踩坑点changeset 明确强调字段存的是 HTML所以请发送标记markup——一个带换行的纯文本值渲染出来会变成一整行连在一起的文字。二、存储模型为什么是文本列 净化 HTML要理解这条契约的由来需要先理解 6.0 的存储重构。6.0 之前不同模型的富文本存储方式并不统一Product 的description是未净化的 text 列而 Taxon后更名 Category、Policy、Collection 的 description 以及 Order/Customer 的内部备注走的是 ActionText内部备注的两个序列化器还存在约定相反的问题Order 只给纯文本、Customer 只给 HTML。6.0 的决策是HTML 存入模型自身表的 text 列。Tiptap 编辑器通过editor.getHTML()产出 HTMLHTML 是店铺前端、邮件、搜索索引、feed、移动端都能消费的通用格式管理端是唯一写入方ActionText 从核心中彻底移除代码在 6.0 移除action_text_rich_texts数据表保留到 6.1 作为数据迁移来源与回滚路径。ActionText 本质是 Trix 的配套设施其允许列表会剥离合法的 Tiptap 输出还会把内容包进div classtrix-content并引入action-text-attachment标签——对 Tiptap 编辑器而言这是静默数据损坏可翻译字段通过 Mobility 的table 后端翻译RICH_TEXT_TRANSLATABLE_FIELDS常量驱动 SPA 翻译矩阵中的富文本编辑器。该决策与实现细节的完整记录见 富文本描述规划文档升级路径见 5.6→6.0 升级指南。三、读契约fieldfield_html成对输出统一后的读取形态覆盖以下字段模型纯文本读字段HTML 读字段Product、Category、Collectiondescriptiondescription_htmlOrder、Customerinternal_noteinternal_note_html从序列化器源码可以直接验证这对契约的落地方式。以 Order 为例order_serializer.rbattribute :internal_note do |order| Spree::RichTextHelper.to_plain_text(order.internal_note).presence end attribute :internal_note_html do |order| order.internal_note.presence endCustomer序列化器customer_serializer.rb采用了完全一致的写法Seller 侧的 Order 序列化器seller/order_serializer.rb同样暴露这对字段。注意一个细节列里存的就是 HTML因此 HTML 字段直接取原值presence只是把空串归一为nil而纯文本字段则由Spree::RichTextHelper.to_plain_text现算得出。纯文本提取为什么不能简单去标签Tiptap 序列化块级元素时块与块之间没有空白pa/ppb/pnaive 地剥离标签会把相邻段落粘成一行ab丢失全部段落与换行。因此 RichTextHelper 的策略是先把块边界/p、/li、/h1–/h6、/blockquote等闭合块标签和br映射为换行符再剥离标签最后折叠多余空白、修剪换行两侧空格、把连续空行压到两行以内。这样得到的纯文本可直接用于店铺前端、meta 标签、搜索索引与 feed 等场景。四、写路径has_spree_rich_text与保存时净化模型侧的契约由 SanitizableRichText concern 实现。以 Product 为例product.rbclass Spree::Product Spree.base_class has_spree_rich_text :description # 翻译写在 Mobility 翻译表上会绕过基础记录的回调 # 因此 Translation 类还需要单独声明一次仅净化不提供 reader self::Translation.class_eval do include Spree::SanitizableRichText sanitizes_rich_text :description end endhas_spree_rich_text :description做两件事sanitizes_rich_text安装一个before_save回调对每个被声明的属性仅在属性确实发生变化时will_save_change_to_attribute?调用Spree::RichTextSanitizer.sanitize重写取值。只碰改动过的字段保证无关保存绝不触碰已存内容rich_text_html_reader为每个属性定义field_html读方法即description_html其实现是public_send(attribute).to_s——特意走属性自身的 reader 而非self[]这样翻译字段仍会经过 Mobility、按当前激活 locale 解析。关键约束源码注释与规划文档一致地强调field_html是只读 reader任何模型上都不存在field_html写入器。该写法在实现期间曾短暂上线后因值在哪个名字下到达都同样是 HTML第二个 setter 只会用读字段的对称性为代价破坏所有既有客户端而回退Mobility 的翻译写入会绕过父记录的回调所以翻译字段必须在Translation类上再声明一次sanitizes_rich_text而 reader 仍属于父类绕过回调的写入update_columns、update_all、裸 SQL不在覆盖范围内规划文档注明这些路径需要显式调用Spree::RichTextSanitizer.sanitizespree_rich_text_attributes类属性用并集而非赋值合并装饰器声明新字段时不会丢掉类已声明的字段SanitizableRichText.declaring_models则通过遍历Spree.base_class的子类发现所有声明了富文本属性的模型而非注册表避免 Rails 惰性加载下注册表为空的陷阱。Orderorder.rb与 Customercustomer_methods.rb均以has_spree_rich_text :internal_note接入同一机制这正是 changeset 中internal_note_html现在可在 Order 上读取的底层依据。五、净化器允许列表严格限定为编辑器实际输出RichTextSanitizer 是在写入时净化、允许列表限定于编辑器实际产出原则的落地核心实现# 默认允许标签Tiptap StarterKit段落、标题、硬换行、分割线、 # 加粗/斜体/删除线/下划线、行内/块级代码、引用、列表 Link Image class_attribute :allowed_tags, default: %w[ p br hr h1 h2 h3 h4 h5 h6 strong em s u code pre blockquote ul ol li a img ].freeze # 来自 Linkhref/target/rel/title来自 Imagesrc/alt/width/height # class 仅因代码块需要 language-* 提示而放行 class_attribute :allowed_attributes, default: %w[href target rel title class src alt width height].freeze class_attribute :allowed_class_pattern, default: /\Alanguage-[a-z0-9#-]\z/i class_attribute :classable_tags, default: %w[code].freeze class_attribute :pruned_tags, default: %w[script style].freezeself.sanitize(html)的工作方式值得注意两个 scrubber 作用于同一个解析后的 fragmentLoofah.html5_fragment。注释明确解释了原因走两遍SafeListSanitizer一次剪枝、一次允许列表会在每次保存时对整个文档做两次解析与重新序列化先跑prune_scrubber自定义Loofah::Scrubber把script/style元素连子树整体删除——只剥标签会让脚本体和样式表作为可见文本残留下来同时把class收窄到code元素上的language-*语言提示Tiptap 通过languageClassPrefix发出的唯一 class再跑permit_scrubberRails::HTML::PermitScrubber按允许标签与允许属性做白名单过滤。src无需单独的协议白名单——permit scrubber 本身会丢弃携带不安全 scheme 的属性值data:与javascript:URI 被剥离https 与相对 URL 存活。几个不做什么的决策同样重要完全没有style属性内联样式是 CSS 覆盖层攻击面且 Spree 中没有任何写入方产出它刻意不使用prune: true的全局剪枝那会把所有不允许标签的内容一并剥掉iframekeepme/iframe会变成空串而不是keepme静默摧毁 legacy TinyMCE 内容。剪枝只作用于script/style这两个内容绝非正文的标签其余被剥离的标签保留文本——这限制了收紧允许列表对 legacy 标记的破坏范围扩宽允许列表是刻意行为而非默认编辑器新增节点类型时在同一个变更里同步扩宽允许列表。legacy 内容中超出集合的标记表格、图片、div/span会在下次保存时被解包——文本存活格式不保。确有更丰富 legacy 内容如 TinyMCE 时代的表格、视频 iframe的商家可以在 initializer 中重新放行且因为内容会在下次保存时被重新净化建议在做 5.6→6.0 迁移之前应用Spree::RichTextSanitizer.allowed_tags %w[table thead tbody tr th td] Spree::RichTextSanitizer.allowed_attributes %w[colspan rowspan]六、客户端集成编辑器水合与发送标记对集成方而言这条契约的落地规则非常具体读取/水合把富文本编辑器初始化为description_html或internal_note_html的值description/internal_note是去标签的纯文本只适合展示、搜索、meta 等文本场景。spree/admin-sdk生成的类型如 Product、Order、Customer均已包含这对字段dashboard 侧的写参数映射见 params.ts写入继续用description、internal_note、body等普通名字参数值是 Tiptap 的editor.getHTML()输出。服务端before_save负责净化客户端无需预净化不要把纯文本塞进富文本字段如 changeset 警告带换行的纯文本会渲染成一行连排的文字。富文本字段里请发送标记。API 规范层面Admin 与 Seller 的 OpenAPI 定义admin.yaml、seller.yaml中这两个字段均声明为可空字符串成对出现与序列化器实现一致。七、升级路径与适用前提对于从 5.6 升级到 6.0 的部署富文本迁移由升级清单中的migrate_rich_text_to_columns步骤完成对应 rake 任务bundle exec rake spree:migrate_rich_text_to_columns把 Category/Collection 的 description、Policy 的 body、Order/Customer 的内部备注从action_text_rich_texts拷入各自表上的文本列按 locale 落入模型的翻译表。升级指南中的关键注意事项拷贝读取的是原始body列而非body.to_s——ActionText 渲染器会把输出包进div classtrix-content走渲染器拷贝会把这个包装迁移进每一列而允许列表会剥掉class留下一个裸divCustomer 备注的拷贝必须在migrate_users_to_customers之后执行——该步骤会重排action_text_rich_texts的记录类型且spree_customers在此之前是空的顺序颠倒会让所有 legacy 备注被当作孤儿永久跳过迁移完成后 Spree 核心不再加载 ActionTextspree_core移除了require action_text/engine全新安装不再创建这些表。若你自己的代码仍在使用has_rich_text或 ActionText 视图辅助需在config/application.rb中自行 require由 Spree starter 生成的应用已经做了。适用前提与边界该契约适用于 Spree 6.0 核心 APIAdmin/Seller 侧的 Order、Customer 内部备注与 Product/Category/Collection 的 description写入参数与 5.6 完全一致因此写侧无集成破坏变化集中在读侧新增字段与序列化器输出形态净化发生在保存时写入时净化列中存的即是干净值绕过回调的裸写路径不在保护范围内action_text_rich_texts表保留到 6.1 作为回滚路径6.1 才随其他 legacy 表一并删除——在 6.1 之前不应手动删除这些表。八、小结Spree 6.0 的富文本契约可以概括为三句话存 HTML净化后写用普通字段名读用纯文本 _html成对字段。写入契约对既有客户端零破坏读取契约则消除了 Order 与 Customer 内部备注的约定分歧并以 SanitizableRichText RichTextSanitizer RichTextHelper 三个核心件保证了每次保存即净化、每次读取形态一致的确定性行为。集成时的唯一硬性纪律是编辑器从_html字段水合写入时发送真正的 HTML 标记。【免费下载链接】spreeOpen Source eCommerce Platform for B2B, Marketplace, and Enterprise. REST API, TypeScript SDK, and production-ready Next.js storefront. Self-host it. Own your stack. No vendor lock-in. Zero platform fees.项目地址: https://gitcode.com/GitHub_Trending/sp/spree创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考