前言
用户在编辑表单时,经常编辑一半中途切换页面,我们需要进行路由拦截,对用户进行询问,是否需要保存当前信息。
React中实现路由拦截使用useBlocker就可以了。本文将使用高阶组件对路由拦截进行封装来扩展功能。
- 最基本的路由拦截,默认拦截弹窗
 - 自定义控制是否需要拦截
 - 自定义弹窗取消和确定事件
 - 自定义弹窗样式
 
后面有代码仓库地址
正文
基本的路由拦截
编写一个WithBlocker来控制路由拦截,在需要拦截的页面使用就行
            
            
              tsx
              
              
            
          
          import { Modal } from 'antd';
import React, { useState, useCallback, useEffect, useRef } from 'react';
import { useBlocker } from 'react-router-dom';
const WithBlocker = (WrappedComponent: React.ComponentType<any>) => {
  return (props: any) => {
    /** 是否出现弹窗*/
    const [isOpen, setIsOpen] = useState(false);
    const blocker: any = useBlocker(
      ({ currentLocation, nextLocation }) => currentLocation.pathname !== nextLocation.pathname
    );
    useEffect(() => {
      if (blocker.state === 'blocked') {
        setIsOpen(true);
      } else {
        setIsOpen(false);
      }
    }, [blocker]);
    return (
      <>
        <WrappedComponent {...props} />
        <Modal
          title="默认弹窗"
          open={isOpen}
          onCancel={() => {
            setIsOpen(false);
            blocker.reset();
          }}
          onOk={() => {
            setIsOpen(false);
            blocker.proceed();
          }}
        >
          <div>是否退出</div>
        </Modal>
      </>
    );
  };
};
export default WithBlocker;
        这就是最基本,只要离开页面就拦截。
            
            
              tsx
              
              
            
          
          import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import WithBlocker from '../../feature/RouteInterception/WithBlocker';
const Index = () => {
  return <div>home</div>;
};
export default WithBlocker(Index);
        自定义控制是否需要拦截
我们只要控制useBlocker里面函数的返回值就可以控制是否拦截,true拦截,false不拦截。
            
            
              typescript
              
              
            
          
          const blocker: any = useBlocker( ({ currentLocation, nextLocation }) => currentLocation.pathname !== nextLocation.pathname );
        使用isIntercept开控制,是否要进行路由拦截
            
            
              tsx
              
              
            
          
          import { Modal } from 'antd';
import React, { useState, useCallback, useEffect, useRef } from 'react';
import { useBlocker } from 'react-router-dom';
const WithBlocker = (WrappedComponent: React.ComponentType<any>) => {
  return (props: any) => {
    /** 是否出现弹窗*/
    const [isOpen, setIsOpen] = useState(false);
    /** 是否拦截路由*/
    const [isIntercept, setIsIntercept] = useState(true);
    const blocker: any = useBlocker(
      ({ currentLocation, nextLocation }) => isIntercept && currentLocation.pathname !== nextLocation.pathname
    );
    useEffect(() => {
      if (blocker.state === 'blocked') {
        setIsOpen(true);
      } else {
        setIsOpen(false);
      }
    }, [blocker]);
    return (
      <>
        <WrappedComponent {...props} setIsIntercept={setIsIntercept} />
        <Modal
          title="默认弹窗"
          open={isOpen}
          onCancel={() => {
            setIsOpen(false);
            blocker.reset();
          }}
          onOk={() => {
            setIsOpen(false);
            blocker.proceed();
          }}
        >
          <div>是否退出</div>
        </Modal>
      </>
    );
  };
};
export default WithBlocker;
        使用场景:修改一个表单,没有发生改变可以直接退出,有修改进行询问。
            
            
              tsx
              
              
            
          
          import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import WithBlocker from '../../feature/RouteInterception/WithBlocker';
import { Modal } from 'antd';
const Index = (data: any) => {
  const {  setIsIntercept } = data;
  useEffect(() => {
    setIsIntercept(false);
  }, []);
  return <div>home</div>;
};
export default WithBlocker(Index);
        自定义弹窗取消和确定事件
需要自定义事件,我们使用的是高阶组件,事件需要从子元素传递给父元素,那就要使用useImperativeHandle这个hook。
            
            
              tsx
              
              
            
          
          import { Modal } from 'antd';
import React, { useState, useCallback, useEffect, useRef } from 'react';
import { useBlocker } from 'react-router-dom';
const WithBlocker = (WrappedComponent: React.ComponentType<any>) => {
  return (props: any) => {
    const childRef = useRef<{
      proceed: () => Promise<void>;
      reset: () => Promise<void>;
    }>();
    /** 是否出现弹窗*/
    const [isOpen, setIsOpen] = useState(false);
    /** 是否拦截路由*/
    const [isIntercept, setIsIntercept] = useState(true);
    const blocker: any = useBlocker(
      ({ currentLocation, nextLocation }) => isIntercept && currentLocation.pathname !== nextLocation.pathname
    );
    useEffect(() => {
      if (blocker.state === 'blocked') {
        setIsOpen(true);
      } else {
        setIsOpen(false);
      }
    }, [blocker]);
    return (
      <>
        <WrappedComponent ref={childRef} {...props} isOpen={isOpen} setIsIntercept={setIsIntercept} />
        {isIntercept && (
          <Modal
            title="默认弹窗"
            open={isOpen}
            onCancel={() => {
              if (childRef.current) {
                childRef.current
                  .reset()
                  .then(() => {
                    setIsOpen(false);
                    blocker.reset();
                  })
                  .catch(() => {
                    setIsOpen(false);
                    blocker.reset();
                  });
                return;
              }
              setIsOpen(false);
              blocker.reset();
            }}
            onOk={() => {
              if (childRef.current) {
                childRef.current
                  .proceed()
                  .then(() => {
                    setIsOpen(false);
                    blocker.proceed();
                  })
                  .catch(() => {
                    setIsOpen(false);
                    blocker.reset();
                  });
                return;
              }
              setIsOpen(false);
              blocker.proceed();
            }}
          >
            <div>是否退出</div>
          </Modal>
        )}
      </>
    );
  };
};
export default WithBlocker;
        使用场景,往往修改完表格退出点击确认,需要调用保存接口,我们使用Promise就可以在请求成功或者失败的时候进行放行和拦截。
            
            
              tsx
              
              
            
          
          import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import WithBlocker from '../../feature/RouteInterception/WithBlocker';
