EN
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)生效。

交互怎样运行

  1. 触发:勾选、价格 change、排序与移除都进入 FacetsFormComponent。createURLParameters 取 FormData,删掉空价格,删除 page,搜索页补回 q,再 history.pushState,每次变更一条历史记录。
  2. 刷新:sectionRenderer.renderSection(sectionId) 以当前 URL 加 section_id 请求并整段 morph;同一分区未完成的上一次渲染会被 abort(此处传了 signal)。在对话框外用 startViewTransition(对话框内直接渲染)。默认 cache = !Shopify.designMode,同 URL 命中内存缓存,页面 load 时也会把现有分区写入缓存;复选框与色板选项在 pointerenter(防抖 200ms)或 pointerdown 时预取预测 URL。
  3. 事件:请求发起后、渲染完成前分发 SearchUpdateEvent/CollectionUpdateEvent,清除按钮据此切换状态;PaginatedList 收到后清空页缓存并等 data-last-page 变化。
  4. 移除:标签是带 data-url="url_to_remove" 的 facet-remove-component,走 updateFiltersByURL(同样 pushState 加刷新)。
  5. 移动端:theme-drawer 在 990px 以下 showModal(),以上 show() 加 trapFocus;打开后焦点移到关闭按钮,Esc 关闭,关闭后回到触发元素。抽屉底部「查看 N 件商品」只关闭抽屉。桌面 horizontal 的筛选面板是 details 加 floating-panel-component,未用原生 Popover API。
  6. 无限滚动: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) 只用键盘与读屏软件操作筛选、抽屉与排序,确认数量播报与焦点。

源码基线