fix(nuxt): prevent keepalive cache reset (#30807)

This commit is contained in:
Alex Liu 2025-02-07 02:10:35 +08:00 committed by GitHub
parent 4be52d341f
commit 5e7d4938cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 153 additions and 15 deletions

View File

@ -1,5 +1,6 @@
// For pnpm typecheck:docs to generate correct types
import { fileURLToPath } from 'node:url'
import { addPluginTemplate, addRouteMiddleware } from 'nuxt/kit'
export default defineNuxtConfig({
@ -17,6 +18,9 @@ export default defineNuxtConfig({
},
],
pages: process.env.DOCS_TYPECHECK === 'true',
dir: {
app: fileURLToPath(new URL('./test/runtime/app', import.meta.url)),
},
typescript: {
shim: process.env.DOCS_TYPECHECK === 'true',
hoist: ['@vitejs/plugin-vue', 'vue-router'],

View File

@ -3,7 +3,8 @@ import type { Ref, VNode } from 'vue'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { PageRouteSymbol } from './injections'
export const RouteProvider = defineComponent({
export const defineRouteProvider = (name = 'RouteProvider') => defineComponent({
name,
props: {
vnode: {
type: Object as () => VNode,
@ -55,3 +56,5 @@ export const RouteProvider = defineComponent({
}
},
})
export const RouteProvider = defineRouteProvider()

View File

@ -1,12 +1,12 @@
import { Fragment, Suspense, defineComponent, h, inject, nextTick, ref, watch } from 'vue'
import type { AllowedComponentProps, ComponentCustomProps, ComponentPublicInstance, KeepAliveProps, TransitionProps, VNode, VNodeProps } from 'vue'
import type { AllowedComponentProps, Component, ComponentCustomProps, ComponentPublicInstance, KeepAliveProps, Slot, TransitionProps, VNode, VNodeProps } from 'vue'
import { RouterView } from 'vue-router'
import { defu } from 'defu'
import type { RouteLocationNormalized, RouteLocationNormalizedLoaded, RouterViewProps } from 'vue-router'
import { generateRouteKey, toArray, wrapInKeepAlive } from './utils'
import type { RouterViewSlotProps } from './utils'
import { RouteProvider } from '#app/components/route-provider'
import { RouteProvider, defineRouteProvider } from '#app/components/route-provider'
import { useNuxtApp } from '#app/nuxt'
import { useRouter } from '#app/composables/router'
import { _wrapInTransition } from '#app/components/utils'
@ -83,6 +83,9 @@ export default defineComponent({
nuxtApp._isNuxtPageUsed = true
}
let pageLoadingEndHookAlreadyCalled = false
const routerProviderLookup = new WeakMap<Component, ReturnType<typeof defineRouteProvider> | undefined>()
return () => {
return h(RouterView, { name: props.name, route: props.route, ...attrs }, {
default: (routeProps: RouterViewSlotProps) => {
@ -128,7 +131,7 @@ export default defineComponent({
default: () => {
const providerVNode = h(RouteProvider, {
key: key || undefined,
vnode: slots.default ? h(Fragment, undefined, slots.default(routeProps)) : routeProps.Component,
vnode: slots.default ? normalizeSlot(slots.default, routeProps) : routeProps.Component,
route: routeProps.route,
renderKey: key || undefined,
vnodeRef: pageRef,
@ -141,7 +144,6 @@ export default defineComponent({
}
// Client side rendering
const hasTransition = !!(props.transition ?? routeProps.route.meta.pageTransition ?? defaultPageTransition)
const transitionProps = hasTransition && _mergeTransitionProps([
props.transition,
@ -165,18 +167,28 @@ export default defineComponent({
},
}, {
default: () => {
const providerVNode = h(RouteProvider, {
const routeProviderProps = {
key: key || undefined,
vnode: slots.default ? h(Fragment, undefined, slots.default(routeProps)) : routeProps.Component,
vnode: slots.default ? normalizeSlot(slots.default, routeProps) : routeProps.Component,
route: routeProps.route,
renderKey: key || undefined,
trackRootNodes: hasTransition,
vnodeRef: pageRef,
})
if (keepaliveConfig) {
(providerVNode.type as any).name = (routeProps.Component.type as any).name || (routeProps.Component.type as any).__name || 'RouteProvider'
}
return providerVNode
if (!keepaliveConfig) {
return h(RouteProvider, routeProviderProps)
}
const routerComponentType = routeProps.Component.type as any
let PageRouteProvider = routerProviderLookup.get(routerComponentType)
if (!PageRouteProvider) {
PageRouteProvider = defineRouteProvider(routerComponentType.name || routerComponentType.__name)
routerProviderLookup.set(routerComponentType, PageRouteProvider)
}
return h(PageRouteProvider, routeProviderProps)
},
}),
)).default()
@ -232,3 +244,8 @@ function hasChildrenRoutes (fork: RouteLocationNormalizedLoaded | null, newRoute
const index = newRoute.matched.findIndex(m => m.components?.default === Component?.type)
return index < newRoute.matched.length - 1
}
function normalizeSlot (slot: Slot, data: RouterViewSlotProps) {
const slotContent = slot(data)
return slotContent.length === 1 ? h(slotContent[0]!) : h(Fragment, undefined, slotContent)
}

View File

@ -661,14 +661,13 @@ describe('routing utilities: `encodeURL`', () => {
})
describe('routing utilities: `useRoute`', () => {
it('should show provide a mock route', () => {
it('should provide a route', () => {
expect(useRoute()).toMatchObject({
fullPath: '/',
hash: '',
href: '/',
matched: [],
matched: expect.arrayContaining([]),
meta: {},
name: undefined,
name: 'catchall',
params: {},
path: '/',
query: {},

View File

@ -0,0 +1,97 @@
/// <reference path="../fixtures/basic/.nuxt/nuxt.d.ts" />
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import { NuxtLayout, NuxtPage } from '#components'
describe('NuxtPage should work with keepalive options', () => {
let visits = 0
const router = useRouter()
beforeEach(() => {
visits = 0
router.addRoute({
name: 'home',
path: '/home',
component: defineComponent({
name: 'home',
setup () {
visits++
return () => h('div', 'home')
},
}),
})
})
afterEach(() => {
router.removeRoute('home')
})
// include/exclude/boolean
it('should reload setup every time a page is visited, without keepalive', async () => {
const el = await mountSuspended({
setup () {
return () => h(NuxtLayout, {}, { default: () => h(NuxtPage) })
},
})
await navigateTo('/home')
await navigateTo('/')
await navigateTo('/home')
expect(visits).toBe(2)
el.unmount()
})
it('should not remount a page when keepalive is enabled', async () => {
const el = await mountSuspended({
setup () {
return () => h(NuxtLayout, {}, { default: () => h(NuxtPage, { keepalive: true }) })
},
})
await navigateTo('/home')
await navigateTo('/')
await navigateTo('/home')
expect(visits).toBe(1)
el.unmount()
})
it('should not remount a page when keepalive is granularly enabled (with include)', async () => {
const el = await mountSuspended({
setup () {
return () => h(NuxtLayout, {}, { default: () => h(NuxtPage, { keepalive: { include: ['home'] } }) })
},
})
await navigateTo('/home')
await navigateTo('/')
await navigateTo('/home')
expect(visits).toBe(1)
el.unmount()
})
it('should not remount a page when keepalive is granularly enabled (with exclude)', async () => {
const el = await mountSuspended({
setup () {
return () => h(NuxtLayout, {}, { default: () => h(NuxtPage, { keepalive: { exclude: ['catchall'] } }) })
},
})
await navigateTo('/home')
await navigateTo('/')
await navigateTo('/home')
expect(visits).toBe(1)
el.unmount()
})
it('should not remount a page when keepalive options are modified', async () => {
const pages = ref('home')
const el = await mountSuspended({
setup () {
return () => h(NuxtLayout, {}, { default: () => h(NuxtPage, { keepalive: { include: pages.value } }) })
},
})
await navigateTo('/home')
await navigateTo('/')
await navigateTo('/home')
pages.value = 'home,catchall'
await navigateTo('/')
await navigateTo('/home')
expect(visits).toBe(1)
el.unmount()
})
})

View File

@ -0,0 +1,17 @@
import type { RouterOptions } from 'nuxt/schema'
import { defineComponent } from 'vue'
export default <RouterOptions> {
routes (_routes) {
return [
{
name: 'catchall',
path: '/:catchAll(.*)*',
component: defineComponent({
name: 'catchall',
setup: () => () => ({}),
}),
},
]
},
}

View File

@ -13,6 +13,7 @@ export default defineVitestConfig({
environmentOptions: {
nuxt: {
overrides: {
pages: true,
runtimeConfig: {
app: {
buildId: 'override',