import { createRoute } from "../router/index.ts" import type { Route, Router } from "../router/index.ts" /** * @description 创建浏览器路由适配器时需要的输入项。 */ export interface CreateBrowserRouterOptions { router: Router } /** * @description 浏览器路由适配器返回的结果。 * * `dispose` 用于释放当前适配器注册的订阅和浏览器事件监听器。 */ export interface CreateBrowserRouterResult { router: Router dispose: () => void } /** * @description 将 Router 和浏览器路由对接在一起,将 Router 作为唯一信源。 * * 该适配器负责把 Router 产生的历史记录同步到浏览器 `history`,并在浏览器 * `popstate` 或 `hashchange` 发生时,把新的地址变化回灌到 Router。 * * Tips: * - 浏览器 F5 刷新之后 `history.state` 不会丢失。 */ export const createBrowserRouter = ( options: CreateBrowserRouterOptions, ): CreateBrowserRouterResult => { const { router } = options interface State { routeHistoryEntryId: string } const isState = (target: unknown): target is State => { if (typeof target !== "object" || target === null) { return false } if ("routeHistoryEntryId" in target === false) { return false } if (typeof target["routeHistoryEntryId"] !== "string") { return false } return true } const insertState = (route: Route, state: State): void => { const url = location.origin + route.getRecord().partialUrl window.history.pushState(state, "", url) } const updateState = (route: Route, state: State): void => { const url = location.origin + route.getRecord().partialUrl window.history.replaceState(state, "", url) } const upsertState = (route: Route, state: State): void => { const oldUrl = location.origin + createRoute(location.href).getRecord().partialUrl const newUrl = location.origin + route.getRecord().partialUrl if (oldUrl === newUrl) { updateState(route, state) } else { insertState(route, state) } } const subscriberEntry = router.eventManager.subscribe("history", (routeHistoryEntry) => { upsertState(routeHistoryEntry.to.route, { routeHistoryEntryId: routeHistoryEntry.id }) }) const disposeRouterSubscriber = (): void => { subscriberEntry.unsubscribe() } const popstateEventListener = (event: PopStateEvent): void => { // 此事件触发的结果有两种: // 1. 来到 Router 设置的路由,此时 state 可识别。 // 2. 来到非 Router 设置的路由,此时 state 不可识别。 // // 对于 1,判断 state 中的 routeHistoryEntryId 是否与 router 的 // 最新的 routeHistoryEntry 的 id 相同,若相同,则说明此事件由 router // 触发,不需要再次操作(否则会导致循环触发),若不同,则作为新路由同步至 // router。 // 对于 2,始终作为新路由同步至 router。 if (isState(event.state)) { const currentRouteHistoryEntryId = router.getCurrentRouteHistoryEntryId() if (event.state.routeHistoryEntryId === currentRouteHistoryEntryId) { return } } router.navigateTo({ url: location.href }) } window.addEventListener("popstate", popstateEventListener) const disposePopstateEventListener = (): void => { window.removeEventListener("popstate", popstateEventListener) } const hashchangeEventListener = (event: HashChangeEvent): void => { if (isState(history.state)) { const currentRouteHistoryEntryId = router.getCurrentRouteHistoryEntryId() if (history.state.routeHistoryEntryId === currentRouteHistoryEntryId) { return } } router.navigateTo({ url: event.newURL }) } window.addEventListener("hashchange", hashchangeEventListener) const disposeHashchangeEventListener = (): void => { window.removeEventListener("hashchange", hashchangeEventListener) } const dispose = (): void => { disposeRouterSubscriber() disposePopstateEventListener() disposeHashchangeEventListener() } return { router, dispose, } }