diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue index fae4246335..4fbdc51d22 100644 --- a/packages/frontend/src/components/MkPageWindow.vue +++ b/packages/frontend/src/components/MkPageWindow.vue @@ -59,7 +59,7 @@ const windowRouter = createRouter(props.initialPath); const pageMetadata = ref<null | PageMetadata>(null); const windowEl = shallowRef<InstanceType<typeof MkWindow>>(); const history = ref<{ path: string; }[]>([{ - path: windowRouter.getCurrentPath(), + path: windowRouter.getCurrentFullPath(), }]); const buttonsLeft = computed(() => { const buttons: Record<string, unknown>[] = []; @@ -97,20 +97,20 @@ function getSearchMarker(path: string) { const searchMarkerId = ref<string | null>(getSearchMarker(props.initialPath)); windowRouter.addListener('push', ctx => { - history.value.push({ path: ctx.path }); + history.value.push({ path: ctx.fullPath }); }); windowRouter.addListener('replace', ctx => { history.value.pop(); - history.value.push({ path: ctx.path }); + history.value.push({ path: ctx.fullPath }); }); windowRouter.addListener('change', ctx => { - if (_DEV_) console.log('windowRouter: change', ctx.path); - searchMarkerId.value = getSearchMarker(ctx.path); + if (_DEV_) console.log('windowRouter: change', ctx.fullPath); + searchMarkerId.value = getSearchMarker(ctx.fullPath); analytics.page({ - path: ctx.path, - title: ctx.path, + path: ctx.fullPath, + title: ctx.fullPath, }); }); @@ -139,14 +139,14 @@ const contextmenu = computed(() => ([{ icon: 'ti ti-external-link', text: i18n.ts.openInNewTab, action: () => { - window.open(url + windowRouter.getCurrentPath(), '_blank', 'noopener'); + window.open(url + windowRouter.getCurrentFullPath(), '_blank', 'noopener'); windowEl.value?.close(); }, }, { icon: 'ti ti-link', text: i18n.ts.copyLink, action: () => { - copyToClipboard(url + windowRouter.getCurrentPath()); + copyToClipboard(url + windowRouter.getCurrentFullPath()); }, }])); @@ -164,12 +164,12 @@ function close() { } function expand() { - mainRouter.push(windowRouter.getCurrentPath(), 'forcePage'); + mainRouter.push(windowRouter.getCurrentFullPath(), 'forcePage'); windowEl.value?.close(); } function popout() { - _popout(windowRouter.getCurrentPath(), windowEl.value?.$el); + _popout(windowRouter.getCurrentFullPath(), windowEl.value?.$el); windowEl.value?.close(); } diff --git a/packages/frontend/src/components/global/NestedRouterView.vue b/packages/frontend/src/components/global/NestedRouterView.vue index 5b22d0b7a8..af00347db8 100644 --- a/packages/frontend/src/components/global/NestedRouterView.vue +++ b/packages/frontend/src/components/global/NestedRouterView.vue @@ -14,8 +14,9 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { inject, onBeforeUnmount, provide, ref, shallowRef } from 'vue'; -import type { Router, Resolved } from '@/router.js'; +import { inject, provide, ref, shallowRef } from 'vue'; +import type { Router } from '@/router.js'; +import type { PathResolvedResult } from '@/lib/nirax.js'; import MkLoadingPage from '@/pages/_loading_.vue'; import { DI } from '@/di.js'; @@ -32,7 +33,7 @@ if (router == null) { const currentDepth = inject(DI.routerCurrentDepth, 0); provide(DI.routerCurrentDepth, currentDepth + 1); -function resolveNested(current: Resolved, d = 0): Resolved | null { +function resolveNested(current: PathResolvedResult, d = 0): PathResolvedResult | null { if (d === currentDepth) { return current; } else { @@ -47,19 +48,13 @@ function resolveNested(current: Resolved, d = 0): Resolved | null { const current = resolveNested(router.current)!; const currentPageComponent = shallowRef('component' in current.route ? current.route.component : MkLoadingPage); const currentPageProps = ref(current.props); -const key = ref(router.getCurrentPath()); +const key = ref(router.getCurrentFullPath()); -function onChange({ resolved }) { +router.useListener('change', ({ resolved }) => { const current = resolveNested(resolved); if (current == null || 'redirect' in current.route) return; currentPageComponent.value = current.route.component; currentPageProps.value = current.props; - key.value = router.getCurrentPath(); -} - -router.addListener('change', onChange); - -onBeforeUnmount(() => { - router.removeListener('change', onChange); + key.value = router.getCurrentFullPath(); }); </script> diff --git a/packages/frontend/src/components/global/RouterView.vue b/packages/frontend/src/components/global/RouterView.vue index 720034a942..fbdb7d261e 100644 --- a/packages/frontend/src/components/global/RouterView.vue +++ b/packages/frontend/src/components/global/RouterView.vue @@ -5,10 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_pageContainer" style="height: 100%;"> - <KeepAlive - :max="prefer.s.numberOfPageCache" - :exclude="pageCacheController" - > + <KeepAlive :max="prefer.s.numberOfPageCache"> <Suspense :timeout="0"> <component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/> @@ -21,10 +18,9 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { inject, onBeforeUnmount, provide, ref, shallowRef, computed, nextTick } from 'vue'; -import type { Router, Resolved, RouteDef } from '@/router.js'; +import { inject, provide, ref, shallowRef } from 'vue'; +import type { Router } from '@/router.js'; import { prefer } from '@/preferences.js'; -import { globalEvents } from '@/events.js'; import MkLoadingPage from '@/pages/_loading_.vue'; import { DI } from '@/di.js'; @@ -44,45 +40,12 @@ provide(DI.routerCurrentDepth, currentDepth + 1); const current = router.current!; const currentPageComponent = shallowRef('component' in current.route ? current.route.component : MkLoadingPage); const currentPageProps = ref(current.props); -const key = ref(router.getCurrentPath()); +const key = ref(router.getCurrentFullPath()); -function onChange({ resolved }) { +router.useListener('change', ({ resolved }) => { if (resolved == null || 'redirect' in resolved.route) return; currentPageComponent.value = resolved.route.component; currentPageProps.value = resolved.props; - key.value = router.getCurrentPath(); - - nextTick(() => { - // ページ遷移完了後に再びキャッシュを有効化 - if (clearCacheRequested.value) { - clearCacheRequested.value = false; - } - }); -} - -router.addListener('change', onChange); - -// #region キャッシュ制御 - -/** - * キャッシュクリアが有効になったら、全キャッシュをクリアする - * - * keepAlive側にwatcherがあるのですぐ消えるとはおもうけど、念のためページ遷移完了まではキャッシュを無効化しておく。 - * キャッシュ有効時向けにexcludeを使いたい場合は、pageCacheControllerに並列に突っ込むのではなく、下に追記すること - */ -const pageCacheController = computed(() => clearCacheRequested.value ? /.*/ : undefined); -const clearCacheRequested = ref(false); - -globalEvents.on('requestClearPageCache', () => { - if (_DEV_) console.log('clear page cache requested'); - if (!clearCacheRequested.value) { - clearCacheRequested.value = true; - } -}); - -// #endregion - -onBeforeUnmount(() => { - router.removeListener('change', onChange); + key.value = router.getCurrentFullPath(); }); </script> diff --git a/packages/frontend/src/components/global/StackingRouterView.vue b/packages/frontend/src/components/global/StackingRouterView.vue index b94e020072..c95c74aef3 100644 --- a/packages/frontend/src/components/global/StackingRouterView.vue +++ b/packages/frontend/src/components/global/StackingRouterView.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only :duration="200" tag="div" :class="$style.tabs" > - <div v-for="(tab, i) in tabs" :key="tab.key" :class="$style.tab" :style="{ '--i': i - 1 }"> + <div v-for="(tab, i) in tabs" :key="tab.fullPath" :class="$style.tab" :style="{ '--i': i - 1 }"> <div v-if="i > 0" :class="$style.tabBg" @click="back()"></div> <div :class="$style.tabFg" @click.stop="back()"> <div v-if="i > 0" :class="$style.tabMenu"> @@ -41,10 +41,9 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { inject, onBeforeUnmount, provide, ref, shallowRef, computed, nextTick } from 'vue'; -import type { Router, Resolved, RouteDef } from '@/router.js'; +import { inject, provide, shallowRef } from 'vue'; +import type { Router } from '@/router.js'; import { prefer } from '@/preferences.js'; -import { globalEvents } from '@/events.js'; import MkLoadingPage from '@/pages/_loading_.vue'; import { DI } from '@/di.js'; import { deepEqual } from '@/utility/deep-equal.js'; @@ -63,26 +62,36 @@ const currentDepth = inject(DI.routerCurrentDepth, 0); provide(DI.routerCurrentDepth, currentDepth + 1); const tabs = shallowRef([{ - key: router.getCurrentPath(), - path: router.getCurrentPath(), - route: router.current.route.path, + fullPath: router.getCurrentFullPath(), + routePath: router.current.route.path, component: 'component' in router.current.route ? router.current.route.component : MkLoadingPage, props: router.current.props, }]); -function onChange({ resolved }) { +function mount() { const currentTab = tabs.value[tabs.value.length - 1]; - const route = resolved.route.path; - if (resolved == null || 'redirect' in resolved.route) return; - if (resolved.route.path === currentTab.path && deepEqual(resolved.props, currentTab.props)) return; - const fullPath = router.getCurrentPath(); + tabs.value = [currentTab]; +} - if (tabs.value.some(tab => tab.route === route && deepEqual(resolved.props, tab.props))) { +function back() { + const prev = tabs.value[tabs.value.length - 2]; + tabs.value = [...tabs.value.slice(0, tabs.value.length - 1)]; + router.replace(prev.fullPath); +} + +router.useListener('change', ({ resolved }) => { + const currentTab = tabs.value[tabs.value.length - 1]; + const routePath = resolved.route.path; + if (resolved == null || 'redirect' in resolved.route) return; + if (resolved.route.path === currentTab.routePath && deepEqual(resolved.props, currentTab.props)) return; + const fullPath = router.getCurrentFullPath(); + + if (tabs.value.some(tab => tab.routePath === routePath && deepEqual(resolved.props, tab.props))) { const newTabs = []; for (const tab of tabs.value) { newTabs.push(tab); - if (tab.route === route && deepEqual(resolved.props, tab.props)) { + if (tab.routePath === routePath && deepEqual(resolved.props, tab.props)) { break; } } @@ -93,45 +102,23 @@ function onChange({ resolved }) { tabs.value = tabs.value.length >= prefer.s.numberOfPageCache ? [ ...tabs.value.slice(1), { - key: fullPath, - path: fullPath, - route, + fullPath: fullPath, + routePath, component: resolved.route.component, props: resolved.props, }, ] : [...tabs.value, { - key: fullPath, - path: fullPath, - route, + fullPath: fullPath, + routePath, component: resolved.route.component, props: resolved.props, }]; -} +}); -function onReplace({ path }) { +router.useListener('replace', ({ fullPath }) => { const currentTab = tabs.value[tabs.value.length - 1]; - console.log('replace', currentTab.path, path); - currentTab.path = path; + currentTab.fullPath = fullPath; tabs.value = [...tabs.value.slice(0, tabs.value.length - 1), currentTab]; -} - -function mount() { - const currentTab = tabs.value[tabs.value.length - 1]; - tabs.value = [currentTab]; -} - -function back() { - const prev = tabs.value[tabs.value.length - 2]; - tabs.value = [...tabs.value.slice(0, tabs.value.length - 1)]; - router.replace(prev.path); -} - -router.addListener('replace', onReplace); -router.addListener('change', onChange); - -onBeforeUnmount(() => { - router.removeListener('replace', onReplace); - router.removeListener('change', onChange); }); </script> diff --git a/packages/frontend/src/events.ts b/packages/frontend/src/events.ts index a74018223c..dfd3d4120c 100644 --- a/packages/frontend/src/events.ts +++ b/packages/frontend/src/events.ts @@ -10,5 +10,4 @@ export const globalEvents = new EventEmitter<{ themeChanging: () => void; themeChanged: () => void; clientNotification: (notification: Misskey.entities.Notification) => void; - requestClearPageCache: () => void; }>(); diff --git a/packages/frontend/src/lib/nirax.ts b/packages/frontend/src/lib/nirax.ts index cc20d497e6..8783874bc2 100644 --- a/packages/frontend/src/lib/nirax.ts +++ b/packages/frontend/src/lib/nirax.ts @@ -5,7 +5,7 @@ // NIRAX --- A lightweight router -import { onMounted, shallowRef } from 'vue'; +import { onBeforeUnmount, onMounted, shallowRef } from 'vue'; import { EventEmitter } from 'eventemitter3'; import type { Component, ShallowRef } from 'vue'; @@ -45,28 +45,28 @@ type ParsedPath = (string | { optional?: boolean; })[]; -export type RouterEvent = { +export type RouterEvents = { change: (ctx: { - beforePath: string; - path: string; - resolved: Resolved; + beforeFullPath: string; + fullPath: string; + resolved: PathResolvedResult; }) => void; replace: (ctx: { - path: string; + fullPath: string; }) => void; push: (ctx: { - beforePath: string; - path: string; + beforeFullPath: string; + fullPath: string; route: RouteDef | null; props: Map<string, string> | null; }) => void; same: () => void; }; -export type Resolved = { +export type PathResolvedResult = { route: RouteDef; props: Map<string, string | boolean>; - child?: Resolved; + child?: PathResolvedResult; redirected?: boolean; /** @internal */ @@ -102,39 +102,39 @@ function parsePath(path: string): ParsedPath { return res; } -export class Nirax<DEF extends RouteDef[]> extends EventEmitter<RouterEvent> { +export class Nirax<DEF extends RouteDef[]> extends EventEmitter<RouterEvents> { private routes: DEF; - public current: Resolved; - public currentRef: ShallowRef<Resolved>; + public current: PathResolvedResult; + public currentRef: ShallowRef<PathResolvedResult>; public currentRoute: ShallowRef<RouteDef>; - private currentPath: string; + private currentFullPath: string; // /foo/bar?baz=qux#hash private isLoggedIn: boolean; private notFoundPageComponent: Component; private redirectCount = 0; - public navHook: ((path: string, flag?: RouterFlag) => boolean) | null = null; + public navHook: ((fullPath: string, flag?: RouterFlag) => boolean) | null = null; - constructor(routes: DEF, currentPath: Nirax<DEF>['currentPath'], isLoggedIn: boolean, notFoundPageComponent: Component) { + constructor(routes: DEF, currentFullPath: Nirax<DEF>['currentFullPath'], isLoggedIn: boolean, notFoundPageComponent: Component) { super(); this.routes = routes; - this.current = this.resolve(currentPath)!; + this.current = this.resolve(currentFullPath)!; this.currentRef = shallowRef(this.current); this.currentRoute = shallowRef(this.current.route); - this.currentPath = currentPath; + this.currentFullPath = currentFullPath; this.isLoggedIn = isLoggedIn; this.notFoundPageComponent = notFoundPageComponent; } public init() { - const res = this.navigate(this.currentPath, false); + const res = this.navigate(this.currentFullPath, false); this.emit('replace', { - path: res._parsedRoute.fullPath, + fullPath: res._parsedRoute.fullPath, }); } - public resolve(path: string): Resolved | null { - const fullPath = path; + public resolve(fullPath: string): PathResolvedResult | null { + let path = fullPath; let queryString: string | null = null; let hash: string | null = null; if (path[0] === '/') path = path.substring(1); @@ -153,7 +153,7 @@ export class Nirax<DEF extends RouteDef[]> extends EventEmitter<RouterEvent> { hash, }; - function check(routes: RouteDef[], _parts: string[]): Resolved | null { + function check(routes: RouteDef[], _parts: string[]): PathResolvedResult | null { forEachRouteLoop: for (const route of routes) { let parts = [..._parts]; @@ -256,14 +256,14 @@ export class Nirax<DEF extends RouteDef[]> extends EventEmitter<RouterEvent> { return check(this.routes, _parts); } - private navigate(path: string, emitChange = true, _redirected = false): Resolved { - const beforePath = this.currentPath; - this.currentPath = path; + private navigate(fullPath: string, emitChange = true, _redirected = false): PathResolvedResult { + const beforeFullPath = this.currentFullPath; + this.currentFullPath = fullPath; - const res = this.resolve(this.currentPath); + const res = this.resolve(this.currentFullPath); if (res == null) { - throw new Error('no route found for: ' + path); + throw new Error('no route found for: ' + fullPath); } if ('redirect' in res.route) { @@ -291,8 +291,8 @@ export class Nirax<DEF extends RouteDef[]> extends EventEmitter<RouterEvent> { if (emitChange && res.route.path !== '/:(*)') { this.emit('change', { - beforePath, - path, + beforeFullPath, + fullPath, resolved: res, }); } @@ -304,37 +304,45 @@ export class Nirax<DEF extends RouteDef[]> extends EventEmitter<RouterEvent> { }; } - public getCurrentPath() { - return this.currentPath; + public getCurrentFullPath() { + return this.currentFullPath; } - public push(path: string, flag?: RouterFlag) { - const beforePath = this.currentPath; - if (path === beforePath) { + public push(fullPath: string, flag?: RouterFlag) { + const beforeFullPath = this.currentFullPath; + if (fullPath === beforeFullPath) { this.emit('same'); return; } if (this.navHook) { - const cancel = this.navHook(path, flag); + const cancel = this.navHook(fullPath, flag); if (cancel) return; } - const res = this.navigate(path); + const res = this.navigate(fullPath); if (res.route.path === '/:(*)') { - location.href = path; + location.href = fullPath; } else { this.emit('push', { - beforePath, - path: res._parsedRoute.fullPath, + beforeFullPath, + fullPath: res._parsedRoute.fullPath, route: res.route, props: res.props, }); } } - public replace(path: string) { - const res = this.navigate(path); + public replace(fullPath: string) { + const res = this.navigate(fullPath); this.emit('replace', { - path: res._parsedRoute.fullPath, + fullPath: res._parsedRoute.fullPath, + }); + } + + public useListener<E extends keyof RouterEvents, L = RouterEvents[E]>(event: E, listener: L) { + this.addListener(event, listener); + + onBeforeUnmount(() => { + this.removeListener(event, listener); }); } } diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index e9b1ee8101..30b7cf9a86 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -169,7 +169,6 @@ import { langmap } from '@/utility/langmap.js'; import { definePage } from '@/page.js'; import { claimAchievement } from '@/utility/achievements.js'; import { store } from '@/store.js'; -import { globalEvents } from '@/events.js'; import MkInfo from '@/components/MkInfo.vue'; import MkTextarea from '@/components/MkTextarea.vue'; @@ -223,7 +222,6 @@ function saveFields() { os.apiWithDialog('i/update', { fields: fields.value.filter(field => field.name !== '' && field.value !== '').map(field => ({ name: field.name, value: field.value })), }); - globalEvents.emit('requestClearPageCache'); } function save() { @@ -249,7 +247,6 @@ function save() { text: i18n.ts.yourNameContainsProhibitedWordsDescription, }, }); - globalEvents.emit('requestClearPageCache'); claimAchievement('profileFilled'); if (profile.name === 'syuilo' || profile.name === 'しゅいろ') { claimAchievement('setNameToSyuilo'); @@ -281,7 +278,6 @@ function changeAvatar(ev) { }); $i.avatarId = i.avatarId; $i.avatarUrl = i.avatarUrl; - globalEvents.emit('requestClearPageCache'); claimAchievement('profileFilled'); }); } @@ -308,7 +304,6 @@ function changeBanner(ev) { }); $i.bannerId = i.bannerId; $i.bannerUrl = i.bannerUrl; - globalEvents.emit('requestClearPageCache'); }); } diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index b5f59b30c1..dd70571d64 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -13,8 +13,8 @@ import { DI } from '@/di.js'; export type Router = Nirax<typeof ROUTE_DEF>; -export function createRouter(path: string): Router { - return new Nirax(ROUTE_DEF, path, !!$i, page(() => import('@/pages/not-found.vue'))); +export function createRouter(fullPath: string): Router { + return new Nirax(ROUTE_DEF, fullPath, !!$i, page(() => import('@/pages/not-found.vue'))); } export const mainRouter = createRouter(location.pathname + location.search + location.hash); @@ -24,23 +24,23 @@ window.addEventListener('popstate', (event) => { }); mainRouter.addListener('push', ctx => { - window.history.pushState({ }, '', ctx.path); + window.history.pushState({ }, '', ctx.fullPath); }); mainRouter.addListener('replace', ctx => { - window.history.replaceState({ }, '', ctx.path); + window.history.replaceState({ }, '', ctx.fullPath); }); mainRouter.addListener('change', ctx => { - console.log('mainRouter: change', ctx.path); + console.log('mainRouter: change', ctx.fullPath); analytics.page({ - path: ctx.path, - title: ctx.path, + path: ctx.fullPath, + title: ctx.fullPath, }); }); mainRouter.init(); export function useRouter(): Router { - return inject(DI.router, null) ?? mainRouter; + return inject(DI.router) ?? mainRouter; } diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue index fa63586ef7..2ef06726f9 100644 --- a/packages/frontend/src/ui/classic.vue +++ b/packages/frontend/src/ui/classic.vue @@ -112,7 +112,7 @@ function onContextmenu(ev: MouseEvent) { if (isLink(ev.target)) return; if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return; if (window.getSelection().toString() !== '') return; - const path = mainRouter.getCurrentPath(); + const path = mainRouter.getCurrentFullPath(); os.contextMenu([{ type: 'label', text: path, diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index a6695de39d..133360972b 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -186,7 +186,7 @@ const onContextmenu = (ev) => { if (isLink(ev.target)) return; if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return; if (window.getSelection()?.toString() !== '') return; - const path = mainRouter.getCurrentPath(); + const path = mainRouter.getCurrentFullPath(); os.contextMenu([{ type: 'label', text: path,