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

相关推荐
To_OC4 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
vipbic4 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy4 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志5 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble6 小时前
c端系统,其实更像一个信息展示平台
前端
李姆斯7 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜7 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司8 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能
kaiking_g8 小时前
vue项目中,静态导入 vs 动态导入 详解
前端·javascript·vue.js
江华森8 小时前
Web 开发基础:HTTP 与 REST
前端·网络协议·http