为了彻底解决路由返回安全的问题,Chrome提供了navigation.entries这个API

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>点击跳转到新的路由后,会导致以前的访问记录sameDocumentfalse
  • url: 该访问记录对应的地址

如果你在浏览器中页面刷新或者复制tab操作(如下👇所示),那加载的新页面也会附带上同样的访问记录队列

与此同时可以通过调用navigation.currentEntry获取当前路由所处的访问记录

注意这个API是一个实验性功能,而且要留意浏览器的兼容性,目前不能在firefoxsafari上使用(写于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还可以帮助我们轻松实现访问记录面包屑 。所谓访问记录面包屑就是根据访问记录生成的面包屑。

下面分vue3react去实现这个功能。注意下面例子中项目的路由模式为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>
    )
}

实现后的页面效果如下所示:

更多详细可看示例项目地址

后记

这篇文章写到这里就结束了,如果觉得有用可以点赞收藏,如果有疑问可以直接在评论留言,欢迎交流 👏👏👏

相关推荐
神之王楠3 分钟前
如何通过js加载css和html
javascript·css·html
余生H7 分钟前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
花花鱼8 分钟前
@antv/x6 导出图片下载,或者导出图片为base64由后端去处理。
vue.js
流烟默26 分钟前
Vue中watch监听属性的一些应用总结
前端·javascript·vue.js·watch
茶卡盐佑星_1 小时前
meta标签作用/SEO优化
前端·javascript·html
与衫1 小时前
掌握嵌套子查询:复杂 SQL 中 * 列的准确表列关系
android·javascript·sql
金灰1 小时前
HTML5--裸体回顾
java·开发语言·前端·javascript·html·html5
Promise5201 小时前
总结汇总小工具
前端·javascript
Манго нектар1 小时前
JavaScript for循环语句
开发语言·前端·javascript
蒲公英10012 小时前
vue3学习:axios输入城市名称查询该城市天气
前端·vue.js·学习