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