vue-split-screen:让 Vue Router 的导航轨迹变成两个页面

在手机上,从列表打开详情,当前页面被替换是很自然的交互。到了宽屏或折叠屏上,用户往往希望列表留在左边,详情出现在右边,同时还能继续用浏览器的返回和前进。

vue-split-screen 做的事情很集中:它保留一套 Vue Router,把导航过程中的页面记录成一条轨迹,再把轨迹末尾的两个页面呈现在两个面板里。列表 / 详情、父子页面和折叠屏展开后的双栏视图,都可以沿用同一套路由配置。

先把双栏问题还原成一条导航轨迹

直接放两套 RouterView,很快就会遇到两个地址、两份当前路由和两套浏览器历史的问题。这个项目没有再维护一个平行路由,而是把每次成功导航后的页面关系保存下来:

text 复制代码
trail: A -> B -> C
view:       B | C

每个节点只有两个字段:

ts 复制代码
interface SplitRouteNode {
  id: string;
  fullPath: string;
}

fullPath 表示页面实际地址,包含查询参数和 hash。id 表示这一次页面实例在轨迹中的身份,所以两次访问相同地址,也可以对应两个不同节点。这样处理后,路由地址和页面实例就不会混在一起。

双栏只取轨迹末尾的两个节点。假设当前显示的是 B | C,从不同页面发起导航,结果并不一样:

操作 新轨迹
C.push(D) [A, B, C, D]
B.push(D) [A, B, D]
C.replace(D) [A, B, D]
B.replace(D) [A, D]

push 会保留发起导航的节点,replace 会把它也替换掉。这个规则由纯 TypeScript 函数 navigateTrail 完成,和 Vue 组件、浏览器 API 都没有关系,因此可以单独测试。

从哪个面板点击,就从哪个节点继续导航

双栏场景需要知道按钮属于哪个页面,否则在左侧面板点击跳转时,很容易把右侧页面也当成导航起点。SplitScreen 会在页面上下文里为 useRouter() 注入面板局部的 pushreplace,项目另外提供了 useSplitRouter(),让这层依赖更明确。

页面组件里可以使用项目提供的 useSplitRouter()

vue 复制代码
<script setup lang="ts">
import { useSplitRouter } from 'vue-split-screen';

const router = useSplitRouter();
</script>

<template>
  <button @click="router.push('/detail/42')">
    Open detail
  </button>
</template>

SplitScreen 为每个页面代理注入了自己的路由上下文。这个上下文里,pushreplace 会把当前节点的 id 一起传给历史控制器,控制器再根据节点位置计算下一条轨迹。对象形式的导航也会以发起导航的页面作为解析基准,所以从左侧页面只修改查询参数时,目标仍然落在正确的节点上。

同一个上下文还提供了 useSplitRouteNode(),可以拿到当前节点的 idfullPath。项目推荐在页面逻辑里使用这些组合式函数,把"页面属于哪一个面板"这件事写得明确。

把轨迹写进每一个浏览器历史条目

导航轨迹如果只放在组件内存里,浏览器返回后就无法知道之前显示过哪些页面。vue-split-screen 把它序列化到 history.state

ts 复制代码
{
  __vueSplitScreen: {
    version: 1,
    trail: [
      { id: 'A', fullPath: '/list' },
      { id: 'B', fullPath: '/detail/42' },
    ],
  },
}

Vue Router 仍然负责地址栏、导航守卫以及浏览器的 pushStatereplaceState。项目只在调用路由方法前准备好下一条轨迹,并在 afterEach 中确认导航结果。

这里有几个容易被忽略的时机:

  • 重定向成功后,最后一个节点会更新为重定向后的最终地址。
  • 守卫中止、导航取消或重复导航失败时,轨迹不会被提前修改。
  • 浏览器执行 Back 或 Forward 时,控制器直接读取目标历史条目里的轨迹。
  • 没有项目状态的初始历史条目会补上一条包含当前路由的初始轨迹,格式不正确的外部状态则会被忽略。

因此,Back 和 Forward 恢复的是当时的页面关系,而不只是某个 URL。对于需要保留用户浏览上下文的列表 / 详情页面,这个差别很重要。

用一个 RouterView 渲染当前页和伴随页

接入时,SplitScreen 放在 RouterView 的插槽里,当前页面继续使用 Vue Router 提供的 Component

vue 复制代码
<script setup lang="ts">
import { ref } from 'vue';
import { SplitScreen } from 'vue-split-screen';

const split = ref(true);
const reverse = ref(false);
const maxInactivePages = ref(0);
</script>

