qiankun微应用调试神器——microDev模式

前言

对于大型前端项目,尤其是中后台项目,微前端架构已经成为越来越广泛的技术选型。笔者所在团队负责公司内部运营平台的开发和维护,大大小小的项目有7、8个。这些项目有运行了5年的"老"项目,也有配合业务需要搭建的新项目。为了统一管理,笔者逐步将这些项目纳入到monorepo单仓中,并配合微前端重构整个平台,解决项目之间独立开发时代码冲突、发布冲突、规范不一致等问题。

在考察了社区优秀的微前端方案后,决定选择qiankun作为平台的微前端化技术选型。微前端应用的好处qiankun官方文档已经说了很多了,这里讲讲它的痛点:

  1. 部署复杂度变大:对微应用的部署有两种模式:容器化部署 or CDN静态部署 ,无论哪种都需要大量额外的配置以便主应用能正确加载(比如开启CORS)。
  2. 微应用加载性能下降:这算是有舍有得吧,毕竟之前只需要加载一个应用,改为微前端后可是要加载主应用+微应用。
  3. 调试麻烦:主应用还好,启动起来就OK,微应用的调试就费劲了,不仅需要启动自己,还得启动主应用。

痛点1、2基本没啥办法,想用微前端就忍忍吧。痛点3不想忍,能不能优化一下调试方式,让微应用可以丝滑地进行调试呢?

方案设想

微应用调试的有三大痛点:

  1. 一些数据需要主应用传递,此时就需要同时启动主应用才能进行调试;
  2. 微应用独立启动时无法判断主、微应用样式是否冲突;
  3. 接口代理比较麻烦

这些痛点有两点是跟主应用相关,最后一点也是主应用存在并解决了的。那能不能利用生产模式(即打包后部署到服务器上进行访问的模式)下的主应用来调试微应用呢?

来看一下主应用里注册微应用的方法:

js 复制代码
// qiankun官网里的示例代码
import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'react app', // app name registered
    entry: '//localhost:7100',
    container: '#yourContainer',
    activeRule: '/yourActiveRule',
  },
  {
    name: 'vue app',
    entry: { scripts: ['//localhost:7100/main.js'] },
    container: '#yourContainer2',
    activeRule: '/yourActiveRule2',
  },
]);

start();

其中entry便是微应用的服务地址,是提前声明的...能不能改为动态声明动态读取

让在生产模式下主应用动态地注册微应用,并且注册的地址可以让开发者自己配置,比如主应用部署域名是www.main-app.com,微应用本地启动地址为localhost:3000。将微应用的本地地址声明为entry的值,是不是就可以让生产模式的主应用加载本地启动的微应用。

思路有了,接下来便是实施!

方案实施

动态注册实现思路

动态注册的第一层含义是微应用信息列表不是写死在代码里的,而是动态读取的。

从哪里读呢?一个store里,该store还得支持按照开发者进行数据隔离,并能自由地读写。再叠加持久化要求,localStorage是个合适的存储方案。

动态注册的第二层含义是要支持开发者自定义微应用entry的值,这个好办,在web页面里开发一个配置入口,让开发者可以自主设置值。

微应用注册表改造

首先需要声明一个默认注册表,为默认模式下微应用的配置信息,并且该注册表在实际返回时可以根据开发者的输入动态修改:

js 复制代码
// micro-app.ts文件
const S_KEY = "QIANKUN_MICRO_DEV_MODE";

export const MICRO_APP_CONTAINER = "#micro-frontend-root";

const defaultMicroAppConfig = [
  {
    name: "react app", // app name registered
    entry: "//localhost:3001",
    container: "#micro-frontend-root",
    activeRule: "/react-app",
  },
  {
    name: "vue app",
    entry: "//localhost:3002",
    container: "#micro-frontend-root",
    activeRule: "/vue-app",
  },
];

const getFinalMicroAppConfig = () => {
  const storage = window.localStorage;
  try {
    const itemStr = storage.getItem(S_KEY) ?? "";
    let config: any;
    if (itemStr) {
      config = JSON.parse(itemStr);
    }
    if (typeof config === "object") {
      return defaultMicroAppConfig.map((e) => {
        const appName = e.name ?? "";
        const devConfig = config?.[appName] ?? {};
        if (devConfig.devSwitch) {
          return {
            ...e,
            entry: devConfig.url ?? e.entry,
          };
        }
        return e;
      });
    }
    return defaultMicroAppConfig;
  } catch (e: any) {
    console.error(e?.message);
    return defaultMicroAppConfig;
  }
};

// TODO 生产环境可选直接返回defaultMicroAppConfig, 屏蔽microDev模式
const FINAL_MICRO_APP_CONFIG = getFinalMicroAppConfig();

export default FINAL_MICRO_APP_CONFIG;

上面的代码逻辑很简单,就是在返回defaultMicroAppConfig前判断localStorage里有没有自定义的微应用配置,如果有则用自定义配置替换掉默认的配置。

动态配置微应用弹框

为了方便开发者配置微应用入口,需要在主应用里提供一个弹框组件,让开发者以可视化的方式设置要调试的微应用entry:

