qiankun官网:https://qiankun.umijs.org/zh/guide

微前端是什么?

随着业务的复杂程度越来越高,我们的项目也变的越来越大,越来越臃肿,达到一定程度以后,如果后期想要重构,那将是灾难级别的。而且技术随着不断的更新,以前的老项目使用的老旧的技术栈,项目更新新技术就只能从新开一个项目。

那么如何解决这样的问题呢?答案就是微前端。
那什么是微前端呢?

举个例子,我们有一个管理系统,里面有各种模块。我们可以按照业务拆分为多个系统进行开发。我们可以有一个主应用,然后把多个系统当成是子应用,独立开发,独立测试,独立部署,最后所有的子应用就承载到主应用上。
image.png

假设我们的主应用部署到https://main.app.com,有两个子应用分别部署到https://a.app.com,https://b.app.com
那么在主应用中打开子应用是如何访问的呢?如这样的:https://mian.app.com/a,https://mian.app.com/b。这样我们的三个应用都集中到了一个应用上,大家各自开发自己负责的项目,各自测试、部署自己的项目。

微前端如何加载子应用的呢?
image.png
需要先加载基座(主应用),再把选择权交给主应用,由主应用根据注册过的子应用来抉择加载谁,当子应用加载成功后,再由vue-router或react-router来根据路由渲染组件。

微前端有什么价值?

微前端架构具备以下几个核心价值:

  • 技术栈无关
    主框架不限制接入应用的技术栈,微应用具备完全自主权
  • 独立开发、独立部署
    微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
  • 增量升级在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
  • 独立运行时
    每个微应用之间状态隔离,运行时状态不共享

微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用。

如何搭建一个微前端?

构建主应用基座

将普通的项目改造成 qiankun 主应用基座,需要进行三步操作:

  1. 创建微应用容器 - 用于承载微应用,渲染显示微应用;
  2. 注册微应用 - 设置微应用激活条件,微应用地址等等;
  3. 启动 qiankun;

    创建微应用容器

    我们先在主应用中创建微应用的承载容器,这个容器规定了微应用的显示区域,微应用将在该容器内渲染并显示。
    我们先设置路由,路由文件规定了主应用自身的路由匹配规则,代码实现如下: ```javascript // micro-app-main/src/routes/index.ts import Home from “@/pages/home/index.vue”;

const routes = [ { /**

  1. * path: 路径为 / 时触发该路由规则
  2. * name: 路由的 name Home
  3. * component: 触发路由时加载 `Home` 组件
  4. */
  5. path: "/",
  6. name: "Home",
  7. component: Home,

}, ];

export default routes;

// micro-app-main/src/main.ts //… import Vue from “vue”; import VueRouter from “vue-router”;

import routes from “./routes”;

/**

  • 注册路由实例
  • 即将开始监听 location 变化,触发路由规则 */ const router = new VueRouter({ mode: “history”, routes, });

// 创建 Vue 实例 // 该实例将挂载/渲染在 id 为 main-app 的节点上 new Vue({ router, render: (h) => h(App), }).$mount(“#main-app”);

  1. 从上面代码可以看出,我们设置了主应用的路由规则,设置了 Home 主页的路由匹配规则。<br />我们现在来设置主应用的布局,我们会有一个菜单和显示区域,代码实现如下:
  2. ```javascript
  3. // micro-app-main/src/App.vue
  4. //...
  5. export default class App extends Vue {
  6. /**
  7. * 菜单列表
  8. * key: 唯一 Key 值
  9. * title: 菜单标题
  10. * path: 菜单对应的路径
  11. */
  12. menus = [
  13. {
  14. key: "Home",
  15. title: "主页",
  16. path: "/",
  17. },
  18. ];
  19. }

上面的代码是我们对菜单配置的实现,我们还需要实现基座和微应用的显示区域(如下图)
image.png
从上面的分析可以看出,我们使用了在路由表配置的 name 字段进行判断,判断当前路由是否为主应用路由,最后决定渲染主应用组件或是微应用节点。
我们来分析一下上面的代码:

  • 第 5 行:主应用菜单,用于渲染菜单;
  • 第 9 行:主应用渲染区。在触发主应用路由规则时(由路由配置表的 $route.name 判断),将渲染主应用的组件;
  • 第 10 行:微应用渲染区。在未触发主应用路由规则时(由路由配置表的 $route.name 判断),将渲染微应用节点;

