EN
Shopify 知识库 · 指南

Dawn 怎样适配主题编辑器:设计模式、编辑器事件与重渲染清理

基于 Dawn v15.3.0 源码,梳理 request.design_mode 与 Shopify.designMode 的分工、theme-editor.js 对区块选中与分区加载卸载事件的处理、挪到 body 的弹窗如何清理、滚动动画在编辑器中的降级,以及重复加载脚本等问题。
历史资料
请结合文中的适用版本和来源阅读。

本文基于 Dawn v15.3.0,源码固定到 ecb06c3(核验于 2026-10-01)。结论限于此提交;本篇为静态源码分析,未在主题编辑器中实测。Dawn 上游已发布 v16.0.0,本库 Dawn 正文统一以 v15.3.0 为基线,升级前需重新核对。

商家修改设置时,主题编辑器会重新渲染受影响的分区,并在 document 上派发 shopify:section:load、shopify:block:select 等事件。主题如果只考虑店面,就会在编辑器里出现弹窗关不掉、轮播停在错误的一张、动画内容不显示等问题。Dawn 的适配代码不多,但覆盖了最常见的几类问题。

一、两个“设计模式”开关

开关所在层Dawn 的用法
request.design_modeLiquid只在编辑器中输出 theme-editor.js,店面访客不会下载它
Shopify.designModeJS给 <html> 加 shopify-design-mode 类(theme.liquid#L294-L298);滚动动画据此注册编辑器事件(animations.js#L99-L102)

shopify-design-mode 类让 CSS 也能区分编辑器环境,例如 component-product-model.css#L17 调整了编辑器中 AR 按钮的显示。

二、theme-editor.js 处理的事件

theme-editor.js 共 54 行,按事件分为三组:

选中轮播区块时跳到对应的一张。 shopify:block:select 的目标是 .slideshow__slide 时,暂停所在轮播,200ms 后滚动到该张;block:deselect 时,如果轮播原本在自动播放,就恢复播放(#L6-L26)。商家在侧栏点哪个区块,预览就显示哪一张,不会被自动播放切走。

任何编辑操作都先关闭商品媒体弹窗。 分区的加载、排序、选中、取消选中,以及检查器(inspector)的开关,都会调用 hideProductModal()(#L1-L4、#L46-L54),避免全屏弹窗挡住正在编辑的内容。

分区重渲染后补做初始化与清理。

  • section:load 时,把 id 以 EnableZoomOnHover 开头的脚本标签替换成新标签,让 magnify.js 重新执行(#L28-L37)。原因是 magnify.js 不是自定义元素:它在加载时调用一次 enableZoomOnHover(2),给当时页面上的图片绑定 onclick;重渲染出的新图片没有这个处理函数。
  • section:unload 时,删除所有 data-section 等于被卸载分区 ID 的元素,并移除 body 上的 overflow-hidden(#L39-L44)。

三、为什么需要手动清理

ModalDialog 在第一次连接到文档时,把自己移到 <body> 末尾,以避开父容器的 overflow 和层叠上下文;移动前先把所在分区的 ID 记到 data-section,并用 moved 标记防止移动后再次触发:

connectedCallback() {
  if (this.moved) return;
  this.moved = true;
  this.dataset.section = this.closest('.shopify-section').id.replace('shopify-section-', '');
  document.body.appendChild(this);
}

编辑器以分区容器 #shopify-section-<id> 为单位重渲染或删除内容,已经搬到 <body> 下的弹窗不在容器内,会留在页面上,下一次渲染又会搬来一份。section:unload 里按 data-section 删除,就是为这类“搬家”的元素收尾。

同理,弹窗打开时给 body 加了 overflow-hidden。如果弹窗所在的分区在打开状态下被卸载,关闭逻辑不会再执行,页面就无法滚动,因此卸载时一并移除这个类。

除这些情况外,大部分组件不需要专门处理编辑器:订阅写在 connectedCallback、退订写在 disconnectedCallback,重渲染时会自然地退订旧实例、初始化新实例(见组件组织)。

四、滚动动画在编辑器中直接显示

滚动显现动画依赖 IntersectionObserver:元素进入视口前是透明的。编辑器重渲染分区时,如果再走一次观察流程,商家刚改完的内容可能要滚动一下才出现。Dawn 在 section:load 和 section:reorder 时,给分区内的 .scroll-trigger 加上 scroll-trigger--design-mode,跳过观察器(animations.js#L24-L33);CSS 中,这个类把透明度、位移和动画全部重置(base.css#L3295-L3307)。

公告栏另有一处只在编辑器中输出的样式:隐藏 aria-hidden="true" 的轮播项,注释为 theme editor power preview fix(announcement-bar.liquid#L129-L136)。

读源码时发现的问题

均为静态阅读,未在编辑器中复现:

  • 同一个脚本可能执行多次。 theme-editor.js 由 4 个分区分别输出:商品主分区、精选商品、轮播与公告栏(main-product.liquid#L52-L54、slideshow.liquid#L260-L262 等)。普通 <script src> 每出现一次就执行一次,所有监听器会按页面上这类分区的数量重复注册。目前的处理函数大多可以重复执行,但同一页面上有多个这类分区时,选中一个轮播区块会触发多次 pause() 与滚动。
  • 只重新执行第一个放大脚本。 querySelector('[id^=EnableZoomOnHover]') 只取第一个匹配项;商品页与精选商品同时启用悬停放大时,只替换其中一个。不过 enableZoomOnHover 本身作用于全页图片,实际影响可能不大。
  • 任何分区加载都会重跑放大脚本。 section:load 没有检查是哪个分区,修改页脚也会重新执行 magnify.js。
  • section:unload 不检查元素是否已被移出。 data-section 也出现在分区内部的元素上(如 product-info、quantity-input),这些元素本来就会随分区移除,重复删除没有副作用,但说明这个属性同时承担了两种用途。

迁移到自己主题时

  1. 编辑器专用脚本只在 request.design_mode 时输出,并且只在布局里输出一次,不要分散到各个分区。
  2. 把元素移出分区之前,记录它的来源分区,并在 shopify:section:unload 中清理;能用原生 <dialog> 的 showModal() 时,就不需要移动元素。
  3. 非自定义元素的初始化逻辑,改写成自定义元素,或者在 shopify:section:load 中只对 event.target 内的元素重新初始化。
  4. 动画、懒加载等依赖视口的行为,在编辑器中直接给出最终状态。

建议的验证范围

本篇均未执行:在编辑器中依次选中轮播的各个区块,确认预览跳转与自动播放的恢复;打开商品媒体弹窗后切换到另一个分区;删除含弹窗的分区,检查 <body> 下是否还有残留的 modal-dialog;首页同时放置轮播与公告栏,统计 shopify:block:select 监听器的数量。

源码基线