基于qiankun的微前端实战

505 阅读2分钟

项目目录说明

.
|-- README.md
|-- app-react    // 微应用 - 接入 react
|-- app-static   // 微应用 - 接入 静态文件
|-- app-vue      // 微应用 - 接入 vue
|-- docs         // 文档
|-- main         // 主应用 - 基座 (引入的是vue项目)
|-- package.json

1. 主应用

1.1 创建微应用的承载容器

  • js 配置
// main/src/main.ts

import Vue from "vue";
import VueRouter from "vue-router";

import App from "./App.vue";
// 引入主应用的路由文件
// const routes = [
//   {
//     /**
//      * path: 路径为 / 时触发该路由规则
//      * name: 路由的 name 为 Home
//      * component: 触发路由时加载 `Home` 组件
//      */
//     path: "/",
//     name: "Home",
//     component: Home,
//   }
// ];
import routes from "./routes";

Vue.use(VueRouter);


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

// 创建 Vue 实例
// 挂载到 id 为 main-app 的节点上
new Vue({
  router,
  render: (h) => h(App),
}).$mount("#main-app");
  • 视图设置
<!-- main/src/App.vue -->

<template>
  <a-config-provider prefixCls="cns">
    <section id="cns-main-app">
      <section class="cns-menu-wrapper">
        <main-menu :menus="menus" />
      </section>
      <section class="cns-frame-wrapper">
        <!-- 主应用渲染区,用于挂载主应用路由触发的组件 -->
        <router-view v-show="$route.name" />

        <!-- 子应用渲染区,用于挂载子应用节点 -->
        <section v-show="!$route.name" id="frame"></section>
      </section>
    </section>
  </a-config-provider>
</template>

1.2 注册微应用

// main/src/micro/index.ts
import {
  registerMicroApps,
  addGlobalUncaughtErrorHandler,
  start,
} from "qiankun";

// 子应用注册信息
const apps = []

/**
 * 注册子应用
 * 第一个参数 - 子应用的注册信息
 * 第二个参数 - 全局生命周期钩子
 */
registerMicroApps(apps, {
  // qiankun 生命周期钩子 - 加载前
  beforeLoad: (app: any) => {
    console.log("before load", app.name);
    return Promise.resolve();
  },
  // qiankun 生命周期钩子 - 挂载后
  afterMount: (app: any) => {
    console.log("after mount", app.name);
    return Promise.resolve();
  },
});

/**
 * 添加全局的未捕获异常处理器
 */
addGlobalUncaughtErrorHandler((event: Event | string) => {
  console.error(event);
  const { message: msg } = event as any;
  // 加载失败时提示
  if (msg && msg.includes("died in status LOADING_SOURCE_CODE")) {
    message.error("子应用加载失败,请检查应用是否可运行");
  }
});

// 导出 qiankun 的启动函数
export default start;

1.3 启动主应用

在注册完微应用之后,一般在入口启动主应用

// main/src/main.ts
//...
import startQiankun from "./micro";

startQiankun();

2. 接入微应用 - react

2.1 在主应用中注册微应用的信息

// main/src/micro/apps.ts

/**
   * name: 微应用名称 - 具有唯一性
   * entry: 微应用入口 - 通过该地址加载微应用,这里我们使用 config 配置
   * container: 微应用挂载节点 - 微应用加载完成后将挂载在该节点上
   * activeRule: 微应用触发的路由规则 - 触发路由规则后将加载该微应用
   */
  {
    name: "ReactMicroApp",
    entry: "http://localhost:3000",
    container: "#frame",
    activeRule: "/react",
  },

上面当我们通过主应用访问/react时,会进入react应用

2.2 配置微应用

// app-react/src/index.js
import React from "react";
import ReactDOM from "react-dom";
import "antd/dist/antd.css";

import "./public-path";
import App from "./App.jsx";

/**
 * 渲染函数
 * 两种情况:主应用生命周期钩子中运行 / 微应用单独启动时运行
 */
function render(props) { 
  ReactDOM.render(<App />, document.getElementById("root"));
}

// 独立运行时,直接挂载应用(即单独访问应用时,是没有__POWERED_BY_QIANKUN__这个标志的)
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}
// 生命周期
// 注意: 这些生命周期都是返回promise
/**
 * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
 * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
 */
export async function bootstrap() {
  console.log("ReactMicroApp bootstraped");
}

/**
 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
 */
export async function mount(props) {
  console.log("ReactMicroApp mount", props);
  render(props);
}

/**
 * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
 */
export async function unmount() {
  console.log("ReactMicroApp unmount");  
  ReactDOM.unmountComponentAtNode(document.getElementById("root"));
}

2.3 配置微应用的webpack

对微应用的打包成库,并且支持跨域

// 采用脚手架方式构建的react项目,暴露出的 webpack 配置文件 方式配置
// app-react/config-overrieds.js
const path = require("path");

