From 9046c3304b2d6fdeb62b2cbff9bf916988a83bfd Mon Sep 17 00:00:00 2001 From: n0099 Date: Tue, 25 Jun 2024 01:36:21 +0000 Subject: [PATCH] * replace navigation guard `onBeforeRouteUpdate()` with deep watcher to fix the timing of query cannot get the `fetchedPage` as route hasn't changed at that time + `queryStartedAtSSR` & `isQueriedBySSR` to fix not timing from the point of fetch started while SSR * rename variable `startTime` to `queryStartedAt`, const `isCached` to `isQueryCached`, `(network|render)Time` to `-Duration` - ref `isRouteNewQuery` as moved scrolling to top from watcher of query timing to waterch of route @ pages/posts.vue * fix ref of `data.pages` fetched from `useInfiniteQuery()` may contain nesting refs at root level: https://github.com/TanStack/query/pull/6657 @ `` * using media query in css instead of js before hydrate like 9603fafe8f1f623b7fb2803bdd4ee6cad699e2a6 for noscript user @ `` * partial revert 8283197dae59103130f09255aa1989b8111d18f1 as only scroll to `savedPosition` when it's not scrolling to top * fix regression of `isPathsFirstDirectorySame()` from d5fcc95c2d2f323b64f6fe3feedf4a2f3b73624d @ app/router.options.ts @ fe --- fe/src/app/router.options.ts | 7 ++- fe/src/components/Post/PostNav.vue | 5 ++ .../Post/renderers/list/RendererList.vue | 5 +- fe/src/pages/posts.vue | 53 +++++++++++++------ 4 files changed, 49 insertions(+), 21 deletions(-) diff --git a/fe/src/app/router.options.ts b/fe/src/app/router.options.ts index 50087230..527302f2 100644 --- a/fe/src/app/router.options.ts +++ b/fe/src/app/router.options.ts @@ -59,6 +59,9 @@ export default { ]; }, async scrollBehavior(to, from, savedPosition) { + if (savedPosition !== null && savedPosition.top !== 0) + return savedPosition; + const routeScrollBehavior = useRouteScrollBehaviorStore().get; if (routeScrollBehavior !== undefined) { const ret: ReturnType | undefined = @@ -73,10 +76,10 @@ export default { assertRouteNameIsStr(to.name); assertRouteNameIsStr(from.name); - if (isPathsFirstDirectorySame(to.path, from.path)) + if (!isPathsFirstDirectorySame(to.path, from.path)) return { top: 0 }; } - return savedPosition ?? false; + return false; } } as RouterConfig; diff --git a/fe/src/components/Post/PostNav.vue b/fe/src/components/Post/PostNav.vue index 1a3253d5..fb21ebd8 100644 --- a/fe/src/components/Post/PostNav.vue +++ b/fe/src/components/Post/PostNav.vue @@ -67,6 +67,8 @@ const noScriptStyle = ``; // https://github.com/nuxt/nuxt/issues/13848 useHead({ noscript: [{ innerHTML: noScriptStyle }] }); +const postNavDisplay = ref('none'); // using media query in css instead of js before hydrate +onMounted(() => { postNavDisplay.value = 'unset' }); const threadMenuKey = (cursor: Cursor, tid: Tid) => `c${cursor}-t${tid}`; const routeHash = (tid: Tid | string | null, pid?: Pid | string) => `#${pid ?? (tid === null ? '' : `t${tid}`)}`; @@ -216,6 +218,9 @@ watchEffect(() => { } @media (max-width: 900px) { + .post-nav { + display: v-bind(postNavDisplay); + } .post-nav[aria-expanded=true], .post-nav[aria-expanded=true] + .post-nav-expand { position: fixed; z-index: 1040; diff --git a/fe/src/components/Post/renderers/list/RendererList.vue b/fe/src/components/Post/renderers/list/RendererList.vue index 8dd1ab33..d6a3bbea 100644 --- a/fe/src/components/Post/renderers/list/RendererList.vue +++ b/fe/src/components/Post/renderers/list/RendererList.vue @@ -17,8 +17,9 @@ provideUsers(props.initialPosts.users); export type ThreadWithGroupedSubReplies = Thread & { replies: Array }> }; const posts = computed(() => { - // https://github.com/microsoft/TypeScript/issues/33591 - const newPosts = refDeepClone(props.initialPosts) as + // https://github.com/TanStack/query/pull/6657 + // eslint-disable-next-line unicorn/prefer-structured-clone + const newPosts = _.cloneDeep(props.initialPosts) as // https://github.com/microsoft/TypeScript/issues/33591 Modify> }>; newPosts.threads = newPosts.threads.map(thread => { thread.replies = thread.replies.map(reply => { diff --git a/fe/src/pages/posts.vue b/fe/src/pages/posts.vue index b3403cff..dd84f8e6 100644 --- a/fe/src/pages/posts.vue +++ b/fe/src/pages/posts.vue @@ -39,7 +39,6 @@ const route = useRoute(); const queryClient = useQueryClient(); const queryParam = ref(); const shouldFetch = ref(false); -const isRouteNewQuery = ref(false); const initialPageCursor = ref(''); const { data, error, isPending, isFetching, isFetched, dataUpdatedAt, errorUpdatedAt, fetchNextPage, isFetchingNextPage, hasNextPage } = useApiPosts(queryParam, { initialPageParam: initialPageCursor }); @@ -69,26 +68,37 @@ useHead({ }) }); -let startTime = 0; -watch(isFetching, () => { - if (isFetching.value) - startTime = Date.now(); +const queryStartedAtSSR = useState('postsQuerySSRStartTime', () => 0); +let queryStartedAt = 0; +watchEffect(() => { + if (!isFetching.value) + return; + if (import.meta.server) + queryStartedAtSSR.value = Date.now(); + if (import.meta.client) + queryStartedAt = Date.now(); }); watch([dataUpdatedAt, errorUpdatedAt], async (updatedAt: UnixTimestamp[]) => { const maxUpdatedAt = Math.max(...updatedAt); if (maxUpdatedAt === 0) // just starts to fetch, defer watching to next time return; - const isCached = maxUpdatedAt < startTime; - const networkTime = isCached ? 0 : maxUpdatedAt - startTime; + const isQueriedBySSR = queryStartedAtSSR.value < queryStartedAt; + if (isQueriedBySSR) { + queryStartedAt = queryStartedAtSSR.value; + queryStartedAtSSR.value = Infinity; + } + const isQueryCached = maxUpdatedAt < queryStartedAt; + const networkDuration = isQueryCached ? 0 : maxUpdatedAt - queryStartedAt; await nextTick(); // wait for child components to finish dom update - if (isRouteNewQuery.value) - window.scrollTo({ top: 0 }); - const fetchedPage = data.value?.pages.find(i => i.pages.currentCursor === getRouteCursorParam(route)); + const renderDuration = Date.now() - queryStartedAt - networkDuration; + + const fetchedPage = data.value?.pages.find(i => + i.pages.currentCursor === getRouteCursorParam(route)); const postCount = _.sum(Object.values(fetchedPage?.pages.matchQueryPostCount ?? {})); - const renderTime = (Date.now() - startTime - networkTime) / 1000; notyShow('success', `已加载${postCount}条记录 - 前端耗时${renderTime.toFixed(2)}s - ${isCached ? '使用前端本地缓存' : `后端+网络耗时${_.round(networkTime / 1000, 2)}s`}`); + 前端耗时${_.round(renderDuration / 1000, 2)}s + ${isQueriedBySSR ? '使用服务端渲染预请求' : ''} + ${isQueryCached ? '使用前端本地缓存' : `后端+网络耗时${_.round(networkDuration / 1000, 2)}s`}`); }); watch(isFetched, async () => { if (isFetched.value && renderType.value === 'list') { @@ -115,11 +125,20 @@ const parseRouteThenFetch = async (newRoute: RouteLocationNormalized) => { setQueryParam({ query: JSON.stringify(flattenParams) }); }; -onBeforeRouteUpdate(async (to, from) => { +/** {@link onBeforeRouteUpdate()} fires too early for allowing navigation guard to cancel updating */ +// but we don't need cancelling and there's no onAfterRoute*() events instead of navigation guard available +/** watch on {@link useRoute()} directly won't get reactive since it's not returning {@link ref} but a plain {@link Proxy} */ +// https://old.reddit.com/r/Nuxt/comments/15bwb24/how_to_watch_for_route_change_works_on_dev_not/jtspj6b/ +/** watch deeply on object {@link route.query} and {@link route.params} for nesting params */ +/** and allowing reconstruct partial route to pass it as a param of {@link compareRouteIsNewQuery()} */ +/** ignoring string {@link route.name} or {@link route.path} since switching root level route */ +// will unmounted the component of current page route and unwatch this watcher +watchDeep(() => [route.query, route.params], async (_discard, oldQueryAndParams) => { + const [to, from] = [route, { query: oldQueryAndParams[0], params: oldQueryAndParams[1] } as RouteLocationNormalized]; const isTriggeredByQueryForm = useTriggerRouteUpdateStore() - .isTriggeredBy('@submit', { ...to, force: true }); - isRouteNewQuery.value = to.hash === '' - && (isTriggeredByQueryForm || compareRouteIsNewQuery(to, from)); + .isTriggeredBy('@submit', _.merge(to, { force: true })); + if (to.hash === '' && (isTriggeredByQueryForm || compareRouteIsNewQuery(to, from))) + void nextTick(() => { window.scrollTo({ top: 0 }) }); await parseRouteThenFetch(to); /** must invoke {@link parseRouteThenFetch()} before {@link queryClient.resetQueries()} */