EN
Shopify 知识库 · 指南

Dawn 的 Section Rendering 用法:前端拿 HTML,不拿 JSON

基于 Dawn v15.3.0 源码,横向对比变体切换、商品推荐、筛选、预测搜索与购物车六处局部刷新,拆解 section_id 与 sections 两种取法、getSectionsToRender 约定和它带来的重复请求、缓存与乱序代价。
历史资料
请结合文中的适用版本和来源阅读。

本文基于 Dawn v15.3.0,源码固定到 ecb06c3(核验于 2026-10-01)。结论限于此提交;本篇为静态源码分析,未完成店铺运行验证。Dawn 上游已发布 v16.0.0,本库 Dawn 正文统一以 v15.3.0 为基线,升级前需重新核对。

Dawn 几乎所有异步交互都遵循同一个思路:数据变化后,让服务端重新渲染受影响的分区,前端只把返回的 HTML 换进页面。浏览器端没有模板引擎,也不在 JS 里格式化金额或拼接文案。本文不重复各功能的完整流程(见文末链接),只横向比较这套做法在六处的写法差异、约定与代价。

两种取法

取法请求形态Dawn 中的使用者
GET 单分区页面 URL 加 ?section_id=<分区>,响应是该分区的 HTML变体切换 buildRequestUrlWithParams、数量规则 fetchQuantityRules、商品推荐 loadRecommendations、筛选 renderPage、预测搜索 getSearchResults、购物车被动刷新 onCartUpdate
写操作附带分区向 /cart/add、/cart/change 提交时附加 sections 与 sections_url,JSON 响应的 sections[<分区>] 是 HTML 字符串加购 onSubmitHandler、改量与删除 updateQuantity

第二种是 Dawn 最值得借鉴的一处:数据写入和界面刷新合并成一次请求。加购后不必再请求一次抽屉或通知面板,响应里已经带着渲染好的片段。sections_url 传的是 window.location.pathname,让服务端按当前页面的上下文渲染这些分区。

分区标识有两种来源。模板内分区用元素上的 data-id(如 document.getElementById('main-cart-items').dataset.id、筛选的 #product-grid 的 data-id),即 JSON 模板里的区段 key;cart-drawer、cart-icon-bubble、cart-notification-product 这类没有 schema 的独立分区文件,直接写文件名。

getSectionsToRender 约定

谁需要刷新,由各个购物车组件自己声明;发请求的一方(product-form)不关心具体是哪些分区:

组件返回值字段含义
CartItems(购物车页){ id, section, selector } ×4section 是请求键,id 是 DOM 挂载点,selector 从响应中截取片段
CartDrawerItems同上 ×2同上
CartDrawer{ id, selector? } ×2id 同时作为请求键和挂载点
CartNotification{ id, selector? } ×3同上;selector 依赖响应返回的 key

两套字段语义并存。product-form 取的是 section.id(product-form.js#L38),它只会拿到 cart-notification 或 cart-drawer(product-form.js#L11),这两者的 id 恰好就是分区名,所以能正常工作;CartItems 自己发请求时取的是 section.section。新增一种购物车组件时,必须先确认它会被哪一方调用,再决定 id 的含义。

CartNotification 的写法很巧:发请求时只需要 id;selector 里的 cartItemKey 在 renderContents 拿到响应后才赋值,因此同一个方法在请求时和渲染时返回的选择器不同。通知面板借此只截取刚加入的那一行,而服务端分区 cart-notification-product 渲染的是整个购物车。

为什么值得这样做

  • 只有一份模板。价格格式、多币种、翻译、折扣行、数量规则说明都留在 Liquid 里,JS 中没有金额运算。改展示只需改 Liquid,异步刷新和首屏渲染自动保持一致。
  • 前端几乎不需要了解数据模型。变体切换不必下发变体矩阵,筛选不必知道有哪些筛选项,服务端返回什么就展示什么(筛选的 renderFilters 会删除响应里已不存在的筛选组)。
  • 商家可配置项自动生效。区块顺序、开关和文案都在服务端渲染时应用,JS 不需要读取 section 设置。

代价与 Dawn 留下的坑

问题位置说明(均为静态阅读推断)
重复请求product-form 发布 cartUpdate 后,CartDrawerItems 的订阅者再请求一次 ?section_id=cart-drawer(cart.js#L30-L37)抽屉模式下,加购响应已带抽屉 HTML,订阅者又取一遍;详见购物车篇
乱序覆盖筛选 renderSectionFromFetch 没有 AbortController;预测搜索只在重置时中止先发出、后返回的旧响应会覆盖新结果;变体切换在 product-info.js#L116-L118 做了中止,是三者中唯一处理了竞态的
缓存无上限筛选 FacetFiltersForm.filterData 以完整 URL 为键(facets.js#L49-L56);预测搜索 cachedResults 在所有实例间同步一直存到页面卸载,不过期,不受加购等写操作影响
重复解析购物车三个组件的 getSectionInnerHTML 对每个分区各调用一次 DOMParser分区少时可以忽略;renderFilters、renderProductGridContainer、renderProductCount 对同一份响应各解析一次
错误静默筛选的 fetch 没有 catch;推荐只 console.error失败时加载状态可能不复位,用户看不到提示
意外的参数facets.js#L70 把字符串的 .innerHTML(undefined)传给 initializeScrollAnimationTrigger参数退化为默认值 document,结果是重新扫描整页,碰巧可用

迁移到自己主题时

  1. 写操作一律带上 sections,把需要刷新的分区交给组件自己声明;统一 { section, mount, selector } 三个字段的语义,不要沿用 Dawn 的两套含义。
  2. 所有 GET 刷新都加上中止逻辑(或给响应加序号校验),尤其是由输入事件触发的。
  3. 缓存需要设上限,并在购物车或库存变化时作废。
  4. 分区 HTML 里的元素 id 是 JS 与 Liquid 之间的契约;重命名时需要两侧一起搜索(参见局部替换工具)。

建议的验证范围

本篇均未执行:抓包核对抽屉、通知、页面三种 cart_type 下一次加购的请求数;限速下连续勾选筛选、连续输入搜索词,观察乱序;在 Performance 面板记录 DOMParser 耗时与分区 HTML 体积。

源码基线