实现切换路由时当前布局不变并加载其他路由
最近在过 Next.js
文档 ,看到拦截路由这一章,感觉到惊喜(暂时没想到什么词可以形容了),这种设计在交互和代码上都着实可以称赞。同时联想了一下 Vue.js
如何实现。
举一个例子
路由定义
/photo
: 图片列表/photo/xxx
: 具体某张图片的详情
在页面 /photo
单击图片时,路由发生变化,进入图片详情 /photo/123
, 可以通过模态框( Modal )展示内容。(官方称这种行为拦截路由,屏蔽 URL)
此时,如果使用 /photo/123
直接进入图片详情,直接展示的是详情页面,而不是模态框。
此设计优势,参考 Next.js 官方
- 可通过URL分享
- 刷新时可保留上下文,而不是关闭 Modal
- 路由后退时关闭 Modal ,而不是真正的后退路由
- 后退后可以向前导航打开 Modal
实现效果如图,点击图片,路由已跳转,并且是已模态框形式展现;此时刷新,进入详情页。
Next.js 实现
对 Next.js 不熟的同学,需要去看文档过下基础了。
Intercepting Routes 拦截路由主要是可以实现切换路由时,原页面布局不进行改变,同时可以加载新路由。
Next.js 实现起来并不复杂,官方已经提供模板写法。
通过 Parallel Routes 平行路由来实现。
实现一下步骤:
- 在
/app/layout.jsx
布局文件下放入插槽modal
- 新建文件
/app/@modal/(.)photo/[id]/page.js
为 SPA 形式跳转路由的模态框展示代码 - 文件
/app/photo/[id]/page.js
为新开页面进去的代码 - 以上两者的路由是一样的,跳转路由方式不一样展示效果不一样
自己实现的文件结构
完整代码:intercepting-routes-nextjs
Vue.js 实现
大致思路:在最外层需要一个
layout
组件来控制图片是以什么形态展示;图片列表页需作为父页面因为需要在跳转路由后仍然展示,所以需要在结构上作为父组件页面;而图片详情有两种形态展示,需要一个 Container 来进行分发,至于判断条件是从layout
的数据来。
1 | // 文件夹结构 |
首先是需要配置一下路由信息,内容如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22const routes = [
{
path: '/',
component: () => import('../pages/Layout.vue'),
children: [
{
// 图片列表
path: '/',
name: 'PHOTO_LIST',
component: () => import('../pages/PhotoList/PhotoListContainer.vue'),
children: [
// 图片详情
{
name: 'PHOTO_INFO',
path: '/photo/:id',
component: () => import('../pages/PhotoInfo/PhotoInfoContainer.vue'),
},
],
},
],
},
];
看结构可以看出最外层是一个 Layout
组件,其次是PhotoListContainer
,最后 PhotoInfoContainer
,这个嵌套路由的关系与我们开头所说的关系一致。
核心代码就是 useLayout.js
这块儿,将首次进入的页面记录下来,传递给后面的子组件进行渲染或使用。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41// /src/constant.js
import { markRaw } from "vue";
import PhotoList from "./pages/PhotoList/PhotoList.vue";
export const PAGE = {
PHOTO_LIST: 'PHOTO_LIST',
PHOTO_INFO: 'PHOTO_INFO'
};
export const COMPONENT_CONFIG = {
[PAGE.PHOTO_LIST]: markRaw(PhotoList),
}
// /src/pages/composables/useLayout.js
import { inject, provide, ref, watchEffect, watch } from 'vue';
import { COMPONENT_CONFIG } from '../../constant.js';
import { useRoute } from 'vue-router';
const LAYOUT_SYMBOL = Symbol('layout');
export function useLayout() {
const route = useRoute();
const cmp = ref(COMPONENT_CONFIG[route.name]);
let mountedList = !!cmp.value;
provide(LAYOUT_SYMBOL, {
components: cmp,
});
// 防止直接访问图片详情页后返回图片列表不显示内容;以及直接图片详情返回列表页面,再打开图片出现的兼容问题
watch(() => route.path, () => {
if (!mountedList) {
cmp.value = COMPONENT_CONFIG[route.name];
mountedList = true
}
})
}
export function useLayoutData() {
const data = inject(LAYOUT_SYMBOL, {});
return data;
}
接下来是 PhotoList
的两个组件, PhotoListContainer
这个组件在路由中进行引用,根据 useLayout
传下来的数据进行判断是否要显示图片列表。(备注:这里使用 useLayoutData
获取数据)1
2
3
4
5
6
7
8
9
10
11
12
13
14<!-- /src/pages/PhotoList/PhotoListContainer.vue -->
<template>
<div class="PhotoListContainer">
<component :is="components" />
<router-view />
</div>
</template>
<script setup>
import { useLayoutData } from '../composables/useLayout'
const { components } = useLayoutData()
</script>
接下来是图片详情的三个组件,首先是图片详情的 Container
,用来做分发使用,数据从 layout
传下来,会看 useLayout
的逻辑可以看出,只有首次打开页面为图片列表页 components
才会有数据,此处也就可以判断需要加载 Modal
还是 Page
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21<!-- /src/pages/PhotoInfo/PhotoInfoContainer.vue -->
<template>
<div class="PhotoInfoContainer">
<component :is="InfoCmp" />
</div>
</template>
<script lang="ts" setup>
import { useLayoutData } from '~/pages/composables/useLayout'
import { computed, defineAsyncComponent } from 'vue'
const { components } = useLayoutData()
const InfoCmp = computed(() => {
if (components?.value) {
return defineAsyncComponent(() => import('./PhotoInfoModal.vue'))
} else {
return defineAsyncComponent(() => import('./PhotoInfoPage.vue'))
}
})
</script>
核心实现代码已结束,另外 PhotoList.vue
、 PhotoInfoModal
、 PhotoInfoPage
没有贴上来,感兴趣可以到仓库查看。
完整代码: intercepting-routes
总结
Next.js
实现起来有官方实践推荐,支持较好,跟随文档即可实现。
Vue.js
实现起来有点黑魔法的感觉,相关逻辑需要自行实现,不过也是可以实现类似功能,并且具备相同的特性。(小红书官网的feed流打开笔记详情效果,实现原理也类似)
- 通过路由嵌套的形式,将列表页作为父路由,详情页作为子路由,实现路由页面可以共存,并且切换时还可以保留上下文
- 需在子路由实现以什么形式加载,判断和展示逻辑都需手动实现。