import { useCallback, useEffect, useState } from 'react' import { parseViewPath, viewPath, type View } from './data/view' /** * 视图 ↔ 地址同步。 * * 三个入口都要覆盖:首屏(解析地址)、点击(写地址)、浏览器前进后退(popstate)。 * 视图本身仍然放在 React state 里,地址只是它的镜像,避免每次渲染都去读 URL; * 地址与视图的换算规则收在 `data/view.ts`,这里只管 history。 */ /** 存在 history.state 里的键:当前这条记录是应用内第几页(0 = 没有上一页) */ const HISTORY_INDEX_KEY = 'stashIndex' type StashHistoryState = { [HISTORY_INDEX_KEY]?: number } /** 读取当前历史深度;不是本应用写的记录一律当作 0 */ function historyIndex(): number { const state = window.history.state as StashHistoryState | null const index = state === null ? undefined : state[HISTORY_INDEX_KEY] return typeof index === 'number' ? index : 0 } /** 当前 history.state 的副本,写入前先保留别人(如浏览器扩展)放的字段 */ function currentHistoryState(): StashHistoryState { const state = window.history.state as StashHistoryState | null return state === null ? {} : { ...state } } export type ViewRoute = { /** 当前视图,初值来自地址栏 */ view: View /** 打开一个视图:默认压一条新历史,`replace` 表示替换当前这条 */ go: (view: View, options?: { replace?: boolean }) => void /** * 返回。 * * 应用内有上一页时交给浏览器(`history.back()`), * 直接打开条目地址(收藏 / 刷新后没有上一页)时退到 `fallback`, * 这时它替换当前记录,避免在历史里留下一条「自己指向自己」的条目。 */ back: (fallback: View) => void } export function useViewRoute(): ViewRoute { const [view, setView] = useState(() => parseViewPath(window.location.pathname)) // 首屏把地址规范化:去掉多余的斜杠 / 尾部斜杠,未知地址归到首页, // 并补上历史深度(直接打开时是 0,说明应用内没有上一页) useEffect(() => { const canonical = viewPath(parseViewPath(window.location.pathname)) const state = currentHistoryState() const recorded = state[HISTORY_INDEX_KEY] !== undefined if (canonical === window.location.pathname && recorded) return window.history.replaceState({ ...state, [HISTORY_INDEX_KEY]: historyIndex() }, '', canonical) }, []) // 前进后退:地址变了,视图跟着变 useEffect(() => { const handlePopState = () => setView(parseViewPath(window.location.pathname)) window.addEventListener('popstate', handlePopState) return () => window.removeEventListener('popstate', handlePopState) }, []) const go = useCallback((next: View, options?: { replace?: boolean }) => { const path = viewPath(next) const index = historyIndex() // 已经在同一页(例如重复点同一个侧栏项):不写历史,也不改深度 if (path === window.location.pathname) { setView(next) return } const replace = options?.replace === true const state: StashHistoryState = { ...currentHistoryState(), [HISTORY_INDEX_KEY]: replace ? index : index + 1, } if (replace) { window.history.replaceState(state, '', path) } else { window.history.pushState(state, '', path) } setView(next) }, []) const back = useCallback( (fallback: View) => { if (historyIndex() > 0) { window.history.back() return } go(fallback, { replace: true }) }, [go], ) return { view, go, back } }