Dawn 怎样适配主题编辑器:设计模式、编辑器事件与重渲染清理
本文基于 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_mode | Liquid | 只在编辑器中输出 theme-editor.js,店面访客不会下载它 |
Shopify.designMode | JS | 给 <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),这些元素本来就会随分区移除,重复删除没有副作用,但说明这个属性同时承担了两种用途。
迁移到自己主题时
- 编辑器专用脚本只在
request.design_mode时输出,并且只在布局里输出一次,不要分散到各个分区。 - 把元素移出分区之前,记录它的来源分区,并在
shopify:section:unload中清理;能用原生<dialog>的showModal()时,就不需要移动元素。 - 非自定义元素的初始化逻辑,改写成自定义元素,或者在
shopify:section:load中只对event.target内的元素重新初始化。 - 动画、懒加载等依赖视口的行为,在编辑器中直接给出最终状态。
建议的验证范围
本篇均未执行:在编辑器中依次选中轮播的各个区块,确认预览跳转与自动播放的恢复;打开商品媒体弹窗后切换到另一个分区;删除含弹窗的分区,检查 <body> 下是否还有残留的 modal-dialog;首页同时放置轮播与公告栏,统计 shopify:block:select 监听器的数量。