module.exports = {
  webpack: (config) => {
    // 微应用的包名,这里与主应用中注册的微应用名称一致
    config.output.library = `ReactMicroApp`;
    // 将你的 library 暴露为所有的模块定义下都可运行的方式
    config.output.libraryTarget = "umd";
    // 按需加载相关,设置为 webpackJsonp_[name] 即可
    config.output.jsonpFunction = `webpackJsonp_ReactMicroApp`;

    config.resolve.alias = {
      ...config.resolve.alias,
      "@": path.resolve(__dirname, "src"),
    };
    return config;
  },

  devServer: function (configFunction) {
    return function (proxy, allowedHost) {
      const config = configFunction(proxy, allowedHost);
      // 关闭主机检查,使微应用可以被 fetch
      config.disableHostCheck = true;
      // 配置跨域请求头,解决开发环境的跨域问题
      config.headers = {
        "Access-Control-Allow-Origin": "*",
      };
      // 配置 history 模式
      config.historyApiFallback = true;

      return config;
    };
  },
};

3. 接入微应用 - vue

3.1 在主应用中注册微应用的信息

// main/src/micro/apps.ts
  {
    name: "VueMicroApp",
    entry: "http://localhost:3001",
    container: "#frame",
    activeRule: "/vue",
  },

上面当我们通过主应用访问/vue时,会进入vue应用

3.2 配置微应用

// app-vue/src/main.js
import Vue from "vue";
import VueRouter from "vue-router";
import Antd from "ant-design-vue";
import "ant-design-vue/dist/antd.css";

import "./public-path";
import App from "./App.vue";
import routes from "./routes";

Vue.use(VueRouter);
Vue.use(Antd);
Vue.config.productionTip = false;

let instance = null;
let router = null;

/**
 * 渲染函数
 * 两种情况:主应用生命周期钩子中运行 / 微应用单独启动时运行
 */
function render() {
  // 在 render 中创建 VueRouter,可以保证在卸载微应用时,移除 location 事件监听,防止事件污染
  router = new VueRouter({
    // 运行在主应用中时,添加路由命名空间 /vue
    base: window.__POWERED_BY_QIANKUN__ ? "/vue" : "/",
    mode: "history",
    routes,
  });

  // 挂载应用
  instance = new Vue({
    router,
    render: (h) => h(App),
  }).$mount("#app");
}

// 独立运行时,直接挂载应用
if (!window.__POWERED_BY_QIANKUN__) {
  render()
}

/**
 * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
 * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
 */
export async function bootstrap() {
  console.log("VueMicroApp bootstraped");
}

/**
 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
 */
export async function mount(props) {
  console.log("VueMicroApp mount", props);
  render(props);
}

/**
 * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
 */
export async function unmount() {
  console.log("VueMicroApp unmount");
  instance.$destroy();
  instance = null;
  router = null;
}

3.3 配置微应用的webpack

对微应用的打包成库,并且支持跨域

// app-vue/vue.config.js
const path = require('path');

module.exports = {
  devServer: {
    // 监听端口
    port: 3001,
    // 关闭主机检查,使微应用可以被 fetch
    disableHostCheck: true,
    // 配置跨域请求头,解决开发环境的跨域问题
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
  configureWebpack: {
    resolve: {
      alias: {
        '@': path.resolve(__dirname, 'src'),
      }
    },
    output: {
      // 微应用的包名,这里与主应用中注册的微应用名称一致
      library: "VueMicroApp",
      // 将你的 library 暴露为所有的模块定义下都可运行的方式
      libraryTarget: 'umd',
      // 按需加载相关,设置为 webpackJsonp_VueMicroApp 即可
      jsonpFunction: `webpackJsonp_VueMicroApp`,
    }
  }
}

4. CSS隔离

4.1 方式1:采用Shadow DOM

import { start } from 'qiankun'
// 在启动 qinakun 的时候 
start({
  // 是否开启沙箱,默认为true
  // 默认情况下沙箱可以确保单实例场景子应用之间的样式隔离,但是无法确保主应用跟子应用、或者多实例场景的子应用样式隔离
  sandbox: {
    strictStyleIsolation: true, // 表示开启严格的样式隔离模式,这种模式下 qiankun 会为每个微应用的容器包裹上一个 shadow dom 节点,从而确保微应用的样式不会对全局造成影响。
  }
});
  • 显示效果

4.2 方式2:通过添加选择前缀解决样式冲突

import { start } from 'qiankun'
// 在启动 qinakun 的时候 
start({
  // 是否开启沙箱,默认为true
  // 默认情况下沙箱可以确保单实例场景子应用之间的样式隔离,但是无法确保主应用跟子应用、或者多实例场景的子应用样式隔离
  sandbox: {
    experimentalStyleIsolation: true // 通过选择器来解决样式冲突
  }
});
  • 显示效果

项目地址:micro-front