Agentic Awesome Skills 实战用 Expo Router 构建原生级搜索体验headerSearchBarOptions、useSearch 与过滤模式全解【免费下载链接】agentic-awesome-skillsAAS Core is the local, agent-first control plane for complete catalog discovery, agent-owned selection, stack validation, and planning, backed by 2,445 agentic skills. Includes CLI, local MCP, catalog, plugins, and Workbench.项目地址: https://gitcode.com/gh_mirrors/an/agentic-awesome-skills本文以 building-native-ui 技能 的 search.md 参考文档 为核心骨架系统讲解如何在 Expo Router 应用中实现原生级的搜索体验从 Stack 头部的原生搜索栏headerSearchBarOptions、可复用的useSearch状态 Hook到单字段/多字段过滤、防抖搜索、NativeTabs 搜索 Tab 集成以及空状态与搜索建议的完整落地模式。文中所有代码示例均可直接复制到你的 Expo Router 工程中运行并辅以 AAS 仓库内 Web 应用的真实搜索实现作为佐证帮助你快速掌握这一套先搜后选的交互范式。为什么需要原生搜索栏先掌握三条关键约定在动手写搜索功能前先明确 SKILL.md 中与搜索强相关的三条规则它们决定了下面所有示例的写法优先用headerSearchBarOptions而不是自建搜索框。在 Stack.Screen 的 options 中配置原生搜索栏可以免费获得 iOS 原生的搜索动画、聚焦/取消行为与系统键盘处理不需要额外维护输入框组件。滚动视图必须是路由组件中的第一个子元素并设置contentInsetAdjustmentBehaviorautomatic这样搜索栏与内容区的安全区域插入safe area inset才能被系统正确计算。文件名一律使用 kebab-case如search-screen.tsx并把可复用的 Hook 放在app目录之外的utils/或hooks/目录中app目录内不得混入组件、类型或工具函数详见 route-structure.md。另外SKILL.md 强调优先用 Expo Go 验证功能只有用到 Expo Go 不包含的原生模块时才需要npx expo run:ios/eas build。headerSearchBarOptions属于 Expo Router 与 React Navigation 的原生能力Expo Go 开箱即用。在 Stack 头部添加原生搜索栏headerSearchBarOptions原生搜索栏是 iOS 平台搜索的标准交互入口。在 Expo Router 中只需在 Stack 的某个屏幕的options中声明headerSearchBarOptions即可Stack.Screen nameindex options{{ headerSearchBarOptions: { placeholder: Search, onChangeText: (event) console.log(event.nativeEvent.text), }, }} /注意回调接收的是合成事件event输入内容需要通过event.nativeEvent.text获取这与 Web 端onChange事件直接给字符串不同。完整参数说明以下列出headerSearchBarOptions的全部常用配置项直接对照 search.md 的注释逐项展开headerSearchBarOptions: { // 占位提示文本 placeholder: Search items..., // 首字母自动大写行为none | words | sentences | allCharacters // 搜索场景通常设为 none避免用户输入被强制改写 autoCapitalize: none, // 输入类型影响 iOS 弹出的键盘布局 inputType: text, // text | phone | number | email // 取消按钮文案iOS例如设置为 Done cancelButtonText: Cancel, // 向下滚动列表时隐藏搜索栏iOS hideWhenScrolling: true, // 搜索激活时隐藏导航栏iOS搜索栏会占据整行 hideNavigationBar: true, // 搜索激活时压暗背景内容iOS视觉上聚焦输入区 obscureBackground: true, // 搜索栏在导航栏中的布局方式iOS placement: automatic, // automatic | inline | stacked // 输入变化回调 onChangeText: (event) {}, // 点击键盘搜索按钮回调 onSearchButtonPress: (event) {}, // 点击取消按钮回调 onCancelButtonPress: (event) {}, // 搜索栏获得焦点回调 onFocus: () {}, // 搜索栏失去焦点回调 onBlur: () {}, }关键点与适用前提平台差异headerSearchBarOptions是 UISearchController 的封装主要面向 iOS。Android 上部分行为如cancelButtonText、placement、hideWhenScrolling不可用或行为不同跨平台开发时需在文档中标注 iOS-only 的配置。事件对象类型所有on*事件回调接收NativeSyntheticEventTextInputChangeEventData类型参数内部通过event.nativeEvent.text读取当前文本onFocus/onBlur不携带文本事件。与 Stack.Toolbar 的关系如果你使用 SDK 55 的声明式标题方案还可以用 toolbar-and-headers.md 中介绍的Stack.SearchBar placeholderSearch onChangeText{() {}} /组件在屏幕内直接声明搜索栏iOS only效果与headerSearchBarOptions等价适合把头部逻辑提取成独立组件时使用。把搜索状态抽成 HookuseSearch直接在组件里手写useStatenavigation.setOptions会重复且难以维护。search.md 提供了一种可复用的封装思路——把同步搜索栏文本到 React 状态这件事收敛到一个 Hook 中import { useEffect, useState } from react; import { useNavigation } from expo-router; export function useSearch(options: any {}) { const [search, setSearch] useState(); const navigation useNavigation(); useEffect(() { navigation.setOptions({ headerShown: true, headerSearchBarOptions: { ...options, onChangeText(e: any) { setSearch(e.nativeEvent.text); options.onChangeText?.(e); }, onSearchButtonPress(e: any) { setSearch(e.nativeEvent.text); options.onSearchButtonPress?.(e); }, onCancelButtonPress(e: any) { setSearch(); options.onCancelButtonPress?.(e); }, }, }); }, [options, navigation]); return search; }这个 Hook 做了什么为什么这样写组件挂载后通过navigation.setOptions动态注入headerSearchBarOptions把搜索栏配置和搜索状态绑定在同一个地方。三个核心回调统一将文本写入search状态onChangeText实时更新、onSearchButtonPress在用户按键盘搜索键时兜底更新、onCancelButtonPress清空搜索词。通过options.onChangeText?.(e)保留了外部自定义回调的扩展点——调用方可以同时监听输入而不破坏 Hook 的内部状态同步。依赖数组为[options, navigation]因此调用方传入的options对象需要保持引用稳定例如用useMemo包裹否则每次渲染都会重新setOptions可能导致搜索栏重建、丢失焦点。实际用法搜索即过滤有了useSearch屏幕组件只需一行拿到搜索词然后做过滤渲染function SearchScreen() { const search useSearch({ placeholder: Search items... }); const filteredItems items.filter(item item.name.toLowerCase().includes(search.toLowerCase()) ); return ( FlatList data{filteredItems} renderItem{({ item }) ItemRow item{item} /} / ); }useSearch返回的search是受控的它永远与搜索栏当前文本同步取消搜索时自动重置为空字符串因此过滤逻辑无需关心搜索栏内部状态。过滤模式从单字段到多字段search.md 给出了三种由浅入深的过滤写法按需选用。简单文本过滤单字段const filtered items.filter(item item.name.toLowerCase().includes(search.toLowerCase()) );对name做不区分大小写的子串匹配。注意.toLowerCase()的调用顺序先统一大小写再includes避免大小写不一致导致漏匹配。多字段过滤name / description / tagsconst filtered items.filter(item { const query search.toLowerCase(); return ( item.name.toLowerCase().includes(query) || item.description.toLowerCase().includes(query) || item.tags.some(tag tag.toLowerCase().includes(query)) ); });把 query 提取到闭包内计算一次避免每次比较都重复toLowerCase()标签数组用some做任一标签命中即匹配。这是最常用的商品/技能/文档列表搜索模式。防抖搜索Debounced Search当过滤逻辑较昂贵或需要把搜索词发送到远端 API 时应该在过滤/请求前做防抖避免每次按键都触发一次全量计算或网络请求import { useState, useEffect, useMemo } from react; function useDebounceT(value: T, delay: number): T { const [debounced, setDebounced] useState(value); useEffect(() { const timer setTimeout(() setDebounced(value), delay); return () clearTimeout(timer); }, [value, delay]); return debounced; } function SearchScreen() { const search useSearch(); const debouncedSearch useDebounce(search, 300); const filteredItems useMemo(() items.filter(item item.name.toLowerCase().includes(debouncedSearch.toLowerCase()) ), [debouncedSearch] ); return FlatList data{filteredItems} /; }两个细节值得强调useDebounce的 effect 清理函数clearTimeout(timer)保证快速连续输入时只有最后一次输入后的 300ms 静默期结束才更新debounced值。filteredItems用useMemo包裹并依赖debouncedSearch确保只有防抖值变化时才重算列表避免无关渲染触发全量过滤。仓库佐证这套输入 → 300ms 防抖 → 过滤的模式在 AAS 的 Web 端目录检索页 apps/web-app/src/pages/Home.tsx 中有完全一致的落地window.setTimeout(() setDebouncedSearch(search), 300)并且进一步把防抖后的查询词同步进 URL 查询参数q使用replace避免污染浏览器历史实现刷新/分享链接后自动恢复搜索词。可见防抖 状态可恢复是生产级搜索的标配。搜索与 NativeTabs 集成rolesearch在带 Tab 的应用里搜索通常是一个独立 Tab。NativeTabs 通过rolesearch让 Tab 栏与搜索交互原生地结合起来如 iOS 26 的 liquid glass 搜索 Tab。完整的集成分两层第一层根布局声明搜索 Tabapp/_layout.tsx// app/_layout.tsx NativeTabs NativeTabs.Trigger name(home) LabelHome/Label Icon sfhouse.fill / /NativeTabs.Trigger NativeTabs.Trigger name(search) rolesearch LabelSearch/Label /NativeTabs.Trigger /NativeTabs按 tabs.md 的建议搜索 Tab 放在列表最后一位这样它能与搜索栏组合出最佳 UX。Trigger的name必须与路由名完全一致包括括号如(search)。第二层搜索 Tab 内部用 Stack 承载搜索栏// app/(search)/_layout.tsx Stack Stack.Screen nameindex options{{ headerSearchBarOptions: { placeholder: Search..., onChangeText: (e) setSearch(e.nativeEvent.text), }, }} / /StackNativeTabs 本身不渲染头部因此每个 Tab 内部必须嵌套 Stack 来提供导航栏与搜索栏Stacks and Tabs Structure 约定见 route-structure.md。推荐的目录结构当主页列表与搜索页需要共享详情页时用数组路由(index,search)建一个共享 Stack两个 Tab 都能 push 同一批详情页面app/ _layout.tsx — NativeTabs / (index,search)/ _layout.tsx — Stack / index.tsx — 主列表 search.tsx — 搜索视图 i/[id].tsx — 详情页两个 Tab 共享搜索 Tab 之所以能得到系统级的聚焦时展开搜索栏动效正是rolesearch向平台声明了该 Tab 的语义角色——这是 JS 实现的Tabs无法提供的原生行为。空状态与搜索建议让无结果与未输入可感知搜索体验的最后一步是反馈。search.md 给出了两个必备的 UI 分支。空状态No Results当用户已输入关键词但过滤结果为空时展示居中提示而不是空列表function SearchResults({ search, items }) { const filtered items.filter(/* ... */); if (search filtered.length 0) { return ( View style{{ flex: 1, justifyContent: center, alignItems: center }} Text style{{ color: colors.secondaryLabel }} No results for {search} /Text /View ); } return FlatList data{filtered} /; }这里的colors.secondaryLabel建议直接使用 SKILL.md 中介绍的ColorAPIColor.ios.secondaryLabel/Color.android.dynamic.onSurfaceVariant而非手写十六进制以自动适配浅色/深色模式Web 端则用Platform.select提供 hex 兜底。搜索建议Recent Searches / Suggestions在尚未输入!search时展示最近搜索或热门建议点击即可应用该关键词function SearchScreen() { const search useSearch(); const [recentSearches, setRecentSearches] useStatestring[]([]); if (!search recentSearches.length 0) { return ( View Text style{{ color: colors.secondaryLabel }} Recent Searches /Text {recentSearches.map((term) ( Pressable key{term} onPress{() /* apply search */} Text{term}/Text /Pressable ))} /View ); } return SearchResults search{search} /; }分支逻辑很清晰有输入 → 结果列表无输入但有历史 → 建议列表两者皆无 → 空态。recentSearches可持久化到 AsyncStorage/SecureStore 等本地存储见 storage.md实现跨会话的记忆。生产级搜索的进阶要点仓库实证AAS 仓库的 Web 端搜索实现 apps/web-app/src/pages/Home.tsx 与配套的 apps/web-app/src/utils/catalogSearch.ts 提供了比基础过滤更进一步的工程化范式可作为移动端搜索过滤的参考模型多字段 同义词归一化匹配matchCatalogSkill把id、name、description、category、tags全部纳入匹配字段并通过token()做别名alias归一化等价于把多字段 OR 过滤升级为基于 token 的语义近似匹配。三种匹配模式SearchMode all | any | fuzzy分别对应全部命中任一命中模糊字符序匹配fuzzyMatch用游标依次在目标串中查找每个字符这正是移动端搜索 Tab 上常见的模式切换来源。过滤状态 ↔ URL 双向同步搜索词、分类、匹配模式等全部序列化进 URL 查询参数q、category、match等支持replace写入、前进后退恢复、⌘K/Ctrl K快捷键聚焦搜索框——在 Expo Router 中对应useLocalSearchParamsrouter.replace的同等能力。把这套思路搬回 Expo Router 应用useSearchuseDebounce负责状态headerSearchBarOptions负责原生交互matchCatalogSkill式的多字段 token 匹配负责过滤NativeTabsrolesearch负责 Tab 集成空状态与建议负责反馈闭环——一条完整的原生搜索链路就此成型。平台与版本限制小结能力适用前提说明headerSearchBarOptionsiOSAndroid 部分可用基于 UISearchControllerAndroid 行为有限Stack.SearchBar/Stack.ToolbariOS onlySDK 55声明式头部方案见 toolbar-and-headers.mdNativeTabsrolesearchSDK 5455 推荐搜索 Tab 建议放在最后见 tabs.mduseSearchHook任意 Expo Router 版本需保持 options 引用稳定避免搜索栏重建最后再次强调 SKILL.md 的约束headerSearchBarOptions与 NativeTabs 属于 Expo Router 原生能力具体 API 行为以你当前 SDK 版本的官方文档为准文中示例用于理解模式与原理上线前请结合目标平台真机验证搜索栏交互与过滤性能。【免费下载链接】agentic-awesome-skillsAAS Core is the local, agent-first control plane for complete catalog discovery, agent-owned selection, stack validation, and planning, backed by 2,445 agentic skills. Includes CLI, local MCP, catalog, plugins, and Workbench.项目地址: https://gitcode.com/gh_mirrors/an/agentic-awesome-skills创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考