🏃 场景描述
在页面中,经常会带有“返回”按钮。对于这些返回功能的实现,可能会有以下两种常见的处理方式:
调用加载历史栈的前一个地址的
API
,例如vue-router
提供的router.back()
,如下代码所示。back(){
this.$router.back() } 复制代码 ```
但这种实现方式存在的问题是:如果用户是直接在浏览器地址栏输入`URL`进入该页面,当用户点击返回按钮时,会因为历史栈只存在当前地址而无跳转反应。
直接跳转到一个写死的对应上一个页面的地址,如下代码所示。
back(){
// 跳转到该地址上一级的页面 this.$router.push('/xxx') } 复制代码 ```
但这种写法存在的问题,如果该页面可以被多个不同地址的页面跳转进入访问。则当点击返回时,就有可能返回不到用户之前访问的页面。
举一个常见的场景:如果用户处于_表格页面_,其`URL`为`/table?current=3`,`current`代表表格分页页码。当用户点击查阅表格中一个条目而进入_详情页面_,然后再试图通过点击_详情页面_的返回按钮回到刚刚访问的表格页面时,如果开发者写死了返回地址为`/table`,表格会因为`current`的缺失而呈现第 1 页的数据,和用户想继续查看第 3 页的数据的行为预期不一致,从而影响用户的体验。
那么,有没有一种方法可以完美解决上述的情况呢。
🏄 思路分析
解决思路其实很简单:在每次返回时,看一下历史栈是否存在前一个地址,如果有直接调用back()
之类的API
,如果没有,则直接跳转到写死的对应上一个页面即可,如下所示:
back() {
// 通过hasBackHistory布尔量来判断历史栈是否存在前一个地址
if (this.hasBackHistory) {
this.$router.back();
} else {
this.$router.push('xxx');
}
}
复制代码
那么问题是,如何知道历史栈是否存在前一个地址呢?只要同时符合以下两种条件都可以判断存在前一个地址:
存在前一个路由:无论是 vue-router
还是react-router
,都会生成一个初始路由,我们可以通过判断当前路由下的前一个路由是否等于初始路由**浏览器的导航行为类型为 push
:如果是replace
和pop
(即go
、back
或者浏览器前进后退按钮导致的路由变动**),其前一个路由虽然会被vue-router
和react-router
记录,但本质上是不可以跳转的过去的。
由于vue-router
和react-router
的设计和提供的API
不一样,因此对于上述思路的实现方式都不一样。下面的解决方案中会分vue
和react
两种场景去分析如何得知历史栈是否存在前一个地址。
🎣 解决方案
🏊 在 Vue 项目中如何实现
适用的依赖版本为:
vue
:2.6
以上vue-router
:3.6
以上
在vue
中可以直接借用vue-router
提供的beforeRouteEnter
来处理。实现步骤如下所示:
由于
Vue-Router
没有提供可以判断导航行为类型的API
,因此只能在Vue-Router
通过hack
来记录导航行为类型,如下所示:const originPush = VueRouter.prototype.push;
VueRouter.prototype.push = function () {
this.currentNavigateType = "push"; return originPush.apply(this, [...arguments]); };
const originReplace = VueRouter.prototype.replace;
VueRouter.prototype.replace = function () {
this.currentNavigateType = "replace"; return originReplace.apply(this, [...arguments]); };
const originGo = VueRouter.prototype.go;
VueRouter.prototype.go = function () {
this.currentNavigateType = "go"; return originGo.apply(this, [...arguments]); }; 复制代码 ```
在
beforeRouteEnter
路由守卫函数中获取前一个路由,然后对之前说的两种条件进行判断,如下所示:<script>
export default {
data() {
return {
hasBackHistory: false,
};
},
// 借助beforeRouteEnter,该钩子会在渲染该组件的对应路由被生成前调用
beforeRouteEnter(to, from, next) {
next((vm) => {
// 通过判断from是否等于VueRouter.START_LOCATION来判断该页面是否直接从地址栏访问
/** VueRouter.START_LOCATION为初始导航,他的数据结构如下所示:* {
* fullPath: "/",
* hash: "",
* matched: [],
* meta: {},
* name: null,
* params: {},
* path: "/",
* query: {},
* }
*/
// 由于该当守卫函数执行时,组件实例还没被创建,不能获取组件实例 this
,因此我们可以通过next
的回调函数中去获取操作
vm.hasBackHistory =
vm.$router.currentNavigateType === "push" &&
from !== VueRouter.START_LOCATION;
});
},
methods: {
back() {
if (this.hasBackHistory) {
this.$router.back();
} else {
// 这里推荐用replace而非push去更改路由,理由会在代码块下面的文字中解释
this.$router.replace({ path: "xxx" });
}
},
},
};
复制代码
```
对于back
函数中,如果hasBackHistory
为false
,即没有前一个地址的情况下,会使用replace
去更改路由。而为什么推荐用replace
而不是用push
?主要是为了防止一种情况:假设该页面是直接通过输入地址栏加载出来的,然后该页面的上一级页面也有一个返回按键,如果用push
方法返回到上一级页面后,上一级页面用this.$router.back()
返回,就会回到该页面中,从而出现 bug。
大家也可以通过这个示例项目[1]来查看效果。
🏊 在 React 项目中如何实现
适用的依赖版本为:
react
:17
以上react-router-dom
:6
以上。**(5
可以自行根据下面的思路实现,这里我懒得展示了)**
由于react-router
缺乏类似vue-router
提供的路由守卫函数去获取前一个路由,因此需要自己写逻辑监听记录路由变化,这需要借助react-router-dom
的useLocation
和React.Context
来实现。实现步骤如下所示:
新建
LastLocationProvider
来存储前一个路由和监听路由变化import React, { useContext, useEffect, useRef } from "react";
import { Location, useLocation } from "react-router-dom";
const LastLocationContext =
React.createContext<Location | undefined>(undefined);
interface LastLocationProviderProps {
lastLocation?: Location; children?: React.ReactNode; }
const LastLocationProvider: React.FC<LastLocationProviderProps> = ({
children,
}) => {
const location = useLocation();
const lastLocation = useRef
return ( <LastLocationContext.Provider value={lastLocation.current}> {children} </LastLocationContext.Provider> ); };
export default LastLocationProvider;
复制代码
```
`LastLocationProvider`在`<RouterProvider/>`的子组件里被调用,因为`useLocation`需要在`<RouterProvider/>`的包裹下才能调用。
新建
useLastLocation
hook 用以获取前一个路由,通过useNavigationType
获取导航行为类型,然后对之前说的两种条件进行判断,如下所示:export function useLastLocation() {
return useContext(LastLocationContext); } 复制代码 ```
然后放在带有返回功能的组件中调用,如下所示:
```tsx
const navigate = useNavigate();
const navigationType = useNavigationType();
const lastLocation = useLastLocation();
const back = useCallback(() => {
// lastLocation为null 或者 lastLocation.key !== "default"时,可判断其为初始路由 const hasBackHistory = lastLocation && lastLocation.key !== "default" && navigationType !== "REPLACE"; if (hasBackHistory) { navigate(-1); } else { navigate("/table", { replace: true }); } }, []); 复制代码 ```
大家也可以通过这个[示例项目](https://link.juejin.cn?target=https%3A%2F%2Fgitee.com%2FHitotsubashi%2Fback-try-react "https://gitee.com/Hitotsubashi/back-try-react")来查看效果。
🏆 后记
这篇文章写到这里就结束了,如果觉得有用可以点赞收藏,如果有疑问可以直接在评论留言,欢迎交流 👏👏👏。
关于本文
作者:村上小树
https://juejin.cn/post/7205199441287348261