diff options
| author | syuilo <4439005+syuilo@users.noreply.github.com> | 2025-03-19 15:54:30 +0900 |
|---|---|---|
| committer | syuilo <4439005+syuilo@users.noreply.github.com> | 2025-03-19 15:54:30 +0900 |
| commit | 409cd4fbd3289f591e6c7a875dbb8bb2789f5f7b (patch) | |
| tree | 1b5cb0a63e16ad65289f501b5354975d4ca6ac92 /packages/frontend/src/router.ts | |
| parent | refactor(frontend): router refactoring (diff) | |
| download | misskey-409cd4fbd3289f591e6c7a875dbb8bb2789f5f7b.tar.gz misskey-409cd4fbd3289f591e6c7a875dbb8bb2789f5f7b.tar.bz2 misskey-409cd4fbd3289f591e6c7a875dbb8bb2789f5f7b.zip | |
refactor(frontend): router refactoring
Diffstat (limited to 'packages/frontend/src/router.ts')
| -rw-r--r-- | packages/frontend/src/router.ts | 355 |
1 files changed, 30 insertions, 325 deletions
diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index c6f3937cde..b5f59b30c1 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -3,339 +3,44 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -// NIRAX --- A lightweight router +import { inject } from 'vue'; +import { page } from '@/router.definition.js'; +import { $i } from '@/i.js'; +import { Nirax } from '@/lib/nirax.js'; +import { ROUTE_DEF } from '@/router.definition.js'; +import { analytics } from '@/analytics.js'; +import { DI } from '@/di.js'; -import { onMounted, shallowRef } from 'vue'; -import { EventEmitter } from 'eventemitter3'; -import type { Component, ShallowRef } from 'vue'; +export type Router = Nirax<typeof ROUTE_DEF>; -function safeURIDecode(str: string): string { - try { - return decodeURIComponent(str); - } catch { - return str; - } +export function createRouter(path: string): Router { + return new Nirax(ROUTE_DEF, path, !!$i, page(() => import('@/pages/not-found.vue'))); } -interface RouteDefBase { - path: string; - query?: Record<string, string>; - loginRequired?: boolean; - name?: string; - hash?: string; - globalCacheKey?: string; - children?: RouteDef[]; -} - -interface RouteDefWithComponent extends RouteDefBase { - component: Component, -} - -interface RouteDefWithRedirect extends RouteDefBase { - redirect: string | ((props: Map<string, string | boolean>) => string); -} - -export type RouteDef = RouteDefWithComponent | RouteDefWithRedirect; - -export type RouterFlag = 'forcePage'; - -type ParsedPath = (string | { - name: string; - startsWith?: string; - wildcard?: boolean; - optional?: boolean; -})[]; - -export type RouterEvent = { - change: (ctx: { - beforePath: string; - path: string; - resolved: Resolved; - }) => void; - replace: (ctx: { - path: string; - }) => void; - push: (ctx: { - beforePath: string; - path: string; - route: RouteDef | null; - props: Map<string, string> | null; - }) => void; - same: () => void; -}; - -export type Resolved = { - route: RouteDef; - props: Map<string, string | boolean>; - child?: Resolved; - redirected?: boolean; - - /** @internal */ - _parsedRoute: { - fullPath: string; - queryString: string | null; - hash: string | null; - }; -}; - -function parsePath(path: string): ParsedPath { - const res = [] as ParsedPath; - - path = path.substring(1); - - for (const part of path.split('/')) { - if (part.includes(':')) { - const prefix = part.substring(0, part.indexOf(':')); - const placeholder = part.substring(part.indexOf(':') + 1); - const wildcard = placeholder.includes('(*)'); - const optional = placeholder.endsWith('?'); - res.push({ - name: placeholder.replace('(*)', '').replace('?', ''), - startsWith: prefix !== '' ? prefix : undefined, - wildcard, - optional, - }); - } else if (part.length !== 0) { - res.push(part); - } - } - - return res; -} - -export class Router extends EventEmitter<RouterEvent> { - private routes: RouteDef[]; - public current: Resolved; - public currentRef: ShallowRef<Resolved>; - public currentRoute: ShallowRef<RouteDef>; - private currentPath: string; - private isLoggedIn: boolean; - private notFoundPageComponent: Component; - private redirectCount = 0; - - public navHook: ((path: string, flag?: RouterFlag) => boolean) | null = null; - - constructor(routes: Router['routes'], currentPath: Router['currentPath'], isLoggedIn: boolean, notFoundPageComponent: Component) { - super(); - - this.routes = routes; - this.current = this.resolve(currentPath)!; - this.currentRef = shallowRef(this.current); - this.currentRoute = shallowRef(this.current.route); - this.currentPath = currentPath; - this.isLoggedIn = isLoggedIn; - this.notFoundPageComponent = notFoundPageComponent; - } - - public init() { - const res = this.navigate(this.currentPath, false); - this.emit('replace', { - path: res._parsedRoute.fullPath, - }); - } - - public resolve(path: string): Resolved | null { - const fullPath = path; - let queryString: string | null = null; - let hash: string | null = null; - if (path[0] === '/') path = path.substring(1); - if (path.includes('#')) { - hash = path.substring(path.indexOf('#') + 1); - path = path.substring(0, path.indexOf('#')); - } - if (path.includes('?')) { - queryString = path.substring(path.indexOf('?') + 1); - path = path.substring(0, path.indexOf('?')); - } - - const _parsedRoute = { - fullPath, - queryString, - hash, - }; - - function check(routes: RouteDef[], _parts: string[]): Resolved | null { - forEachRouteLoop: - for (const route of routes) { - let parts = [..._parts]; - const props = new Map<string, string>(); - - pathMatchLoop: - for (const p of parsePath(route.path)) { - if (typeof p === 'string') { - if (p === parts[0]) { - parts.shift(); - } else { - continue forEachRouteLoop; - } - } else { - if (parts[0] == null && !p.optional) { - continue forEachRouteLoop; - } - if (p.wildcard) { - if (parts.length !== 0) { - props.set(p.name, safeURIDecode(parts.join('/'))); - parts = []; - } - break pathMatchLoop; - } else { - if (p.startsWith) { - if (parts[0] == null || !parts[0].startsWith(p.startsWith)) continue forEachRouteLoop; - - props.set(p.name, safeURIDecode(parts[0].substring(p.startsWith.length))); - parts.shift(); - } else { - if (parts[0]) { - props.set(p.name, safeURIDecode(parts[0])); - } - parts.shift(); - } - } - } - } - - if (parts.length === 0) { - if (route.children) { - const child = check(route.children, []); - if (child) { - return { - route, - props, - child, - _parsedRoute, - }; - } else { - continue forEachRouteLoop; - } - } - - if (route.hash != null && hash != null) { - props.set(route.hash, safeURIDecode(hash)); - } - - if (route.query != null && queryString != null) { - const queryObject = [...new URLSearchParams(queryString).entries()] - .reduce((obj, entry) => ({ ...obj, [entry[0]]: entry[1] }), {}); - - for (const q in route.query) { - const as = route.query[q]; - if (queryObject[q]) { - props.set(as, safeURIDecode(queryObject[q])); - } - } - } - - return { - route, - props, - _parsedRoute, - }; - } else { - if (route.children) { - const child = check(route.children, parts); - if (child) { - return { - route, - props, - child, - _parsedRoute, - }; - } else { - continue forEachRouteLoop; - } - } else { - continue forEachRouteLoop; - } - } - } - - return null; - } - - const _parts = path.split('/').filter(part => part.length !== 0); - - return check(this.routes, _parts); - } - - private navigate(path: string, emitChange = true, _redirected = false): Resolved { - const beforePath = this.currentPath; - this.currentPath = path; - - const res = this.resolve(this.currentPath); - - if (res == null) { - throw new Error('no route found for: ' + path); - } - - if ('redirect' in res.route) { - let redirectPath: string; - if (typeof res.route.redirect === 'function') { - redirectPath = res.route.redirect(res.props); - } else { - redirectPath = res.route.redirect + (res._parsedRoute.queryString ? '?' + res._parsedRoute.queryString : '') + (res._parsedRoute.hash ? '#' + res._parsedRoute.hash : ''); - } - if (_DEV_) console.log('Redirecting to: ', redirectPath); - if (_redirected && this.redirectCount++ > 10) { - throw new Error('redirect loop detected'); - } - return this.navigate(redirectPath, emitChange, true); - } - - if (res.route.loginRequired && !this.isLoggedIn) { - res.route.component = this.notFoundPageComponent; - res.props.set('showLoginPopup', true); - } +export const mainRouter = createRouter(location.pathname + location.search + location.hash); - this.current = res; - this.currentRef.value = res; - this.currentRoute.value = res.route; +window.addEventListener('popstate', (event) => { + mainRouter.replace(location.pathname + location.search + location.hash); +}); - if (emitChange && res.route.path !== '/:(*)') { - this.emit('change', { - beforePath, - path, - resolved: res, - }); - } +mainRouter.addListener('push', ctx => { + window.history.pushState({ }, '', ctx.path); +}); - this.redirectCount = 0; - return { - ...res, - redirected: _redirected, - }; - } +mainRouter.addListener('replace', ctx => { + window.history.replaceState({ }, '', ctx.path); +}); - public getCurrentPath() { - return this.currentPath; - } +mainRouter.addListener('change', ctx => { + console.log('mainRouter: change', ctx.path); + analytics.page({ + path: ctx.path, + title: ctx.path, + }); +}); - public push(path: string, flag?: RouterFlag) { - const beforePath = this.currentPath; - if (path === beforePath) { - this.emit('same'); - return; - } - if (this.navHook) { - const cancel = this.navHook(path, flag); - if (cancel) return; - } - const res = this.navigate(path); - if (res.route.path === '/:(*)') { - location.href = path; - } else { - this.emit('push', { - beforePath, - path: res._parsedRoute.fullPath, - route: res.route, - props: res.props, - }); - } - } +mainRouter.init(); - public replace(path: string) { - const res = this.navigate(path); - this.emit('replace', { - path: res._parsedRoute.fullPath, - }); - } +export function useRouter(): Router { + return inject(DI.router, null) ?? mainRouter; } |