Shopify 知识库 · 指南
Horizon 筛选、排序与分页:分区刷新、URL 状态与无限滚动
基于 Horizon 4.2.0 源码,拆解 Shopify 集合与搜索结果页的筛选表单、URL 与分区刷新、已选条件移除、移动端抽屉、排序,以及默认无限滚动与普通分页的差别,并对照通用模块列出差距。
历史资料
请结合文中的适用版本和来源阅读。
本文基于 Horizon 4.2.0,源码固定到 5acd1b6(核验于 2026-10-07)。结论限于此提交,不表示其他版本行为相同;本篇为静态源码分析,未完成店铺运行验证。
集合与搜索划分了 facets.js、paginated-list.js、results-list.js 的职责,但未展开请求链、无限滚动与状态差距,本篇补上。
对应哪些通用模式
- 模块:集合浏览与筛选的筛选、排序、翻页与结果计数。
- 展现:Pagination 的分页与无限滚动(本主题两种都有,默认无限滚动);移动端筛选用
theme-drawer,对应 Drawer。搜索结果页复用同一套,预测搜索见预测搜索。
入口与文件
| 文件 | 职责 | 何时输出 |
|---|---|---|
| sections/main-collection.liquid | 集合页:results-list、paginate、filters 块、商品卡 | 集合模板 |
| sections/search-results.liquid | 搜索页:仅 search.results_count > 0 时输出 filters,否则改用备用集合商品 | 搜索模板 |
| blocks/filters.liquid | 桌面栏、移动条与 theme-drawer#filters-drawer 三套外壳,各含 facets-form-component,另有 role="status" 计数 | 上述两页 |
| snippets/list-filter.liquid / snippets/price-filter.liquid / snippets/sorting.liquid | 列表与色板筛选、价格区间、排序 | 由 filters 渲染 |
| snippets/filter-remove-buttons.liquid | 已选条件标签与「全部清除」 | 有筛选时 |
| assets/facets.js | 表单转 URL、pushState、分区刷新、预取、清除与移除 | 脚本加载后 |
| assets/paginated-list.js、assets/results-list.js | 无限滚动、前后页预取与追加、网格密度切换 | 无限滚动或布局切换时 |
| snippets/product-grid.liquid、snippets/pagination-controls.liquid | 网格、空结果、无限滚动哨兵或页码导航 | 仅无限滚动关闭时输出页码 |
数据从哪来
- 筛选定义:Liquid
results.filters,类型price_range、boolean与列表;字段用到param_name、values[].active、count、url_to_remove、swatch、image、min_value、max_value、range_max。输入名与移除链接由 Liquid 给出,主题不自己拼filter.*参数名;表单action为results_url。 - 排序与计数:
results.sort_options,当前值sort_by | default: default_sort_by;计数取collection.products_count或search.results_count,超过 25000 显示item_count_cutoff。 - 设置:
filters块enable_filtering(schema 默认 false,templates/collection.json与search.json设为 true)、filter_style(horizontal 或 vertical,两个模板均为 horizontal)、enable_sorting、enable_grid_density。分区设置enable_infinite_scroll默认 true,为 true 时每页固定 24;关闭后products_per_page(8–36,步长 4)生效。
交互怎样运行
- 触发:勾选、价格
change、排序与移除都进入FacetsFormComponent。createURLParameters取FormData,删掉空价格,删除page,搜索页补回q,再history.pushState,每次变更一条历史记录。 - 刷新:
sectionRenderer.renderSection(sectionId)以当前 URL 加section_id请求并整段 morph;同一分区未完成的上一次渲染会被 abort(此处传了signal)。在对话框外用startViewTransition(对话框内直接渲染)。默认cache = !Shopify.designMode,同 URL 命中内存缓存,页面 load 时也会把现有分区写入缓存;复选框与色板选项在pointerenter(防抖 200ms)或pointerdown时预取预测 URL。 - 事件:请求发起后、渲染完成前分发
SearchUpdateEvent/CollectionUpdateEvent,清除按钮据此切换状态;PaginatedList收到后清空页缓存并等data-last-page变化。 - 移除:标签是带
data-url="url_to_remove"的facet-remove-component,走updateFiltersByURL(同样pushState加刷新)。 - 移动端:
theme-drawer在 990px 以下showModal(),以上show()加trapFocus;打开后焦点移到关闭按钮,Esc 关闭,关闭后回到触发元素。抽屉底部「查看 N 件商品」只关闭抽屉。桌面 horizontal 的筛选面板是details加floating-panel-component,未用原生 Popover API。 - 无限滚动:
viewMorePrevious/Next哨兵被IntersectionObserver(rootMargin: 100px)命中,追加预取的?page=N卡片,并对每个新页pushState;点击商品卡时replaceState写入#卡片id与page,返回后由main-collection.liquid的javascript标签脚本滚动到该卡片;search-results.liquid中未读到等价脚本,搜索页返回后的位置恢复未核验。
与通用契约的一致与差距
对照集合浏览与筛选与 Pagination。
| 契约 | Horizon 4.2.0 | 判断 |
|---|---|---|
| URL 表达全部状态 | 筛选、排序、页码在 URL 中;搜索页重建 URL 时只补 q,丢掉 type、options[prefix](createURLParameters) | 部分一致 |
| 后退回上一个筛选状态 | 全仓库搜索 popstate、hashchange,只有 scroll-container.js 监听 popstate 且仅恢复滚动位置,未见重新渲染;静态推断后退只改地址不刷结果 | 缺口,需实测 |
| 无 JS 仍可筛选 | 筛选相关文件无 type="submit",全仓库无 <noscript,无 JS 时筛选与排序不生效 | 缺口 |
| 零结果值不可点 | count == 0 且未选中时输出 disabled 并划线 | 一致 |
| 加载时保留旧结果、取消过期 | 旧结果保留,上一渲染被 abort;筛选与分页相关文件无 aria-busy(全仓库仅 cart-discount.js 使用);焦点是否保留未核验 | 部分 |
| 错误时保留条件并可重试 | pushState 先于请求;getSectionHTML 不检查 response.ok,失败无提示 | 缺口 |
| 结果数量以状态文字通知 | filters.liquid L122、L284 有常驻 role="status" 计数,按 morph 就地更新推断;无限滚动追加不更新 | 一致,播报未核验 |
| 已选条件可逐项移除 | 标签为 role="button",含条件名与隐藏「移除」;horizontal 样式在 750px 以上把标签隐藏,只剩单值摘要、每组「清除」与「全部清除」,而两个默认模板都是 horizontal | 部分 |
| 筛选组用 fieldset 与 legend | 列表筛选是 details/summary 加 ul;仅色板与图片值各有 fieldset;无组级 fieldset | 缺口 |
| 变更即应用需事先告知 | on:change 直接刷新,无「应用」按钮,是否构成上下文变化未核验 | 未核验 |
| 价格不依赖拖拽 | 两个文本框,inputmode="decimal",越界自动钳制 | 一致 |
| 空结果给出口 | 集合页 products.size == 0 时显示 no_products_found 与指向 collection.url 的清除链接;搜索页若筛选后 search.results_count 为 0,filters 块不再输出,静态推断筛选条件与标签一并消失 | 集合一致,搜索有缺口 |
| 分页为真实链接 | 关闭无限滚动后有 nav 名称、<a href>、aria-current="page"、禁用项 aria-disabled,为整页跳转 | 一致 |
| 无限滚动需兜底与稳定 URL | 默认设置下 product-grid.liquid L168–172 不输出 pagination-controls,无「加载更多」按钮,HTML 无第 2 页链接;追加时有 pushState,但新增数量无播报(paginated-list.js 无 aria 与 live) | 缺口 |
| 页面标题反映第 N 页 | meta-tags.liquid L109 服务端输出英文「Page N」,追加页时不更新 document.title | 部分 |
| 抓取控制 | 全仓库搜索 noindex、robots、rel="next" 无命中;canonical_url 是否含分页与筛选参数未核验 | 未核验 |
定制入口与风险
- 编辑器:
filters块的filter_style、enable_filtering、enable_sorting、enable_grid_density、show_swatch_label、show_filter_label;分区的enable_infinite_scroll、products_per_page、layout_type、product_card_size。筛选项本身在 Search & Discovery,不在主题。 - 耦合点:
facets.js依赖facets-form-component的section-id、data-page-type、data-results-count;paginated-list.js依赖ref="cards[]"、data-page、data-last-page;三套外壳里控件id靠id_prefix与filter_style区分;抽屉触发按钮未写aria-controls,theme-drawer的回退聚焦选择器依赖该属性。
建议的验证范围
本篇未执行。1) 依次选两个筛选与同组两个值,核对 URL、计数与结果;2) 筛选后按浏览器后退,观察地址与结果是否一致;3) 在搜索页筛到 0 结果,检查筛选与标签是否仍在;4) 断网或返回 500 时筛选,观察 URL 与页面;5) 默认设置下翻到第 3 页再进商品并返回,检查位置、URL 与第 1、2 页链接;6) 关闭无限滚动核对页码链接、标题与 canonical;7) 关闭 JS 测筛选与排序;8) 只用键盘与读屏软件操作筛选、抽屉与排序,确认数量播报与焦点。