qiankun框架下主项目和子项目路由拦截

262 阅读3分钟

前言

qiankun目前没有看到有直接的控制主项目和子项目一同路由拦截的场景,比如,如果你在子项目中有个页面的编辑没有被提交,然后你想跳转到其它项目页面或者主项目的路由页面

此时,产品经理告诉你,需要增加一个拦截跳转的需求,然后你兴致冲冲的加了React-router4支持的Prompt组件,你尝试了一下可以拦截跳转,页面不变,但是主项目的路由怎么变了(umi的路由),而且当你取消跳转的时候,此时对应的菜单左侧路由高亮也对不上了。

这时候,你会发现这种做法有问题

实现

通过查看react-router的源码,发现实现路由跳转的实际上是由一个history的库来实现的。以及Prompt的原理也是来自于history中的block方法

history是一个对象,内部包含了一些状态,所以,当在外部调用的时候,它实际上相当于一个有着内部状态的闭包函数。当引入react-router中的Router组件的时候,会初始化一个history对象,通常最常用的是browser router

这时候,生成的history对象,可以通过调用history.push等方法渲染不同路由对应的组件。这里我们讲一下history.block

history block实现

  1. 这里不放源码了,当执行history.push()或者history.back()时候,会查看有没有执行的block,通常会内部储存一个block列表,如果列表中有值,说明当前页面被block掉,执行push或者back,会直接被忽略掉,所以这也是拦截的主要操作

  2. 当满足条件时候,清理block列表中的值,此时在push或者back的时候,可以直接执行。

qiankun框架下,主项目和子项目如何同时block?

从上文我们知道,主项目的history和子项目的history不是同一个闭包函数,如果想要同时block子项目和主项目,那么就需要同时处理两边的数据

所以这里采用了传递主项目的block方法,按自己的需要执行block 源码如下

import { Modal } from 'antd';
import { useEffect, useRef } from 'react';
import { history } from 'umi';
const { confirm } = Modal;

interface PromptType {
  when: boolean;
  title?: string; // words about the prompt to leave
  onCallback?: () => void;
}
const fatherHistory = 'main_project_history'; // it is the definition of window?.main_project_history in the main project
let flag = true;
let timer: any = null;
const _location = location;
const modalConfirm = (title: any, onOk: () => void) => {
  if (flag) {
    flag = false;
    clearTimeout(timer);
    timer = setTimeout(() => {
      flag = true;
      confirm({
        title: title || 'Are you sure you want to leave?',
        okText: 'yes',
        cancelText: 'no',
        onOk,
      });
    }, 50);
  }
};

// there is a secene that could be not support, that is when you are in a subproject, you pushstate in a subproject new page, then you back the history, that could cause a problem
function usePrompt({ when = false, title, onCallback }: PromptType) {
  const unblockOutRef = useRef<any>(null);
  const unblockInnerRef = useRef<any>(null);
  const recordCount = useRef<any>(0);
  useEffect(() => {
    function runValidateBeforeLeave(__history: any, type: string) {
      const isLatestHistory = __history?.back;
      try {
        if (!when && isLatestHistory) return; // the realization is different from react-router V5
        /**
         * set 'when' true, it will generate two blocks, one is by son project, the other one is father block.
         * blocker is like a listener, every time you pushstate, it will trigger the block
         * */
        return __history.block((data: any) => {
          let location = data.location ? data.location : data; // cause react-router updated
          if (when) {
            modalConfirm(title, () => {
              unblockOutRef.current?.(); // remove blocker
              unblockInnerRef.current?.(); // remove blocker
              if (location.pathname === _location.pathname) {
                recordCount.current += 1;
                // when you are in a son page, then you click the son page link to another page, you back history, it will cause two back history(father and son)
                if (recordCount.current > 1) return;
                __history.go(-1);
              } else {
                __history.push(location.pathname);
              }
              onCallback?.();
            });
          }
          if (!when && !isLatestHistory) return true;
          return false;
        });
      } catch (error) {
        console.log('error', error);
      }
    }

    // micro main project to prevent leave
    unblockOutRef.current = runValidateBeforeLeave(window?.[fatherHistory], 'father');
    // this project to prevent leave
    unblockInnerRef.current = runValidateBeforeLeave(history, 'son');

    return () => {
      unblockOutRef.current?.(); // remove blocker
      unblockInnerRef.current?.(); // remove blocker
    };
  }, [when, title]);
}

export default usePrompt;