从上面的分析可以看出,我们使用了在路由表配置的 name 字段进行判断,判断当前路由是否为主应用路由,最后决定渲染主应用组件或是微应用节点。

最后主应用的实现效果如下图所示:
image.png

注册微应用

在构建好了主框架后,我们需要使用 qiankun 的 registerMicroApps 方法注册微应用,代码实现如下:

  1. // micro-app-main/src/micro/apps.ts
  2. // 此时我们还没有微应用,所以 apps 为空
  3. const apps = [];
  4. export default apps;
  5. // micro-app-main/src/micro/index.ts
  6. // 一个进度条插件
  7. import NProgress from "nprogress";
  8. import "nprogress/nprogress.css";
  9. import { message } from "ant-design-vue";
  10. import {
  11. registerMicroApps,
  12. addGlobalUncaughtErrorHandler,
  13. start,
  14. } from "qiankun";
  15. // 微应用注册信息
  16. import apps from "./apps";
  17. /**
  18. * 注册微应用
  19. * 第一个参数 - 微应用的注册信息
  20. * 第二个参数 - 全局生命周期钩子
  21. */
  22. registerMicroApps(apps, {
  23. // qiankun 生命周期钩子 - 微应用加载前
  24. beforeLoad: (app: any) => {
  25. // 加载微应用前,加载进度条
  26. NProgress.start();
  27. console.log("before load", app.name);
  28. return Promise.resolve();
  29. },
  30. // qiankun 生命周期钩子 - 微应用挂载后
  31. afterMount: (app: any) => {
  32. // 加载微应用前,进度条加载完成
  33. NProgress.done();
  34. console.log("after mount", app.name);
  35. return Promise.resolve();
  36. },
  37. });
  38. /**
  39. * 添加全局的未捕获异常处理器
  40. */
  41. addGlobalUncaughtErrorHandler((event: Event | string) => {
  42. console.error(event);
  43. const { message: msg } = event as any;
  44. // 加载失败时提示
  45. if (msg && msg.includes("died in status LOADING_SOURCE_CODE")) {
  46. message.error("微应用加载失败,请检查应用是否可运行");
  47. }
  48. });
  49. // 导出 qiankun 的启动函数
  50. export default start;

从上面可以看出,我们的微应用注册信息在 apps 数组中(此时为空,我们在后面接入微应用时会添加微应用注册信息),然后使用 qiankun 的 registerMicroApps 方法注册微应用,最后导出了 start 函数,注册微应用的工作就完成啦!

启动主应用

我们在注册好了微应用,导出 start 函数后,我们需要在合适的地方调用 start 启动主应用。
我们一般是在入口文件启动 qiankun 主应用,代码实现如下:

  1. // micro-app-main/src/main.ts
  2. //...
  3. import startQiankun from "./micro";
  4. startQiankun();

接入vue微应用

我们在主应用的同级目录(micro-app-main 同级目录),使用 vue-cli 先创建一个 Vue 的项目,在命令行运行如下命令:

  1. vue create micro-app-vue

在新建项目完成后,我们创建几个路由页面再加上一些样式,最后效果如下:
image.png

注册微应用

在创建好了 Vue 微应用后,我们可以开始我们的接入工作了。首先我们需要在主应用中注册该微应用的信息,代码实现如下:

  1. // micro-app-main/src/micro/apps.ts
  2. const apps = [
  3. /**
  4. * name: 微应用名称 - 具有唯一性
  5. * entry: 微应用入口 - 通过该地址加载微应用
  6. * container: 微应用挂载节点 - 微应用加载完成后将挂载在该节点上
  7. * activeRule: 微应用触发的路由规则 - 触发路由规则后将加载该微应用
  8. */
  9. {
  10. name: "VueMicroApp",
  11. entry: "//localhost:10200",
  12. container: "#frame",
  13. activeRule: "/vue",
  14. },
  15. ];
  16. export default apps;

通过上面的代码,我们就在主应用中注册了我们的 Vue 微应用,进入 /vue 路由时将加载我们的 Vue 微应用。
我们在菜单配置处也加入 Vue 微应用的快捷入口,代码实现如下:

  1. // micro-app-main/src/App.vue
  2. //...
  3. export default class App extends Vue {
  4. /**
  5. * 菜单列表
  6. * key: 唯一 Key 值
  7. * title: 菜单标题
  8. * path: 菜单对应的路径
  9. */
  10. menus = [
  11. {
  12. key: "Home",
  13. title: "主页",
  14. path: "/",
  15. },
  16. {
  17. key: "VueMicroApp",
  18. title: "Vue 主页",
  19. path: "/vue",
  20. },
  21. {
  22. key: "VueMicroAppList",
  23. title: "Vue 列表页",
  24. path: "/vue/list",
  25. },
  26. ];
  27. }


