likes
comments
collection
share

[React防护盾]:如何通过页面路由拦截保护用户免受误操作的伤害?

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

有意思的需求又来啦~

[React防护盾]:如何通过页面路由拦截保护用户免受误操作的伤害?

需求描述: 当前存在一个页面级的复杂表单,当用户在未保存表单数据的情况下,就想去浏览其他页面。为了提高用户体验,需要增加一个二次确认的弹窗,提示用户是否确认在未保存数据的情况下,继续执行跳转的操作。如果确认,继续执行用户想要的操作,取消则停留在当前位置。

方法1: Prompt

目前用得最多、也是最简单的方法就是使用react-router 提供的Prompt组件。 Prompt可用于在用户导航离开当前页面时显示一个提示或确认对话框,可以用来阻止用户无意中离开未保存的表单或数据。

他接收两个参数:

  • when:boolean (用于控制Prompt组件的显示与隐藏)
  • message:(location: Location, action: Action) => string | boolean | void;(定义在用户尝试导航离开当前页面时显示的提示文本或回调函数)。
    • location:表示导航的目标位置,包含了目标URL和其他导航信息。
    • action:表示导航的动作类型,可以是PUSHREPLACEPOP之一。

示例

<Prompt when={true} message="确定要离开该页面吗?" />

我们还可以通过getUserConfirmation属性来自定义确认对话框的显示方式。默认情况下,它使用的是浏览器默认的对话框。如果想要使用自定义的对话框组件或其他交互方式,可以提供一个自定义的getUserConfirmation函数。

import { Prompt } from 'react-router-dom';

function MyFormComponent() {
  const customConfirmation = (message, callback) => {
    // 自定义的对话框逻辑
    const isConfirmed = window.confirm(message);
    callback(isConfirmed);
  };

  return (
    <div>
      <Prompt
        message="确定要离开该页面吗?"
        getUserConfirmation={customConfirmation}
      />
      {/* 其他表单组件和逻辑 */}
    </div>
  );
}

如果还是有点不清晰,可以直接看看这个例子~

在线demo

总的来说,Prompt实现了我们想要的需求,但他也存在不足,首先不得不说,他这个提示的弹窗真的有点丑,大部分情况下,都得我们自定义去实现一个拦截的弹窗,时间成本就会上来了。还有最最最最重要的问题是,Prompt在v6已经不支持啦!!!具体请查看官方文档why? 真的太让人悲伤了,只能默默将路由回退到v6之前,或者重新找一个新方法了啊,呜呜呜呜呜

如果你的项目刚好是v6以下的,也可以再给大家推荐一个方法~

方法2: 利用 history 实现路由拦截

history是由 React Router 库提供的一种管理导航历史的方式。它可以用于在 React 应用中执行导航操作、管理路由状态以及与浏览器历史记录进行交互。

api应用场景
history.push(path, [state])将新的路径推送到历史堆栈中,导航到指定的路径,并可选择性地传递状态对象
history.replace(path, [state])替换当前路径,导航到指定的路径,并可选择性地传递状态对象
history.goBack()导航回上一个页面,等效于用户点击浏览器的“返回”按钮
history.goForward()导航到下一个页面,等效于用户点击浏览器的“前进”按钮
history.listen(listener)注册一个监听器,用于监听导航的变化,当导航发生时执行回调函数
history.location表示当前的位置对象,包含当前的路径、搜索参数和哈希值等信息
history.block(prompt)设置一个拦截器,当用户尝试导航离开页面时执行回调函数,并可选择性地返回一个提示文本

没错!我们这次用的就是history.block

核心要点:

  • 自定义进行拦截的逻辑
  • 在组件卸载的时候取消路由拦截

import { useHistory } from 'react-router-dom';

function MyComponent() {
  const history = useHistory();

  useEffect(() => {
    const unblock = history.block((location, action) => {
      // 执行你的自定义逻辑
      if (!shouldAllowNavigation()) {
        return '确定要离开该页面吗?';
      }
    });

    return () => {
      unblock(); // 在组件卸载时取消拦截
    };
  }, [history]);

  return (
    // 组件内容
  );
}

在线示例

代码解析:

  • opened:控制确认对话框是否打开的状态。
  • unblock:用于存储 history.block 返回的取消拦截的函数。
  • newUrl:表示新的目标路径。
  • action:表示导航的动作类型。 看起来有点抽象,但是看看代码你就懂了。总的来说,就是利用了history.block返回的路由信息,以及路由状态进行判断比较,对路由进行控制,最终做出路由拦截的效果。

最后给大家推荐一个非常好用的方法,他支持v6以上版本喔~~

方法3 使用 unstable_Blocker 进行路由拦截

