← 路由设计 · 稳定入口与可变目标 / 页面栈 · vue-router 版 · 同一套 scheme,换框架原语拼 待审核 20 / 23
页面栈 · vue-router

页面栈 · vue-router 版 · 同一套 scheme,换框架原语拼

原生版是「自己手写一台 reconcile 栈引擎」。这页把完全相同的 scheme——基础层级 push、设置模态栈、返回销毁、直达合成背景——落到 vue-router 的现成原语上,看框架替你接管了哪几块、哪几块仍要自己补。下面的 lab 用 vue-router 同款的 <transition><keep-alive :include> 组合真实驱动过渡与保活,而导航与 history 栈用一套受控状态模型驱动(不真的挂一个 vue-router 实例,以免与本站自身的路由抢地址栏)。

映射关系:meta.layer 区分 push / modal,决定滑动方向;<transition> 按导航是还是退给前后两层不同的进出场 class;<keep-alive :include> 的白名单就是「当前还活着的栈」——push 时上一层留在名单里(保活)、pop 时把离开层移出名单(缓存被剪除 = 销毁);最后一道 beforeEach 守卫,在直达设置页时把背景补进历史。

整页全屏体验:vue-router 整页独立 demo(占满视口、手机系统返回手势直接弹栈);对照原生整页版 page-stack-app

1 · 路由表:把 layer / level 写进 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 式的页面栈语义仍要你用积木拼。

3 · 相关页

  • 本系列 · 页面栈的原生 JS 实现——同一套 scheme 不依赖任何框架,只用 History API + 一个声明式 reconcile,把「保活 / 销毁 / 合成背景」从零搭出来。
  • 本系列 · 嵌套路由 / layout——keep-alive 的「外层复用、只换最内一格」与这页的「公共前缀保活」是同一直觉的两种体现。
  • Vue Router · Transitions · router.vuejs.org——router-view v-slot + transition + keep-alive 的官方组合写法——本页方向滑动就建立在它之上。