菜单配置完成后,我们的主应用基座效果图如下
image.png

配置微应用

在主应用注册好了微应用后,我们还需要对微应用进行一系列的配置。首先,我们在 Vue 的入口文件 main.js 中,导出 qiankun 主应用所需要的三个生命周期钩子函数,代码实现如下:
image.png
从上图来分析:

  • 第 6 行:webpack 默认的 publicPath 为 “” 空字符串,会基于当前路径来加载资源。我们在主应用中加载微应用时需要重新设置 publicPath,这样才能正确加载微应用的相关资源。(public-path.js 具体实现在后面)
  • 第 21 行:微应用的挂载函数,在主应用中运行时将在 mount 生命周期钩子函数中调用,可以保证在沙箱内运行。
  • 第 38 行:微应用独立运行时,直接执行 render 函数挂载微应用。
  • 第 46 行:微应用导出的生命周期钩子函数 - bootstrap。
  • 第 53 行:微应用导出的生命周期钩子函数 - mount。
  • 第 61 行:微应用导出的生命周期钩子函数 - unmount。

完整代码实现如下:

  1. // micro-app-vue/src/public-path.js
  2. if (window.__POWERED_BY_QIANKUN__) {
  3. // 动态设置 webpack publicPath,防止资源加载出错
  4. // eslint-disable-next-line no-undef
  5. __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
  6. }
  7. // micro-app-vue/src/main.js
  8. import Vue from "vue";
  9. import VueRouter from "vue-router";
  10. import Antd from "ant-design-vue";
  11. import "ant-design-vue/dist/antd.css";
  12. import "./public-path";
  13. import App from "./App.vue";
  14. import routes from "./routes";
  15. Vue.use(VueRouter);
  16. Vue.use(Antd);
  17. Vue.config.productionTip = false;
  18. let instance = null;
  19. let router = null;
  20. /**
  21. * 渲染函数
  22. * 两种情况:主应用生命周期钩子中运行 / 微应用单独启动时运行
  23. */
  24. function render() {
  25. // 在 render 中创建 VueRouter,可以保证在卸载微应用时,移除 location 事件监听,防止事件污染
  26. router = new VueRouter({
  27. // 运行在主应用中时,添加路由命名空间 /vue
  28. base: window.__POWERED_BY_QIANKUN__ ? "/vue" : "/",
  29. mode: "history",
  30. routes,
  31. });
  32. // 挂载应用
  33. instance = new Vue({
  34. router,
  35. render: (h) => h(App),
  36. }).$mount("#app");
  37. }
  38. // 独立运行时,直接挂载应用
  39. if (!window.__POWERED_BY_QIANKUN__) {
  40. render();
  41. }
  42. /**
  43. * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
  44. * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
  45. */
  46. export async function bootstrap() {
  47. console.log("VueMicroApp bootstraped");
  48. }
  49. /**
  50. * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
  51. */
  52. export async function mount(props) {
  53. console.log("VueMicroApp mount", props);
  54. render(props);
  55. }
  56. /**
  57. * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
  58. */
  59. export async function unmount() {
  60. console.log("VueMicroApp unmount");
  61. instance.$destroy();
  62. instance = null;
  63. router = null;
  64. }

在配置好了入口文件 main.js 后,我们还需要配置 webpack,使 main.js 导出的生命周期钩子函数可以被 qiankun 识别获取。
我们直接配置 vue.config.js 即可,代码实现如下:

  1. // micro-app-vue/vue.config.js
  2. const path = require("path");
  3. module.exports = {
  4. devServer: {
  5. // 监听端口
  6. port: 10200,
  7. // 关闭主机检查,使微应用可以被 fetch
  8. disableHostCheck: true,
  9. // 配置跨域请求头,解决开发环境的跨域问题
  10. headers: {
  11. "Access-Control-Allow-Origin": "*",
  12. },
  13. },
  14. configureWebpack: {
  15. resolve: {
  16. alias: {
  17. "@": path.resolve(__dirname, "src"),
  18. },
  19. },
  20. output: {
  21. // 微应用的包名,这里与主应用中注册的微应用名称一致
  22. library: "VueMicroApp",
  23. // 将你的 library 暴露为所有的模块定义下都可运行的方式
  24. libraryTarget: "umd",
  25. // 按需加载相关,设置为 webpackJsonp_VueMicroApp 即可
  26. jsonpFunction: `webpackJsonp_VueMicroApp`,
  27. },
  28. },
  29. };

