navigation.entires的介绍
navigation.entries
是一个可以包含当前网站中所有的访问记录 的访问记录队列 (并不等于历史栈)。单个访问记录 被称为NavigationHistoryEntry
,其数据结构如下所示:
图中的每个属性的解释如下所示:
id
: 该访问记录 的id
index
: 该访问记录 所处的**访问记录队列(navigation.entries()
)**里的索引key
: 该访问记录 所在的位置的id
,注意是位置的id
而不是访问记录 自身的id
。可通过navigation.traverseTo(key)
跳转到对应的访问记录sameDocument
: 表示该访问记录 的document
是否与当前访问记录 的document
一致,若不一致,则该访问记录 不能通过history.go
或者浏览器前进/后退按钮跳转到达。通常如果通过<a href="xxx"></a>
点击跳转到新的路由后,会导致以前的访问记录 的sameDocument
为false
url
: 该访问记录对应的地址
如果你在浏览器中页面刷新或者复制tab
操作(如下👇所示),那加载的新页面也会附带上同样的访问记录队列。
与此同时可以通过调用navigation.currentEntry
获取当前路由所处的访问记录
注意这个API
是一个实验性功能,而且要留意浏览器的兼容性,目前不能在firefox
和safari
上使用(写于2023.12.24),如下所示
使用navigation.entries解决路由返回安全
首先说一下什么是路由返回安全。
假设有一个存在路由返回按钮 的页面,点击这个路由返回按钮 后会返回到前一个访问的路由。但如果这个页面是从地址栏直接输入url
进行访问的,或者通过ctrl+click
新开tab进行访问的,那么就会导致在历史栈中仅存在一个地址,从而导致点击返回按钮后页面无响应。
而路由返回安全 就是指对这种情况进行兼容处理。navigation.entries
的出现能够让我们很高效地实现路由返回安全,其实现方式如下所示:
js
// 这里以使用react-router的情况为例,当然其余情况例如vue-router也可以举一反三
function goBack(){
// 获取当前访问记录所在访问记录队列里的索引
const index = window.navigation.currentEntry.index
/**
* 需要判断是否可以直接返回上一级路由的条件有两个:
* 1. index===0: 代表整个访问记录队列中只有当前访问记录,意味着该页面是直接从地址栏或者`ctrl+click`访问的
* 2. !window.navigation.entries()[index-1].sameDocument: 代表该页面是通过点击<a/>标签访问的,因为新旧路由的上下文不同(资源已重新加载),
* 因此历史栈也不一致,因此也不能正常返回
*/
const canNotGoBack = index===0 || !window.navigation.entries()[index-1].sameDocument
if(canNotGoBack){
// 如果历史栈中不存在上级,则直接写死url且用replace进行。为什么要用`replace`呢?主要是防止在新页面里使用路由返回操作返回到该页面
navigate('/previous-route', {replace: true})
}else{
navigate(-1)
}
}
实现访问记录面包屑
除了实现路由返回安全 ,navigation.entires
还可以帮助我们轻松实现访问记录面包屑 。所谓访问记录面包屑就是根据访问记录生成的面包屑。
下面分vue3
和react
去实现这个功能。注意下面例子中项目的路由模式为history
模式。hash
模式的也可以根据以下代码进行举一反三,就不展示了。
在vue3
中实现
首先路由列表中每个路由要定义meta.name
用作面包屑的标题,如下所示:
js
const routes = [
{
path: '/',
component: Home,
meta:{
name:'Home页面'
}
},
{
path: '/users',
component: UserTable,
meta:{
name:'用户列表'
},
},
// ...省略
];
然后我们的访问记录面包屑组件的代码如下所示:
html
<template>
<Breadcrumb>
<BreadcrumbItem
v-for="item in histories"
:key="item.id"
:class="{'pointable-breadcrumb-item': item.backIndex!==0}"
@click="goBack(item.backIndex)"
>
{{ item.title }}
</BreadcrumbItem>
</Breadcrumb>
</template>
<script setup>
import {Breadcrumb, BreadcrumbItem} from 'ant-design-vue';
import {useRouter, useRoute} from 'vue-router'
import {watch,ref} from 'vue'
const route = useRoute()
const router = useRouter()
// 访问列表变量
const histories = ref([])
watch(
// 监视route,有变化则刷新访问列表
()=>route,
()=>{
const temp= window.navigation.entries()
// 只截取开头到当前的访问记录
.slice(
0,
window.navigation.currentEntry.index + 1
)
// 去掉sameDocument为false,即不是同一上下文的反问记录
.filter(({sameDocument})=>sameDocument);
histories.value = temp.map((item,index)=>({
id: item.id,
// 通过router.resolve(pathname)解析出每个访问记录的url对应的路由对象,再取路由对象的meta.name做面包屑的标题
title: router.resolve(new URL(item.url).pathname).meta?.name,
// 用于记录离当前访问记录的偏移量,点击对应面包屑条目时会调用router.go(backIndex)进行返回
backIndex: index + 1 - temp.length
}))
},
{ deep: true,immediate: true }
)
function goBack(backIndex){
if(backIndex!==0){
router.go(backIndex)
}
}
</script>
<style lang="css">
.pointable-breadcrumb-item{
cursor: pointer;
}
</style>
实现后的页面效果如下所示:
更多详细可看示例项目地址
在react
中实现
首先路由列表中每个路由(除了根路由)要定义handle.name
用作面包屑的标题,如下所示:
jsx
// 注意要把路由列表导出
export const routes = [
{
path: "/",
element: <Page />,
children:[
{
index: true,
// 定义handle.name
handle: {name: '首页'},
element: <Home/>,
},
{
path: '/apps',
handle: {name: '应用列表'},
element: <AppTable/>
},
// ...省略
]
},
]
然后我们的访问记录面包屑组件的代码如下所示:
jsx
import { useMemo } from "react";
import { matchRoutes, useLocation, useNavigate } from "react-router-dom";
import { routes } from "../router";
import './RouteBreadcrumb.css'
import { Breadcrumb } from "antd";
export default function RouteBreadcrumb(){
const location = useLocation()
const navigate = useNavigate()
// 访问记录列表变量
const histories = useMemo(()=>{
const temp= window.navigation.entries()
// 只截取开头到当前的访问记录
.slice(
0,
window.navigation.currentEntry.index + 1
)
// 去掉sameDocument为false,即不是同一上下文的反问记录
.filter(({sameDocument})=>sameDocument);
return temp.map((item,index)=>{
const {search,pathname} = new URL(item.url)
// 通过matchRoutes解析出每个访问记录的url对应的路由对象,再取路由对象的handle.name做面包屑的标题
const result = matchRoutes(routes, {search,pathname})
return {
id: item.id,
title: result[result.length-1].route.handle.name,
backIndex: index + 1 - temp.length
}
})
// 监听location生成新的访问记录列表
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location])
function goBack(backIndex){
if(backIndex!==0){
navigate(backIndex)
}
}
return (
<Breadcrumb>
{histories.map(item=>
<Breadcrumb.Item
key={item.id}
className={item.backIndex!==0?'pointable-breadcrumb-item': undefined}
onClick={()=>goBack(item.backIndex)}>
{ item.title }
</Breadcrumb.Item>)
}
</Breadcrumb>
)
}
实现后的页面效果如下所示:
更多详细可看示例项目地址
后记
这篇文章写到这里就结束了,如果觉得有用可以点赞收藏,如果有疑问可以直接在评论留言,欢迎交流 👏👏👏