/** * 安全路由 Hook - 完全代替原生的 useRouter 和 useLocalSearchParams * * 提供的 Hook: * - useSafeRouter: 代替 useRouter,包含所有路由方法,并对 push/replace/navigate/setParams 进行安全编码 * - useSafeSearchParams: 代替 useLocalSearchParams,获取路由参数 * * 解决的问题: * 1. URI 编解码不对称 - useLocalSearchParams 会自动解码,但 router.push 不会自动编码, * 当参数包含 % 等特殊字符时会拿到错误的值 * 2. 类型丢失 - URL 参数全是 string,Number/Boolean 类型会丢失 * 3. 嵌套对象无法传递 - URL search params 不支持嵌套结构 * * 解决方案: * 采用 Payload 模式,将所有参数打包成 JSON 并 Base64 编码后传递, * 接收时再解码还原,确保数据完整性和类型安全。 * * 优点: * 1. 自动处理所有特殊字符(如 %、&、=、中文、Emoji 等) * 2. 保留数据类型(Number、Boolean 不会变成 String) * 3. 支持嵌套对象和数组传递 * 4. 三端兼容(iOS、Android、Web) * * 使用方式: * ```tsx * // 发送端 - 使用 useSafeRouter 代替 useRouter * const router = useSafeRouter(); * router.push('/detail', { id: 123, uri: 'file:///path/%40test.mp3' }); * router.replace('/home', { tab: 'settings' }); * router.navigate('/profile', { userId: 456 }); * router.back(); * if (router.canGoBack()) { ... } * router.setParams({ updated: true }); * * // 接收端 - 使用 useSafeSearchParams 代替 useLocalSearchParams * const { id, uri } = useSafeSearchParams<{ id: number; uri: string }>(); * ``` */ import { useMemo } from 'react'; import { useRouter as useExpoRouter, useLocalSearchParams as useExpoParams } from 'expo-router'; import { Base64 } from 'js-base64'; const PAYLOAD_KEY = '__safeRouterPayload__'; const LOG_PREFIX = '[SafeRouter]'; const getCurrentParams = (rawParams: Record): Record => { const payload = rawParams[PAYLOAD_KEY]; if (payload && typeof payload === 'string') { const decoded = deserializeParams>(payload); if (decoded && Object.keys(decoded).length > 0) { return decoded; } } const { [PAYLOAD_KEY]: _, ...rest } = rawParams; return rest as Record; }; const serializeParams = (params: Record): string => { try { const jsonStr = JSON.stringify(params); return Base64.encode(jsonStr); } catch (error) { console.error(LOG_PREFIX, 'serialize failed:', error instanceof Error ? error.message : 'Unknown error'); return ''; } }; const deserializeParams = >( payload: string | string[] | undefined ): T | null => { if (!payload || typeof payload !== 'string') { return null; } try { const jsonStr = Base64.decode(payload); return JSON.parse(jsonStr) as T; } catch (error) { console.error(LOG_PREFIX, 'deserialize failed:', error instanceof Error ? error.message : 'Unknown error'); return null; } }; /** * 安全路由 Hook,用于页面跳转,代替 useRouter * @returns 路由方法(继承 useRouter 所有方法,并对以下方法进行安全编码) * - push(pathname, params) - 入栈新页面 * - replace(pathname, params) - 替换当前页面 * - navigate(pathname, params) - 智能跳转(已存在则返回,否则 push) * - setParams(params) - 更新当前页面参数(合并现有参数) */ export function useSafeRouter() { const router = useExpoRouter(); const rawParams = useExpoParams>(); const push = (pathname: string, params: Record = {}) => { const encodedPayload = serializeParams(params); router.push({ pathname: pathname as `/${string}`, params: { [PAYLOAD_KEY]: encodedPayload }, }); }; const replace = (pathname: string, params: Record = {}) => { const encodedPayload = serializeParams(params); router.replace({ pathname: pathname as `/${string}`, params: { [PAYLOAD_KEY]: encodedPayload }, }); }; const navigate = (pathname: string, params: Record = {}) => { const encodedPayload = serializeParams(params); router.navigate({ pathname: pathname as `/${string}`, params: { [PAYLOAD_KEY]: encodedPayload }, }); }; const setParams = (params: Record) => { const currentParams = getCurrentParams(rawParams); const mergedParams = { ...currentParams, ...params }; const encodedPayload = serializeParams(mergedParams); router.setParams({ [PAYLOAD_KEY]: encodedPayload }); }; return { ...router, push, replace, navigate, setParams, }; } /** * 安全获取路由参数 Hook,用于接收方,代替 useLocalSearchParams * 兼容两种跳转方式: * 1. useSafeRouter 跳转 - 自动解码 Payload * 2. 外部跳转(深链接、浏览器直接访问等)- 回退到原始参数 * @returns 解码后的参数对象,类型安全 */ export function useSafeSearchParams>(): T { const rawParams = useExpoParams>(); const decodedParams = useMemo(() => { return getCurrentParams(rawParams) as T; }, [rawParams]); return decodedParams; }