Dawn 购物车:抽屉、加购通知与购物车页的更新链
本文基于 Dawn v15.3.0,源码固定到 ecb06c3(核验于 2026-09-29)。结论限于此提交,不表示其他版本行为相同;本篇为静态源码分析,未完成店铺运行验证。Dawn 上游已发布 v16.0.0(2026-08-10,GitHub Releases 核对),本库 Dawn 正文统一以 v15.3.0 为基线,升级前需重新核对。
对应哪些通用模式
覆盖 Drawer、Toast 与购物车摘要模块在 Dawn 上的实现。全局设置 cart_type(drawer / page / notification,默认 notification)决定加购后的反馈形态;三者共享 Section Rendering 与部分标记,不共享同一套组件。Horizon 的对应实现见 Horizon 购物车,两个主题的结论互不推断。
入口与文件
| 文件 | 职责 / 何时输出 |
|---|---|
| config/settings_schema.json | Cart 设置组:cart_type、show_vendor、show_cart_note、cart_drawer_collection、cart_color_scheme |
| layout/theme.liquid | cart_type == 'drawer' 时 render 'cart-drawer'、加载抽屉 CSS 与 cart-drawer.js;始终输出 window.routes、window.cartStrings |
| sections/header.liquid | 图标链接 #cart-icon-bubble;cart_type == "notification" 时 render 'cart-notification';始终加载 cart-notification.js |
| snippets/cart-drawer.liquid、snippets/cart-notification.liquid | 抽屉外壳(条目、备注、小计、结账、空车块);通知面板骨架 |
| sections/cart-drawer.liquid、sections/cart-notification-product.liquid | 无 schema,只作 Section Rendering 取回目标:前者一行 render 'cart-drawer',后者遍历整个购物车并给每行加上以 item.key 结尾的 id(前缀 cart-notification-product-);cart-notification-button、cart-icon-bubble、cart-live-region-text 同样无 schema |
| sections/main-cart-items.liquid、sections/main-cart-footer.liquid、templates/cart.json | 购物车页:cart-items 元素与页脚(区块 subtotal、buttons、@app);模板后接 featured-collection |
| assets/cart.js | CartItems(页面与抽屉共用基类)、CartRemoveButton、CartNote |
| assets/cart-drawer.js、assets/cart-notification.js、assets/product-form.js | 抽屉与通知的开合、内容替换;加购请求 |
数据从哪来
cart对象:cart.items、item.final_line_price、cart.total_price、cart.cart_level_discount_applications,金额经money输出,小计标为sections.cart.estimated_total;税费与运费说明按cart.taxes_included、cart.duties_included、shop.shipping_policy分支。- 数量规则:输入框的
data-min、max、step来自item.variant.quantity_rule。 - 端点:
window.routes提供cart_add_url、cart_change_url、cart_update_url、cart_url。加购与改量请求带sections与sections_url(当前路径),在同一响应里取回分区 HTML;备注只 POSTcart_update_url。 - 购物车页的分区标识来自元素
data-id,即模板区段 key(cart-items、cart-footer;按 JSON 模板区段 id 的约定推断,未运行核验)。
三种入口怎样分叉
| 维度 | page | drawer | notification |
|---|---|---|---|
| 加购后 | 没有 cart-* 元素,product-form 直接 window.location = routes.cart_url | renderContents 替换 #CartDrawer 与图标后 open() | 只取刚加入的一行(按响应 key),更新「查看购物车」文字与徽标后 open() |
| 请求的分区 | 无 | cart-drawer、cart-icon-bubble | cart-notification-product、cart-notification-button、cart-icon-bubble |
| 可改量 | 页内 | 抽屉内(cart-drawer-items 继承 CartItems) | 不可,需进购物车页 |
| 结账 | #checkout 加 content_for_additional_checkout_buttons(仅 main-cart-footer 使用) | #CartDrawer-Checkout,未找到动态结账按钮 | 面板内 form 提交 name="checkout" |
| 图标 | 普通链接 | 点击 preventDefault 开抽屉,补 role="button"、aria-haspopup="dialog" | 普通链接 |
product-form.js 用 document.querySelector('cart-notification') || document.querySelector('cart-drawer') 选出 this.cart,所以形态完全取决于哪个元素被输出。
交互怎样运行
加购到展示。 onSubmitHandler 把 this.cart.getSectionsToRender() 的 id 放进 FormData;成功后先 publish(cartUpdate),再 this.cart.renderContents(response),用 response.sections[id] 替换 DOM;快速加购弹窗内则等 modalClosed 后再渲染。
开合。 抽屉 open() 加 animate active,transitionend 后 trapFocus(空车时圈定 .drawer__inner-empty),body 加 overflow-hidden;Escape 走 keyup,遮罩点击关闭,close() 把焦点还给触发者。通知 open() 后 notification.focus() 并 trapFocus,监听 body 点击,点到面板外即关闭;cart-notification.js 没有定时器,不自动消失。
改量与移除(页面、抽屉共用)。 change 冒泡到 CartItems,防抖 300ms 后 validateQuantity:按 data-min、max、step 客户端校验,文案复用 quickOrderListStrings;通过后进入 updateQuantity:给列表加 cart__items--disabled、显示行内加载图标、让焦点元素失焦,POST cart_change_url,按 parsedState.sections 替换分区,再 publish(cartUpdate, { source: 'cart-items' })。移除同样是 updateQuantity(line, 0);页面版移除控件是 url_to_remove 链接,被 JS preventDefault 接管。
其他入口触发的刷新。 CartItems 订阅 cartUpdate,来源非 cart-items 时 onCartUpdate:抽屉请求 cart_url?section_id=cart-drawer 并替换 cart-drawer-items 与 .cart-drawer__footer;页面请求 ?section_id=main-cart-items。因此抽屉模式下一次加购既执行 renderContents,又由订阅者再请求一次分区(静态阅读,未抓包)。
空车分支。 初始由 cart == empty 决定:抽屉输出 drawer__inner-empty(提示、关闭、「继续购物」、未登录的登录提示,以及 cart_drawer_collection 的集合卡片)并给 cart-drawer 加 is-empty;页面给 cart-items 加 is-empty,CSS 隐藏条目与页脚。删除最后一件后,updateQuantity 按 item_count === 0 切换两处 is-empty 并整体替换分区,抽屉聚焦空状态里的第一个 a。
与通用契约的一致与差距
| 通用要求 | Dawn 现状 |
|---|---|
模态抽屉有 role="dialog"、aria-modal、名称 | 已做:.drawer__inner 带三者(名称为 aria-label);通知面板同样标为 dialog |
| 背景真正 inert | 未做:全仓库搜索 inert(唯一命中是变量名 containerToTrapFocusOn 的子串)、showModal、<dialog 均无使用,背景靠遮罩、overflow-hidden 与手写 Tab 循环 |
| 焦点进入、循环、Escape、返还 | 已做(trapFocus、keyup Escape、removeTrapFocus);改量后按 name 找回同一控件,删除后落到空状态链接或首个商品名 |
| 减少动态 | 未找到:抽屉与通知的 CSS、JS 中无 prefers-reduced-motion(检索 reduced-motion,其余命中与购物车无关) |
| RTL 用起始 / 结束侧 | 未见适配:抽屉固定 translateX(100%) 与 flex-end,未核验 |
| Toast 不夺焦点、用 status / alert | 相反:加购通知是 role="dialog",打开即移动焦点并圈定,「已加入」未写进 role="status" 区域 |
| Toast 可关闭、不因超时丢失 | 不自动消失;关闭方式有按钮、「继续购物」、Escape、点击外部 |
| 连续加购合并同类 | 未合并:只展示最新加入的一行,无累计数量 |
| 错误就近保留 | 加购被拒(响应含 status)时 description 写入按钮上方 role="alert" 区域,不经通知;改量失败写入该行的 role="alert" 区域 |
| 金额、数量规则来自平台 | 已做:金额来自 cart 与分区响应,cart.js、cart-drawer.js 无金额运算;规则输出自 quantity_rule,有规则或阶梯价时提供说明浮层 |
| 服务端夹紧后提示 | 已做:响应里的行数量与输入值不同且行数未变时,显示 cart_quantity_error_html |
| 折扣码、免运费进度 | 未找到:检索 discount_code、coupon、free_shipping、shipping_rates,只有行级与购物车级折扣项展示;progress-bar 片段属于快速订购加载条 |
| 缺货或失效行标识 | 未找到:条目模板仅在单位价处读 item.variant.available |
| 无 JavaScript | 头部图标是 /cart 真实链接,页面版移除是链接;抽屉与通知需脚本;表单提交结果由平台决定,未核验 |
读源码时发现的疑点
均为静态阅读,未在浏览器复现:
- 抽屉的数量输入框
id是Drawer-quantity-N,CartItems.resetQuantityInput只查#Quantity-N;客户端校验失败时setValidity会调用它,抽屉里预期找不到元素而报空引用。 - 抽屉备注文本域没有
form属性,靠cart-note防抖input事件保存;页面版有form="cart"。点击结账前是否已写入,属时序问题,未核验。 - 抽屉的
CartDrawer-LiveRegionText服务端输出为空,分区列表里没有为它供文字的分区(页面版靠cart-live-region-text填充);抽屉.totals带role="status",但位于被整体替换的.drawer__inner内,是否被朗读未核验。 - 加购的
.catch只console.error,无面向用户的提示。 - 相邻文件
quick-add-bulk.js的renderSections对CartDrawer元素不判空,并把空车判断写成items.length.length === 0;不在本文范围,仅提示回归。
定制入口与风险
商家可改 cart_type、供应商、备注开关、空抽屉的推荐集合与抽屉色板;购物车页的边距、配色在两个区段设置里,页脚区块为 subtotal、buttons、@app。耦合点:
#cart-icon-bubble的标记在header.liquid与sections/cart-icon-bubble.liquid各一份,需同步。getSectionsToRender()的id、selector与 Liquid 元素id(CartDrawer、cart-notification-product等)一一对应,重命名会让替换静默失效。- 抽屉与页面是两套条目标记(
CartDrawer-Item-N与CartItem-N,Drawer-quantity-N与Quantity-N),改条目内容要改两处。 - 新增第四种反馈形态,需要实现
getSectionsToRender、renderContents、setActiveElement。
建议的验证范围
本篇均未执行:
- 三种
cart_type下从商品页与快速加购加购,核对图标数量、面板内容、焦点与关闭后的焦点位置。 - 抽屉与页面里把数量改到规则之外,再改到超过库存,记录请求、响应、提示与输入值。
- 删除最后一件,检查空状态、焦点与两处
is-empty;空抽屉选定集合后核对卡片。 - 抓包确认抽屉模式下一次加购的请求数量与顺序。
- 键盘与读屏软件走完「打开、改量、删除、关闭」;开启系统减少动态观察动画;关闭 JavaScript 核对链接与结账入口。