qiankun上线稳定,问题解决汇总

2,857 阅读4分钟

接入qiankun,项目上线也有段时间了,记录一下遇到的问题。

1. 子应用更新路由,父应用菜单高亮不匹配问题

子应用 history.push 之后,父应用的菜单还停留在之前的菜单高亮,但是页面内容又是新的路由内容,url 也是新的。导致菜单高亮不正确。

解决办法:

import { useEffect } from 'react';
import { history } from 'umi';

// 处理子应用push操作,父应用的菜单高亮不正确BUG
export function useBrowserHistory() {
  useEffect(() => {
    const handlePopState = (event: any) => {
      const { href } = event.target.location; // eg: http://localhost:1200/#/bps/producMange/imageAlbum?a=b
      const pathNameWithSearch = href.split('#')[1]; // eg: /bps/producMange/imageAlbum?a=b
      const pathname = pathNameWithSearch.split('?')[0];
      console.group('---handlePopState---');
      console.log('history.pathname-->', history.location.pathname);
      console.log('event=>pathname-->', pathname);
      console.log('event=>pathNameWithSearch-->', pathNameWithSearch);
      console.groupEnd();
      // 如果2个不一样,说明是子应用自己push的路由
      if (history.location.pathname !== pathname) {
        history.replace(pathNameWithSearch);
      }
    };
    window.addEventListener('popstate', handlePopState);
    return () => {
      window.removeEventListener('popstate', handlePopState);
    };
  }, []);
}

2. 子应用页面级路由拦截在接入乾坤后不生效,如何处理。

子应用要实现的是表单页面离开前如果有修改表单要询问是否确定要离开,子应用是 vue2 应用,使用 routeBeforeLeave 可以容易实现,独立运行时没有问题。

现在接入我们平台后,菜单是属于父应用的内容,点击后已经去了新的路由,新的页面,那个 routeBeforeLeave 虽然会触发,但是提示弹窗不出现,交互效果也不对。

1 现在改造父应用的菜单,声明式改为命令式

<Menu.Item key={item.id}>
-  <Link to={item.path}>{item.name}</Link>
+  <div onClick={() => handleClickRoute(item.path)}>{item.name}</div>
</Menu.Item>

2 自己处理点击的逻辑

  // NEED_HISTORY_BLOCK_ROUTE 配置的是需要拦截的url
  const handleClickRoute = async (path: string) => {
    console.log(needHistoryBlock, path, location.pathname);
    const isNeedBlockRoute = NEED_HISTORY_BLOCK_ROUTE.includes(
      location.pathname,
    );
    if (isNeedBlockRoute) {
      actions.setGlobalState({
        routeChange: true,
      });
      // 延迟500ms,等待子应用通信过来,数据有没有改变,是否需要拦截路由
      await delay(500);
      if (needHistoryBlock) {
        Modal.confirm({
          title: '提示',
          content: '修改的信息没有保存,现在离开,设置不会保存',
          okText: '确认',
          cancelText: '取消',
          onOk: () => {
            actions.setGlobalState({
              needHistoryBlock: false,
              routeChange: false,
            });
            setNeedHistoryBlock(false);
            history.push(path);
          },
          onCancel: () => {},
        });
      } else {
        history.push(path);
      }
    } else {
      history.push(path);
    }
  };

父应用在点击菜单时先把 routeChange 改为 true,然后子应用监听,如果表单有更改,则通信告诉我设置 needHistoryBlocktrue,如果没有更改则啥都不用处理。

父应用监听状态变化,如果 needHistoryBlocktrue,则设置值,设置到全局变量里面。

if (state.needHistoryBlock) {
    setNeedHistoryBlock(true);
    return;
 }

