likes
comments
collection
share

对于页面的"返回"操作,这样处理比较合适🎯🎯

作者站长头像
站长
· 阅读数 57

🏃场景描述

在页面中,经常会带有“返回”按钮。对于这些返回功能的实现,可能会有以下两种常见的处理方式:

  1. 调用加载历史栈的前一个地址API,例如vue-router提供的router.back(),如下代码所示。

    back(){
      this.$router.back()
    }
    

    但这种实现方式存在的问题是:如果用户是直接在浏览器地址栏输入URL进入该页面,当用户点击返回按钮时,会因为历史栈只存在当前地址而无跳转反应。

  2. 直接跳转到一个写死的对应上一个页面的地址,如下代码所示。

    back(){
     // 跳转到该地址上一级的页面
      this.$router.push('/xxx')
    }
    

    但这种写法存在的问题,如果该页面可以被多个不同地址的页面跳转进入访问。则当点击返回时,就有可能返回不到用户之前访问的页面。

    举一个常见的场景:如果用户处于表格页面,其URL/table?current=3current代表表格分页页码。当用户点击查阅表格中一个条目而进入详情页面,然后再试图通过点击详情页面的返回按钮回到刚刚访问的表格页面时,如果开发者写死了返回地址为/table,表格会因为current的缺失而呈现第 1 页的数据,和用户想继续查看第 3 页的数据的行为预期不一致,从而影响用户的体验。

那么,有没有一种方法可以完美解决上述的情况呢。

🏄思路分析

解决思路其实很简单:在每次返回时,看一下历史栈是否存在前一个地址,如果有直接调用back()之类的API,如果没有,则直接跳转到写死的对应上一个页面即可,如下所示:

back() {
  // 通过hasBackHistory布尔量来判断历史栈是否存在前一个地址
  if (this.hasBackHistory) {
    this.$router.back();
  } else {
    this.$router.push('xxx');
  }
}

那么问题是,如何知道历史栈是否存在前一个地址呢?由于vue-routerreact-router的设计和提供的API不一样,因此下面得解决方案中会分vuereact两种场景去分析如何得知历史栈是否存在前一个地址

🎣解决方案

🏊在Vue项目中如何实现

适用的依赖版本为:

  • vue: 2.6以上
  • vue-router3.6以上

vue中可以直接借用vue-router提供的beforeRouteEnter来处理,如下所示:

<script>
  export default {
    data() {
      return {
        hasBackHistory: false,
      };
    },
    methods: {
      back() {
        if (this.hasBackHistory) {
          this.$router.back();
        } else {
          this.$router.push("xxx");
        }
      },
    },
    // 借助beforeRouteEnter,该钩子会在渲染该组件的对应路由被生成前调用
    beforeRouteEnter(to, from, next) {
      let hasBackHistory = false;
      // 通过from的name是否为null来判断是否存在前一次的路由
      /** 当不存在前一个地址时,from的值如下所示
       *  {
       *    fullPath: "/",
       *    hash: "",
       *    matched: [],
       *    meta: {},
       *    name: null,
       *    params: {},
       *    path: "/",
       *    query: {},
       * }
       */
      if (from.name !== null) hasBackHistory = true;
      //  由于该当守卫函数执行时,组件实例还没被创建,不能获取组件实例 `this`,因此我们可以通过`next`的回调函数中去获取操作
      next((vm) => {
        vm.hasBackHistory = hasBackHistory;
      });
    },
  };
</script>

值得一提的时,如果开发者在vue-router注册路由没加上name属性,那此时from.name的值为undefined。并不会不影响判断逻辑。

大家也可以通过这个示例项目来查看效果。

🏊在React项目中如何实现

适用的依赖版本为:

  • react: 17以上
  • react-router-dom6以上。5可以自行根据下面的思路实现,这里我懒得展示了)

由于react-router缺乏类似vue-router提供的路由守卫函数,因此需要自己写逻辑监听记录路由变化,这需要借助react-router-domuseLocationReact.Context来实现。实现步骤如下所示:

  1. 新建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<Location>();
      // 通过useEffect记录上次数据,useEffect中的函数执行时机为reconcile之后,commit之前,因此LastLocationContext.Provider的value值是lastLocation在(lastLocation.current = location)语句执行前的值。
      useEffect(() => {
        lastLocation.current = location;
      }, [location]);
    
      return (
        <LastLocationContext.Provider value={lastLocation.current}>
          {children}
        </LastLocationContext.Provider>
      );
    };
    
    export default LastLocationProvider;
    

    LastLocationProvider<RouterProvider/>的子组件里被调用,因为useLocation需要在<RouterProvider/>的包裹下才能调用。

  2. 新建useLastLocationhook 用以获取上一次路由,如下所示:

    export function useLastLocation() {
      return useContext(LastLocationContext);
    }
    

    然后放在带有返回功能的组件中调用,如下所示:

    const navigate = useNavigate();
    const lastLocation = useLastLocation();
    
    const back = useCallback(() => {
      if (lastLocation && lastLocation.key !== "default") {
        navigate(-1);
      } else {
        navigate("/table");
      }
    }, []);
    

    大家也可以通过这个示例项目来查看效果。

🏆后记

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