<template>
  <RouterView v-slot="{ Component }">
    <SplitScreen
      :turn-on="split"
      :split-reverse="reverse"
      :max-inactive-pages="maxInactivePages"
    >
      <component :is="Component" />
      <template #placeholder>
        Select a page to open the second pane.
      </template>
    </SplitScreen>
  </RouterView>
</template>

当前节点直接渲染插槽里的组件,伴随节点则通过已解析的路由记录渲染对应组件。每个页面外面都有一个 ScreenProxy,它负责提供面板自己的 routerouter 和节点信息。

这里有一个实际的接入边界:默认插槽需要直接渲染 RouterView 提供的 Component。任意额外的包装节点和兄弟节点不会被项目重新构造,独立嵌套的 RouterView 也不属于稳定契约。全局组件属性上的 $router$route 仍然是应用级对象,页面逻辑应使用组合式函数获取面板上下文。

页面离开视野后,要不要保留状态

双栏只展示最后两个页面,前面的页面会进入非活动状态。默认的 maxInactivePages0,页面一旦离开可见的两个位置就会卸载。需要保留输入框草稿、滚动位置或局部组件状态时,可以设置一个正整数:

vue 复制代码
<SplitScreen :max-inactive-pages="2">
  ...
</SplitScreen>

这个数字只计算非活动页面,当前两个面板不占用名额。保留策略按最近使用顺序淘汰,重新回到缓存中的页面时会触发 Vue 的 activated,离开可见区域时会触发 deactivated。项目的演示页把这些生命周期计数直接显示出来,便于观察卸载、缓存和重新激活的区别。

Playground:用可操作的行为说明验证路由

仓库里的 playground 提供了 A、B、C、D 四个页面,可以从任意面板执行 pushreplace、重定向和被守卫拦截的导航。顶部还可以切换双栏方向、单页模式、保留数量,以及浏览器的 Back 和 Forward。

运行演示:

sh 复制代码
pnpm play

实现里的模型测试覆盖轨迹转移、页面呈现和 LRU 保留选择,浏览器测试则直接验证真实的 Vue 组件行为,包括以下场景:

  • 从伴随页面发起 push,轨迹按发起节点分支。
  • 返回和前进恢复精确的历史轨迹。
  • 重定向只提交最终地址,失败导航不改变页面。
  • 页面默认及时卸载,开启保留后按 LRU 淘汰并重新激活缓存页面。
  • 页面守卫和 beforeRouteEnter 仍然拿到正确的组件实例。

项目根目录的完整检查命令是:

sh 复制代码
pnpm check
pnpm test:browser

其中 pnpm check 会依次执行 lint、类型检查、单元测试、库构建和 playground 构建,浏览器行为测试单独通过 Vitest Browser Mode 在 Chromium 中运行。

适合什么场景

如果应用已经使用 Vue Router,又希望在宽屏上同时保留列表和详情,或者让折叠屏展开后展示两个有关联的页面,这个项目提供了一种比较窄的接入方式:路由仍只有一套,页面关系变成可序列化的轨迹,布局层只负责呈现轨迹末尾的节点。

它不试图把 Vue Router 变成两个互相独立的路由器。需要多个地址栏、多个完全独立的导航历史,或者复杂的嵌套路由树时,仍然应该按应用自身的路由模型设计。

项目地址:github.com/zcf0508/vue...,安装命令如下:

sh 复制代码
pnpm add vue-split-screen
相关推荐
bug总结1 小时前
uniapp总结
开发语言·前端·javascript
云运维笔记1 小时前
华为VRP系统文件管理全攻略
前端·华为
走到天涯海角1 小时前
pinia和Vuex的区别以及使用pinia
开发语言·前端·javascript
电梯界知识分子2 小时前
江西抚州临川九尊府五层别墅:受限楼梯间里,全黑铝合金观光井道配曳引龙门架的落地记录
大数据·前端·网络·算法·家用电梯
晴天162 小时前
Web 页面内存溢出与 CPU 溢出:区别、诊断与最佳实践
前端·javascript
摸鱼仙人~2 小时前
需求深度分析提示词-grill me
前端·chrome
Highcharts.js2 小时前
常见报错排雷指南3:兼容性问题的官方解法
前端·javascript·react.js·可视化·highcharts·报错解法
雪芽蓝域zzs2 小时前
第三十六节:keep-alive 页面缓存(Vue3 + 后台管理系统)
前端·vue.js·缓存
言乐62 小时前
Python加速器2视频网页加速器
前端·javascript·css·python·音视频