在 React Router v6 中,为了更好地支持路由导航的拦截和控制,unstable_Blocker被引入作为一个实验性的组件。所以我们会发现在官方文档里面找不到他的踪迹,感兴趣的小伙伴可以查看源码例子:

unstable_Blocker源码例子

同时需要注意的是,由于 unstable_Blocker是一个实验性的功能,它的 API 和行为可能在未来的版本中发生变化。因此,建议在使用时查看官方文档或相关资源,以了解最新的使用方式和注意事项~

核心代码

export function usePrompt(props: PromptProps) {
  const { when = false, message, customFn } = props;
  let blocker = useBlocker(when);

  useEffect(() => {
    if (!when || blocker.state === 'blocked') {
      blocker.reset?.();
    }
  }, []);

  useEffect(() => {
    async function shouldBlock() {
      if (blocker.state === 'blocked') {
        let proceed = false;
        // 直接传入 message 显示
        if (message) {
          proceed = window.confirm(message);
          if (proceed) {
            setTimeout(blocker.proceed, 0);
          } else {
            blocker.reset();
          }
        } else {
          // 返回一个 Promise 对象,便于后期自定义确认框
          new Promise((resolve, reject) => {
            // @ts-ignore
            customFn(resolve, reject);
          })
            // @ts-ignore
            .then(() => blocker.proceed())
            // @ts-ignore
            .catch(() => blocker.reset());
        }
      }
    }
    shouldBlock();
  }, [blocker, message]);
}

代码解析:

  • 接收参数

    • when: boolean; (用于控制显示拦截界面或执行相关操作)

    • message?: string; (自定义拦截的提示信息)

    • customFn: (resolve: () => void, reject: () => void) => void;(控制拦截操作的回调,我们对拦截弹窗的自定义也会变得非常方便)

  • 核心思想

    1. 通过判断blocker.state 拦截器状态来确认是否进行下一步路由跳转操作。

    • blocker.state 主要包括以下 4 种状态:
      1. uninitialized:表示拦截器尚未初始化,还没有执行任何拦截操作。
      2. blocked:表示拦截器已被阻止,即路由导航操作被拦截,不能继续进行。
      3. proceeded:表示拦截器已被继续,即路由导航操作被允许继续进行。
      4. resolved:表示拦截器已解决,即拦截操作已经完成。

    2. 返回一个 Promise 对象,自定义执行用户的确认、取消操作,利用blocker.proceed以及blocker.reset来继续执行被阻塞的操作或者重置阻塞状态和清除任何未完成的异步操作。

👏🏻👏🏻👏🏻路由拦截的功能就完成啦!!!看起来也很简单对不对,但这里还可以再优化一下~

chrome 为例,页面在卸载阶段会触发 beforeunload 事件监听器,它允许我们在用户离开页面之前执行一些操作或提供一些提示。 然而,由于 beforeunload 事件会在每次页面刷新、导航或关闭时触发,如果没有进行适当的优化,可能就会导致性能问题。

为了避免性能问题,我们可以自定义控制开启 beforeunload 监听器的方式。也就是说只有在特定条件下,例如表单中存在已更改、但未保存的数据或其他需要提示的情况下,才去添加 beforeunload 监听器,以避免不必要的事件处理。

很可惜,在查阅React Router过程中发现,对于路由拦截等需要使用 beforeunload 的场景,React Router 并没有提供专门的钩子函数。

所以我们需要自行实现这样的逻辑,请看代码:

function useBeforeUnload(doBlock?: boolean) {
  useEffect(() => {
    const blockCallback = (e: BeforeUnloadEvent) => {
      if (doBlock) {
        e.preventDefault();
        return (e.returnValue = '');
      }
    };

    // 只有开启 block 才会启用 beforeunload 监听事件,优化性能
    if (doBlock) {
      window.addEventListener('beforeunload', blockCallback);
      return () => window.removeEventListener('beforeunload', blockCallback);
    }
  }, [doBlock]);
}

useBeforeUnload监听 beforeUnload 事件,这个主要能监听到 a href 前进后退等这种跳转 browser navigation,同时调用 preventDefault 再把 returnValue 设置为空字符串,以确保标记取消了 beforeunload 事件;

最后,我们只需要在使用usePrompt同时加上监听器的优化就完成啦🎉🎉

export default function useCustomPrompt(props: PromptProps) {
  const { when = false, message, customFn } = props;
  useBeforeUnload(when);
  usePrompt({ when, message, customFn });
}

最后附上在线实战demo:路由拦截demo

好了,本文完毕,希望对你有所帮助✌🏻

如果还有疑问或者觉得文章存在不足,欢迎多多交流指正哟~

转载自:https://juejin.cn/post/7241408775718682684
评论
请登录