我们需要重点关注一下 output 选项,当我们把 libraryTarget 设置为 umd 后,我们的 library 就暴露为所有的模块定义下都可运行的方式了,主应用就可以获取到微应用的生命周期钩子函数了。
在 vue.config.js 修改完成后,我们重新启动 Vue 微应用,然后打开主应用基座 http://localhost:9999。我们点击左侧菜单切换到微应用,此时我们的 Vue 微应用被正确加载啦!

image.png
接入react微应用

我们在主应用的同级目录(micro-app-main 同级目录),使用 create-react-app 先创建一个 React 的项目,在命令行运行如下命令:

  1. npx create-react-app micro-app-react

在项目创建完成后,我们在根目录下添加 .env 文件,设置项目监听的端口,代码实现如下:

  1. # micro-app-react/.env
  2. PORT=10100
  3. BROWSER=none

然后,我们创建几个路由页面再加上一些样式,最后效果如下:
image.png

注册微应用

在创建好了 React 微应用后,我们可以开始我们的接入工作了。首先我们需要在主应用中注册该微应用的信息,代码实现如下:

  1. // micro-app-main/src/micro/apps.ts
  2. const apps = [
  3. /**
  4. * name: 微应用名称 - 具有唯一性
  5. * entry: 微应用入口 - 通过该地址加载微应用
  6. * container: 微应用挂载节点 - 微应用加载完成后将挂载在该节点上
  7. * activeRule: 微应用触发的路由规则 - 触发路由规则后将加载该微应用
  8. */
  9. {
  10. name: "ReactMicroApp",
  11. entry: "//localhost:10100",
  12. container: "#frame",
  13. activeRule: "/react",
  14. },
  15. ];
  16. export default apps;

通过上面的代码,我们就在主应用中注册了我们的 React 微应用,进入 /react 路由时将加载我们的 React 微应用。
我们在菜单配置处也加入 React 微应用的快捷入口,代码实现如下:

  1. // micro-app-main/src/App.vue
  2. //...
  3. export default class App extends Vue {
  4. /**
  5. * 菜单列表
  6. * key: 唯一 Key 值
  7. * title: 菜单标题
  8. * path: 菜单对应的路径
  9. */
  10. menus = [
  11. {
  12. key: "Home",
  13. title: "主页",
  14. path: "/",
  15. },
  16. {
  17. key: "ReactMicroApp",
  18. title: "React 主页",
  19. path: "/react",
  20. },
  21. {
  22. key: "ReactMicroAppList",
  23. title: "React 列表页",
  24. path: "/react/list",
  25. },
  26. ];
  27. }

菜单配置完成后,我们的主应用基座效果图如下:
image.png

配置微应用

在主应用注册好了微应用后,我们还需要对微应用进行一系列的配置。首先,我们在 React 的入口文件 index.js 中,导出 qiankun 主应用所需要的三个生命周期钩子函数,代码实现如下:

  1. // micro-app-react/src/public-path.js
  2. if (window.__POWERED_BY_QIANKUN__) {
  3. // 动态设置 webpack publicPath,防止资源加载出错
  4. // eslint-disable-next-line no-undef
  5. __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
  6. }
  7. // micro-app-react/src/index.js
  8. import React from "react";
  9. import ReactDOM from "react-dom";
  10. import "antd/dist/antd.css";
  11. import "./public-path";
  12. import App from "./App.jsx";
  13. /**
  14. * 渲染函数
  15. * 两种情况:主应用生命周期钩子中运行 / 微应用单独启动时运行
  16. */
  17. function render() {
  18. ReactDOM.render(<App />, document.getElementById("root"));
  19. }
  20. // 独立运行时,直接挂载应用
  21. if (!window.__POWERED_BY_QIANKUN__) {
  22. render();
  23. }
  24. /**
  25. * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
  26. * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
  27. */
  28. export async function bootstrap() {
  29. console.log("ReactMicroApp bootstraped");
  30. }
  31. /**
  32. * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
  33. */
  34. export async function mount(props) {
  35. console.log("ReactMicroApp mount", props);
  36. render(props);
  37. }
  38. /**
  39. * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
  40. */
  41. export async function unmount() {
  42. console.log("ReactMicroApp unmount");
  43. ReactDOM.unmountComponentAtNode(document.getElementById("root"));
  44. }