import { Modal } from 'antd';
const Index = forwardRef((data: any, ref: any) => {
  const {  setIsIntercept } = data;
  useImperativeHandle(ref, () => ({
    proceed: () => {
      return new Promise((resolve, reject) => {
        //dosomething
        resolve(true);
      });
    },
    reset: () => {
      return new Promise((resolve, reject) => {
        //dosomething
        resolve(true);
      });
    },
  }));
  return <div>home</div>;
});
export default WithBlocker(Index);
        自定义弹窗样式
使用isCustomModule来标记是否需要自定义弹窗,blockerComponent来设置自定义的弹窗组件。
            
            
              tsx
              
              
            
          
          import { Modal } from 'antd';
import React, { useState, useCallback, useEffect, useRef } from 'react';
import { useBlocker } from 'react-router-dom';
const WithBlocker = (WrappedComponent: React.ComponentType<any>) => {
  return (props: any) => {
    const childRef = useRef<{
      proceed: () => Promise<void>;
      reset: () => Promise<void>;
    }>();
    /** 是否出现弹窗*/
    const [isOpen, setIsOpen] = useState(false);
    /** 是否拦截路由*/
    const [isIntercept, setIsIntercept] = useState(true);
    /** 是否使用自定义弹窗*/
    const [isCustomModule, setIsCustomModule] = useState(false);
    const blocker: any = useBlocker(
      ({ currentLocation, nextLocation }) => isIntercept && currentLocation.pathname !== nextLocation.pathname
    );
    /** 自定义弹窗的组件*/
    const [blockerComponent, setBlockerComponent] = useState<any>();
    /** 设置自定义弹窗*/
    const setInterceptionModule = useCallback(
      (Component: React.ComponentType<any>) => {
        if (!isCustomModule) {
          return;
        }
        setBlockerComponent(() => {
          return <Component />;
        });
      },
      [isCustomModule]
    );
    useEffect(() => {
      if (blocker.state === 'blocked') {
        setIsOpen(true);
      } else {
        setIsOpen(false);
      }
    }, [blocker]);
    return (
      <>
        <WrappedComponent
          ref={childRef}
          {...props}
          setIsCustomModule={setIsCustomModule}
          blocker={blocker}
          isOpen={isOpen}
          setInterceptionModule={setInterceptionModule}
          setIsBlocking={setIsOpen}
          setIsIntercept={setIsIntercept}
        />
        {isCustomModule && isIntercept && blockerComponent}
        {!isCustomModule && isIntercept && (
          <Modal
            title="默认弹窗"
            open={isOpen}
            onCancel={() => {
              if (childRef.current) {
                childRef.current
                  .reset()
                  .then(() => {
                    setIsOpen(false);
                    blocker.reset();
                  })
                  .catch(() => {
                    setIsOpen(false);
                    blocker.reset();
                  });
                return;
              }
              setIsOpen(false);
              blocker.reset();
            }}
            onOk={() => {
              if (childRef.current) {
                childRef.current
                  .proceed()
                  .then(() => {
                    setIsOpen(false);
                    blocker.proceed();
                  })
                  .catch(() => {
                    setIsOpen(false);
                    blocker.reset();
                  });
                return;
              }
              setIsOpen(false);
              blocker.proceed();
            }}
          >
            <div>是否退出</div>
          </Modal>
        )}
      </>
    );
  };
};
export default WithBlocker;
        使用
            
            
              tsx
              
              
            
          
          import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import WithBlocker from '../../feature/RouteInterception/WithBlocker';
import { Modal } from 'antd';
const Index = forwardRef((data: any, ref: any) => {
  const { blocker, isOpen, setInterceptionModule, setIsCustomModule, setIsOpen } = data;
  useEffect(() => {
    setIsCustomModule(true);
    setInterceptionModule(() => {
      return (
        <Modal
          title="自定义弹窗"
          open={isOpen}
          onCancel={() => {
            setIsOpen(false);
            blocker.reset();
          }}
          onOk={() => {
            setIsOpen(false);
            blocker.proceed();
          }}
        >
          <div>sadasd</div>
          <div>sadasd</div>
        </Modal>
      );
    });
  }, [isOpen, blocker]);
  return <div>home</div>;
});
export default WithBlocker(Index);
        结语
感兴趣的可以去试试,有更好的写法可以在评论区说说