handleClickRoute 里面会延迟 100ms 去监听这个值 needHistoryBlock,如果为 true则给出弹窗,如果【确认】要离开则先把2个状态设置为false,然后 history.push`。如果【取消】什么都不用做。

如果觉得这样写不好,父应用不应该处理子应用的逻辑,那就在子应用听到消息后,自己弹出弹窗,然后在用户点击【确定】或【取消】后,自己执行逻辑,然后通信给父应用要不要去新的路由。也是可以的。【我们因为目前只有这个页面需要这个操作,就先这样简单处理了,嘻嘻~~】

3. 同域名下多环境登录问题

核心思想是在 WebStorage 存储时,把 key 增加 环境 信息。详细代码如下(下面代码因为多个视角都要改成父应用,所以还有额外的多平台配置):

文件路径:.src/utils/BapStore.ts

/** WebStorage 类型 */
enum StoreType {
  'local',
  'session',
}

/** 视角前缀 */
enum Prefix {
  'platform' = 'bspPlatform',
  'tenant' = 'bspTenant',
  'user' = 'bspUser',
}

/**
 * 命名空间:平台视角:父应用为 `bspPlatform$`,子应用为 `bspPlatform-${name}`
 * 命名空间:租户视角:父应用为 `bspTenant$`,子应用为 `bspTenant-${name}`
 * 命名空间:用户视角:父应用为 `bspUser$`,子应用为 `bspUser-${name}`
 * 前缀为 `命名空间_环境_key`,
 * 不考虑过期时间(因为用不到),如果要短期过期请使用 sessionStorage 实例
 * #开头的属性表示私有属性,隐藏内部实现且不暴露给外界
 */
class BapStore {
  /** 命名空间,一般取项目的英文简写,通常从package.json中取 */
  #namespace: string;
  /** 当前环境,目前有 local, dev, test, pre, pro 5种 */
  #env: string;
  /** 实际用的Storage */
  #store: Storage;
  #prefix: string;
  constructor(namespace: string, env: string, type: StoreType) {
    this.#namespace = namespace;
    this.#env = env;
    this.#store =
      type === StoreType.local ? window.localStorage : window.sessionStorage;
    this.#prefix = `${this.#namespace}_${this.#env}_`;
  }

  set(key: string, val: any) {
    const k = `${this.#prefix}${key}`;
    if (val === undefined || val === null) {
      this.#store.removeItem(k);
    } else {
      const v = JSON.stringify(val);
      this.#store.setItem(k, v);
    }
  }
  get(key: string) {
    const k = `${this.#prefix}${key}`;
    const v = this.#store.getItem(k);
    if (v === null) {
      return null;
    } else {
      return JSON.parse(v);
    }
  }
  has(key: string) {
    const k = `${this.#prefix}${key}`;
    const v = this.#store.getItem(k);
    return v !== null;
  }
  remove(key: string) {
    const k = `${this.#prefix}${key}`;
    const v = this.#store.getItem(k);
    if (v === null) {
      console.warn(`不存在key为${key}的存储项`);
    } else {
      this.#store.removeItem(k);
    }
  }
  /** 仅清空自己前缀的key */
  clear() {
    // 注意这里不能边删边查,会导致BUG;要一次性查出来,然后删除
    /** 包含我的前缀的数组 */
    const myStoreArr = Array.from(
      { length: this.#store.length },
      (_, i) => this.#store.key(i) as string,
    ).filter((str) => str?.startsWith(this.#prefix));
    myStoreArr.forEach((str) => this.#store.removeItem(str));
  }
}

/** 父应用类似`bspPlatform$`,子应用类似 `bspPlatform-${name}` */
const namespace = `${Prefix.tenant}$`;

export const localStore = new BapStore(
  namespace,
  REACT_APP_ENV,
  StoreType.local,
);
export const sessionStore = new BapStore(
  namespace,
  REACT_APP_ENV,
  StoreType.session,
);

4. css样式污染,见另外一篇文章

umi插件解决难题

5. 播放器不可用,同上见另外一篇文章

umi插件解决难题

6. 切换应用时,前一个应用的 message 弹窗一直存在,永不消失

切换应用时,前一个应用的弹窗应当全部销毁。unmount做处理。

export async function unmount () {
   console.log(`%c[${name} unmount 函数]: `, 'color:green;')
   // ... 其他正常逻辑
+  // 子应用 unmount 时要 message.destroy() 等
+  message.destroy()
+  notification.destroy()
}

7. 子应用更细代码后,在父应用里面刷新还是旧代码

nginx 配置 Cache-Controlno-cache即可。

user root;
worker_processes 2;

events {
  worker_connections 65535;
}

http {
  include mime.types;
  default_type application/octet-stream;
  sendfile on;
  keepalive_timeout 100;
  client_max_body_size 20M;
  server {
    listen 8000;
    server_name localhost;
    charset utf-8;
    location / {
+     # 子应用务必记得加`no-cache`,否则发版后代码不更新!
+     add_header Cache-Control no-cache;
      alias /home/dist/;
      index index.html index.htm;
      try_files $uri $uri/ /index.html;
    }
    error_page 405 =200 $uri;
  }
}