js 复制代码
import React, { useState, useEffect, useMemo, useReducer, useCallback } from 'react';
import { Dialog } from 'tdesign-react';
import { useKeyPress } from 'ahooks';
import { handleKeyCode } from './utils';
import MicroFeTable from './components/micro-fe-table';
import MicroFePopup from './components/micro-fe-popup';
import { Provider, contextReducer, defaultContext, ACTION } from './context';
import useMicroDevData, { S_KEY } from './hooks/use-micro-dev-data';

export { S_KEY };

/**
 * 微前端联调模式组件
 * @returns
 */
export default function MicroDevMode(): React.ReactElement {
  const [visible, setVisible] = useState(false);
  const [data,,saveData] = useMicroDevData();
  const [currentContext, dispatchContext] = useReducer(contextReducer, defaultContext);

  const microDevApp = useMemo(() => currentContext?.configData?.filter(e => e.devSwitch), [currentContext?.configData]);

  useEffect(() => {
    (window as any).microDev = (open: any = undefined) => {
      setVisible(open === undefined ? true : Boolean(open));
    };
  }, []);

  useEffect(() => {
    if (data) {
      dispatchContext({
        type: ACTION.SAVE_CONFIG_DATA,
        data,
      });
    }
  }, [data]);

  useKeyPress(
    handleKeyCode('KeyM'),
    () => {
      setVisible(prev => !prev);
    },
  );

  const closeModal = useCallback(() => {
    setVisible(false);
  }, []);

  const openModal = useCallback(() => {
    setVisible(true);
  }, []);

  const onConfirm = () => {
    const { configData } = currentContext;
    if (configData) {
      saveData(configData);
    }
    window.location.reload();
    closeModal();
  };

  const onCancel = () => {
    closeModal();
  };
  return (
    <Provider value={{ ...currentContext, modalVisible: visible, dispatchContext }}>
      {microDevApp?.length ? <MicroFePopup count={microDevApp?.length} onClick={openModal} /> : null}
      <Dialog
        header="Micro Dev"
        width={800}
        visible={visible}
        confirmOnEnter
        onConfirm={onConfirm}
        onCancel={onCancel}
        onEscKeydown={onCancel}
        onCloseBtnClick={onCancel}
        onOverlayClick={onCancel}
      >
        <MicroFeTable />
      </Dialog>
    </Provider>
  );
}

上面列出了主入口代码,用一个Dialog展示配置列表,并且提供两种方式弹出该配置框:

  1. 在浏览器Console调用microDev()
  2. 快捷键:ctrl+shift(macos系统下为option键)+M

弹框效果: 上面的应用状态是起到提示效果,让开发者确认当前服务状态,以防没有启动本地服务就修改了配置,造成微应用加载失败。

如何检测服务健康度

很简单,在浏览器端发起一次http嗅探请求,当请求响应码为200时,应用状态正常,否则异常

js 复制代码
fetch(url)
  .then(({ statusText, status }) => {
    if (status === 200 && statusText === "OK") {
      setTagMeta({
        theme: "success",
        text: "正常",
      });
    } else {
      setTagMeta({
        theme: "danger",
        text: "异常",
      });
    }
  })
  .catch(() => {
    setTagMeta({
      theme: "danger",
      text: "异常",
    });
  });

如何让动态配置生效

一般来说,主应用注册微应用的时机是在应用入口。所以为了更新微应用的配置,只能重新加载页面:

js 复制代码
const onConfirm = () => {
  const { configData } = currentContext;
  if (configData) {
    // 保存配置到localStorage
    saveData(configData);
  }
  // 重新加载页面
  window.location.reload();
  closeModal();
};

效果

为了演示本地联调效果,故意将微应用初始entry设置一个不存在的服务,然后通过本地联调模式调用localhost:3001地址下的微应用,页面刷新后,微应用被正确加载:

总结

本文通过设计microDev模式,解决了微前端框架微应用调试的痛点,需要注意的是,该模式存在一定安全风险,最好在非正式环境使用(比如内部测试环境)

附录

本文的demo项目代码已上传到github,需要完整代码的读者可以自行clone:qiankun-micro-dev

相关推荐
Lysun0018 分钟前
[less] Operation on an invalid type
前端·vue·less·sass·scss
J总裁的小芒果24 分钟前
Vue3 el-table 默认选中 传入的数组
前端·javascript·elementui·typescript
Lei_zhen9626 分钟前
记录一次electron-builder报错ENOENT: no such file or directory, rename xxxx的问题
前端·javascript·electron
咖喱鱼蛋29 分钟前
Electron一些概念理解
前端·javascript·electron
yqcoder30 分钟前
Vue3 + Vite + Electron + TS 项目构建
前端·javascript·vue.js
鑫宝Code1 小时前
【React】React Router:深入理解前端路由的工作原理
前端·react.js·前端框架
Mr_Xuhhh2 小时前
重生之我在学环境变量
linux·运维·服务器·前端·chrome·算法
永乐春秋3 小时前
WEB攻防-通用漏洞&文件上传&js验证&mime&user.ini&语言特性
前端
鸽鸽程序猿3 小时前
【前端】CSS
前端·css
ggdpzhk3 小时前
VUE:基于MVVN的前端js框架
前端·javascript·vue.js