EN
Shopify 知识库 · 指南

Horizon 预测搜索:搜索弹窗、分区请求与键盘交互

基于 Horizon 4.2.0 源码,追踪 Shopify 预测搜索从头部按钮、dialog 弹窗、分区渲染请求到结果面板的全链路,并对照站内搜索模块列出空状态、键盘、aria 与请求失败处理的差距。
历史资料
请结合文中的适用版本和来源阅读。

本文基于 Horizon 4.2.0,源码固定到 5acd1b6(核验于 2026-10-07)。结论限于此提交,不表示其他版本行为相同;本篇为静态源码分析,未完成店铺运行验证。

集合与搜索只点出预测搜索有独立入口,本篇补上请求、渲染、空状态与键盘实现;结果页的筛选与分页见筛选、排序与分页。

对应哪些通用模式

  • 模块:站内搜索的搜索框、建议、无结果与最近浏览。
  • 展现:搜索面板是 <dialog> 以 showModal() 打开的模态,对应 Modal。在 search-modal.liquid、search.liquid、dialog.js、predictive-search.js 中未读到 popover、popovertarget 或 theme-drawer,Popover 与 Drawer 在此不适用;集合、页面、文章结果放在横向 slideshow 中,与 Carousel 相关。

入口与文件

文件职责何时输出
snippets/search.liquid头部按钮 on:click="#search-modal/showDialog"show_search 开启时
snippets/search-modal.liquiddialog-component、predictive-search-component、表单、初始空状态theme.liquid 无条件输出(约 L171)
sections/predictive-search.liquid结果面板:建议、商品、集合、页面、文章、状态文本仅经 Section Rendering 请求
sections/predictive-search-empty.liquid复用 predictive-search-empty-state重置时请求
assets/predictive-search.js防抖、请求、morph、键盘、重置、最近浏览脚本加载后
assets/section-renderer.jsgetSectionHTML 拼 section_id 并 fetch每次请求

结果页顶部的 blocks/_search-input.liquid 是不含预测逻辑的普通 GET 表单。

数据从哪来

  • 请求:Theme.routes.predictive_search_url(scripts.liquid 输出 routes.predictive_search_url),参数 q、resources[limit_scope]=each,由 buildSectionRenderingURL 追加 section_id=predictive-search。全仓库搜索 search/suggest 无命中,路径按官方 Predictive Search API 理解为 /search/suggest,未运行核对。
  • 类型与数量:未设置 resources[type]、resources[limit],类型由平台默认与后台搜索设置决定。Liquid 处理 queries、products、collections、pages、articles,商品列表默认最多渲染 8 个。
  • 空状态:服务端渲染 settings.empty_state_collection | default: collections.all 的前 4 件商品;设置在 config/settings_schema.json「搜索」组。
  • 最近浏览:localStorage 键 viewedProducts,最多 4 个 id,由商品页脚本写入。重置时用 /search?q=id:… OR id:…&resources[type]=product 加同一 section_id 取回,前置到空状态列表。
  • 文案:accessibility.search_results_count、search_suggestions_available、search_results_no_results,zh-CN 均有译文。

交互怎样运行

  1. 打开:showDialog() 调 showModal() 并锁滚动。快捷键判断为 event.metaKey && event.key === 'k',Ctrl+K 不触发。
  2. 输入:on:input="/search",200ms 防抖;空白词重置,否则请求并显示 Clear。
  3. 渲染:响应文本交给 morph(predictiveSearchResults, html) 就地合并,再按 [ref="resultsItems[]"] 计数并结束 SearchUpdateEvent 的 promise。
  4. 过期与失败:每次搜索或重置新建 AbortController 并 abort 上一个,但 getSectionHTML 调用没有传 signal(predictive-search.js L338–339),网络请求不取消,只在响应回来后丢弃过期结果。fetch(...).then(r => r.text()) 不检查 response.ok(section-renderer.js L152–154);在这两个文件中搜索 ok、429、Retry-After 无命中,即无限流退避、错误提示或重试,非 2xx 响应体交给 morph 后的表现未核验。
  5. 提交:表单 GET /search,带 q 与 options[prefix]=last。Enter 三分支:结果恰好 1 项时(data-single-result-url)直接跳转该资源;有选中项时点击其首个 <a>;否则 location.href 为 search_url?q=(只带 q)。
  6. 关闭:dialog:close 触发 #resetSearch,清空输入并重新请求空状态与最近浏览,不走缓存。
  7. 无 JS:头部按钮只有 on:click,无指向 /search 的链接;按静态阅读,脚本失败时该入口无法打开搜索。

与通用契约的一致与差距

对照站内搜索的「状态与退化」与「无障碍」。

契约Horizon 4.2.0判断
取消过期请求200ms 防抖,丢弃过期响应,请求未取消部分一致
combobox 结构输入框有 role="combobox"、aria-controls、aria-autocomplete="list";aria-expanded="false" 写死在 search-modal.liquid,JS 未修改缺口
aria-activedescendant 指示当前项焦点留在输入框,选中项为 li[aria-selected="true"];listbox 内是 ul/li,未见 role="option";全仓库搜索该词仅命中 localization.js缺口,读屏播报未核验
键盘Up/Down 循环;有结果时 Tab 被拦截当作移动;按源码 Esc 同时进入 onSearchKeyDown(重置)与 DialogComponent 的 keydown 监听(关闭),叠加表现未核验部分一致
未选建议时 Enter 提交原词结果只有 1 项时直接跳转,不进结果页与契约不同
结果数量以状态文字通知predictive-search.liquid L29 有 visually-hidden 的 role="status",数量不含 queries;节点随响应插入,非常驻 live region,播报未核验一致,效果未核验
建议为空时收起面板显示 search_results_no_results,View all 仍可提交不一致
空输入显示最近浏览或分类指定集合前 4 件加本机最近浏览,未见说明文案一致,告知未核验
加载与错误旧结果保留到新响应;预测搜索相关文件无 aria-busy;失败无提示缺口
回显原词、按原词搜索入口只在无结果文案与状态文本回显;未读到入口缺口,容错属平台
焦点与减少动态无显式 focus(),依赖 showModal()/close() 默认行为;search-modal.liquid 与 predictive-search-styles.liquid 无 prefers-reduced-motion,滚动到选中项用 prefersReducedMotion()未核验 / 部分

定制入口与风险

  • 编辑器:头部 show_search、search_position、search_row;「搜索」组 empty_state_collection、product_corner_radius、card_corner_radius、card_title_case。predictive-search 分区自身 settings 为空。
  • 耦合点:分区 ID predictive-search、predictive-search-empty 写死在 JS;ref="resultsItems[]" 与 .predictive-search-results__card 决定键盘范围和计数;search-modal.liquid 里的 on:click="/handleModalClick"、/closeDialogOnClickOutside、/closeDialogOnEscapePress 在 JS 中无同名公有方法,component.js 静默忽略,实际由 addEventListener 绑定。
  • 全站输出:隐藏头部搜索图标不会移除弹窗与脚本。

建议的验证范围

本篇未执行。1) 抓取输入英文词、中文词时的 /search/suggest 请求,确认参数与返回类型;2) 快速连输并模拟 429/500,观察面板与控制台;3) 仅键盘完成打开、选择、Enter、Esc,并用读屏软件核对选中项与数量播报;4) 让结果恰好 1 项,确认 Enter 直达,并比较无选中时最终 URL 是否含 options[prefix];5) 设置空状态集合与最近浏览,检查关闭重开的请求次数。

源码基线