页面栈的 vue-router 实现
原生版自己手写了一台 reconcile 栈引擎。本页把完全相同的 scheme——基础层级 push、设置模态栈、返回销毁、直达合成背景——落到 vue-router 的现成原语上,看框架接管了哪几块、哪几块仍要自己补。
映射关系有四条:meta.layer 区分 push 与 modal 并决定滑动方向;<transition> 按导航是进还是退,给前后两层不同的进出场 class;<keep-alive :include> 的白名单就是「当前还活着的栈」,push 时上一层留在名单里即保活、pop 时把离开层移出名单即缓存剪除;最后一道
beforeEach 守卫在直达设置页时把背景补进历史。
<transition> 与 <keep-alive :include> 真实驱动,导航与 history 栈走一套受控状态模型,以免与本站自身的路由抢地址栏。整页版见 vue-router 整页 demo,对照的原生整页版是 page-stack-app。
1 · 路由表与 meta
和原生版同一张表,只是规则挂到了 meta 上——路由本身不再「知道」自己是模态还是 push,由 meta.layer 告诉 <transition> 该怎么滑、告诉栈面板该怎么标;meta.level 决定层叠的 z-index。
const routes = [
{ path: '/', component: Home, meta: { layer: 'base', level: 1 } },
{ path: '/item', component: Item, meta: { layer: 'base', level: 2 } },
{ path: '/item/reviews', component: Reviews, meta: { layer: 'base', level: 3 } },
{ path: '/settings', component: SetRoot, meta: { layer: 'modal', level: 1 } },
{ path: '/settings/account', component: SetAcc, meta: { layer: 'modal', level: 2 } },
];
const router = createRouter({ history: createWebHashHistory(), routes });
// 进设置前记下来源页, 供直达时合成背景 (对应原生版往 sessionStorage 写来源)
router.beforeEach((to, from) => {
if (to.path.startsWith('/settings') && !from.path.startsWith('/settings'))
sessionStorage.setItem('pageStackBg', from.path);
return true;
});
2 · keep-alive 白名单与栈的对应
vue-router 同一时刻只渲染一条当前路由,<keep-alive> 把离场的组件连同它的 DOM 与滚动位置缓存起来,这就是「返回时上一页还在」。但默认会把所有走过的页都缓存住,与「pop 即销毁」相悖。
解法是用 :include 白名单精确控制谁被保活:每次导航重算 stackFromPath,白名单只留当前栈里的页,返回时离开的那层被移出名单、缓存剪除、组件销毁。
<router-view v-slot="{ Component }">
<transition :enter-from-class="enterFrom" :leave-to-class="leaveTo">
<keep-alive :include="include"> <!-- 白名单 = 当前还活着的栈 -->
<component :is="Component" :key="$route.path" />
</keep-alive>
</transition>
</router-view>
// 每次导航: 算方向 (进/退) + 重算保活白名单。只有「模态根」进出走垂直, 其余走水平 push
const modalRoot = (p) => p === '/settings';
router.afterEach((to, from) => {
const forward = stackFromPath(to.path).length >= stackFromPath(from.path).length;
if (forward) {
enterFrom.value = modalRoot(to.path) ? 'off-y' : 'off-x'; // 模态根从下浮起 / 其余从右
leaveTo.value = modalRoot(to.path) ? '' : 'under'; // 背景留住 / 左移压暗
} else {
leaveTo.value = modalRoot(from.path) ? 'off-y' : 'off-x'; // 模态根向下退出 / 其余向右
enterFrom.value = modalRoot(from.path) ? '' : 'under'; // 背景原地露出 / 从左滑回
}
include.value = stackFromPath(to.path).map(nameOf); // 离开层被移出 → keep-alive 剪除 → 销毁
});
// 直达深层设置页: 守卫里重建历史 [背景, 设置root, ...], 返回才逐级有处可去
async function enterDirect(path) {
const want = stackFromPath(path); // 已含合成的背景
await router.replace(want[0]);
for (let i = 1; i < want.length; i++) await router.push(want[i]);
}
建议 · 框架接管的是 history 同步、<a> 拦截、缓存与复用(keep-alive)、过渡编排(transition)。仍要自己补的是「哪些该保活」的栈策略(include 白名单)、滑动方向判定(进或退)、以及直达时的背景合成(守卫里重建历史)。框架给的是积木,UIKit
式的页面栈语义仍要用积木拼。
keep-alive 的「外层复用、只换最内一格」与本页的「公共前缀保活」是同一直觉的两种体现,嵌套路由一页从另一个角度讲它。不依赖框架的从零实现见原生版。