代码分析:

  • 第 5 行:webpack 默认的 publicPath 为 “” 空字符串,会基于当前路径来加载资源。我们在主应用中加载微应用时需要重新设置 publicPath,这样才能正确加载微应用的相关资源。(public-path.js 具体实现在后面)
  • 第 12 行:微应用的挂载函数,在主应用中运行时将在 mount 生命周期钩子函数中调用,可以保证在沙箱内运行。
  • 第 17 行:微应用独立运行时,直接执行 render 函数挂载微应用。
  • 第 25 行:微应用导出的生命周期钩子函数 - bootstrap。
  • 第 32 行:微应用导出的生命周期钩子函数 - mount。
  • 第 40 行:微应用导出的生命周期钩子函数 - unmount。

在配置好了入口文件 index.js 后,我们还需要配置路由命名空间,以确保主应用可以正确加载微应用,代码实现如下:

  1. // micro-app-react/src/App.jsx
  2. const BASE_NAME = window.__POWERED_BY_QIANKUN__ ? "/react" : "";
  3. const App = () => {
  4. //...
  5. return (
  6. // 设置路由命名空间
  7. <Router basename={BASE_NAME}>{/* ... */}</Router>
  8. );
  9. };

接下来,我们还需要配置 webpack,使 index.js 导出的生命周期钩子函数可以被 qiankun 识别获取。
我们需要借助 react-app-rewired 来帮助我们修改 webpack 的配置,我们直接安装该插件:

  1. npm install react-app-rewired -D

在 react-app-rewired 安装完成后,我们还需要修改 package.json 的 scripts 选项,修改为由 react-app-rewired 启动应用,就像下面这样

  1. // micro-app-react/package.json
  2. //...
  3. "scripts": {
  4. "start": "react-app-rewired start",
  5. "build": "react-app-rewired build",
  6. "test": "react-app-rewired test",
  7. "eject": "react-app-rewired eject"
  8. }

在 react-app-rewired 配置完成后,我们新建 config-overrides.js 文件来配置 webpack,代码实现如下:

  1. const path = require("path");
  2. module.exports = {
  3. webpack: (config) => {
  4. // 微应用的包名,这里与主应用中注册的微应用名称一致
  5. config.output.library = `ReactMicroApp`;
  6. // 将你的 library 暴露为所有的模块定义下都可运行的方式
  7. config.output.libraryTarget = "umd";
  8. // 按需加载相关,设置为 webpackJsonp_VueMicroApp 即可
  9. config.output.jsonpFunction = `webpackJsonp_ReactMicroApp`;
  10. config.resolve.alias = {
  11. ...config.resolve.alias,
  12. "@": path.resolve(__dirname, "src"),
  13. };
  14. return config;
  15. },
  16. devServer: function (configFunction) {
  17. return function (proxy, allowedHost) {
  18. const config = configFunction(proxy, allowedHost);
  19. // 关闭主机检查,使微应用可以被 fetch
  20. config.disableHostCheck = true;
  21. // 配置跨域请求头,解决开发环境的跨域问题
  22. config.headers = {
  23. "Access-Control-Allow-Origin": "*",
  24. };
  25. // 配置 history 模式
  26. config.historyApiFallback = true;
  27. return config;
  28. };
  29. },
  30. };

我们需要重点关注一下 output 选项,当我们把 libraryTarget 设置为 umd 后,我们的 library 就暴露为所有的模块定义下都可运行的方式了,主应用就可以获取到微应用的生命周期钩子函数了。
在 config-overrides.js 修改完成后,我们重新启动 React 微应用,然后打开主应用基座 http://localhost:9999。我们点击左侧菜单切换到微应用,此时我们的 React 微应用被正确加载啦!
image.png
此时我们打开控制台,可以看到我们所执行的生命周期钩子函数(见下图)
image.png
到这里,React 微应用就接入成功了!

实际效果预览

image.png代码地址

https://github.com/a1029563229/micro-front-template/tree/feature-inject-sub-apps