Post

微前端解决方案:qiankun

微前端解决方案:qiankun

qiankun 简介

微前端是一种将前端应用分解为更小、更易管理的独立部分的架构模式。每个部分可以由不同的团队独立开发、测试和部署,使用不同的技术栈,最终组合成一个完整的应用。 qiankun 是基于 single-spa 封装的微前端框架,提供了更加完善的微前端解决方案。它解决了 single-spa 在实际应用中的一些痛点,如样式隔离、JS 沙箱、预加载等。

优势

  • 技术栈无关:不同子应用可以使用不同的技术栈
  • 独立开发部署:各团队可以独立工作,提高开发效率
  • 渐进式升级:可以逐步迁移老项目,降低重构风险
  • 团队自治:每个团队对自己的模块拥有完全控制权

qiankun 的核心特性

  • 基于 single-spa:享受 single-spa 生态的同时,提供更完善的 API
  • 技术栈无关:任意技术栈的应用均可使用/接入
  • HTML Entry 接入方式:像使用 iframe 一样简单
  • 样式隔离:自动隔离子应用样式,避免样式污染
  • JS 沙箱:确保子应用之间、子应用与主应用间的 JS 隔离
  • 资源预加载:在浏览器空闲时间预加载未打开的微应用资源
  • umi 插件:提供@umijs/plugin-qiankun 一键切换成微前端架构

核心原理解析

HTML Entry

qiankun 采用 HTML Entry 的方式接入子应用,相比 JS Entry 有以下优势:

1
2
3
4
5
6
7
8
9
// HTML Entry方式
registerMicroApps([
  {
    name: "react-app",
    entry: "//localhost:7100",
    container: "#subapp-viewport",
    activeRule: "/react",
  },
]);

这种方式像在页面中嵌入一个 iframe 一样。

JS 沙箱机制

qiankun 提供了三种沙箱模式:

  1. SnapshotSandbox:快照沙箱,兼容性最好
  2. LegacySandbox:单例沙箱,基于 Proxy 实现
  3. ProxySandbox:多例沙箱,支持多个子应用同时运行
1
2
3
4
5
6
7
8
9
10
11
12
13
// 沙箱示例
class ProxySandbox {
  constructor() {
    this.proxyWindow = new Proxy(window, {
      get: (target, prop) => {
        // 获取属性逻辑
      },
      set: (target, prop, value) => {
        // 设置属性逻辑
      },
    });
  }
}

样式隔离

qiankun 提供了两种样式隔离方案:

  • Scoped CSS:自动添加 data 属性进行样式隔离
  • Shadow DOM:使用 Shadow DOM 实现完全隔离

实战:搭建 qiankun 微前端项目

1. 主应用搭建

首先创建主应用:

1
2
3
npx create-react-app main-app
cd main-app
npm install qiankun

配置主应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// src/App.js
import { registerMicroApps, start } from "qiankun";

const microApps = [
  {
    name: "vue-app",
    entry: "//localhost:8081",
    container: "#subapp-container",
    activeRule: "/vue",
  },
  {
    name: "react-app",
    entry: "//localhost:8082",
    container: "#subapp-container",
    activeRule: "/react",
  },
];

// 注册微应用
registerMicroApps(microApps, {
  beforeLoad: (app) => {
    console.log("before load", app);
  },
  beforeMount: (app) => {
    console.log("before mount", app);
  },
  afterUnmount: (app) => {
    console.log("after unmount", app);
  },
});

// 启动qiankun
start({
  prefetch: true, // 预加载
  sandbox: true, // 开启沙箱
});

function App() {
  return (
    <div className="App">
      <header>
        <nav>
          <a href="#/vue">Vue应用</a>
          <a href="#/react">React应用</a>
        </nav>
      </header>
      <main id="subapp-container"></main>
    </div>
  );
}

export default App;

2. Vue 子应用改造

创建 Vue 子应用并进行微前端改造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// vue.config.js
const { name } = require("./package.json");

module.exports = {
  devServer: {
    headers: {
      "Access-Control-Allow-Origin": "*",
    },
    port: 8081,
  },
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      libraryTarget: "umd",
      jsonpFunction: `webpackJsonp_${name}`,
    },
  },
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// src/main.js
import Vue from "vue";
import App from "./App.vue";
import router from "./router";

Vue.config.productionTip = false;

let instance = null;

function render(props = {}) {
  const { container } = props;
  instance = new Vue({
    router,
    render: (h) => h(App),
  }).$mount(container ? container.querySelector("#app") : "#app");
}

// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

export async function bootstrap() {
  console.log("[vue] vue app bootstraped");
}

export async function mount(props) {
  console.log("[vue] props from main framework", props);
  render(props);
}

export async function unmount() {
  instance.$destroy();
  instance.$el.innerHTML = "";
  instance = null;
}

3. React 子应用改造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// config-overrides.js
const { name } = require("./package.json");

module.exports = {
  webpack: (config) => {
    config.output.library = `${name}-[name]`;
    config.output.libraryTarget = "umd";
    config.output.jsonpFunction = `webpackJsonp_${name}`;
    return config;
  },
  devServer: (configFunction) => {
    return function (proxy, allowedHost) {
      const config = configFunction(proxy, allowedHost);
      config.headers = {
        "Access-Control-Allow-Origin": "*",
      };
      return config;
    };
  },
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// src/index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

function render(props) {
  const { container } = props;
  ReactDOM.render(
    <App />,
    container
      ? container.querySelector("#root")
      : document.querySelector("#root")
  );
}

if (!window.__POWERED_BY_QIANKUN__) {
  render({});
}

export async function bootstrap() {
  console.log("[react] react app bootstraped");
}

export async function mount(props) {
  console.log("[react] props from main framework", props);
  render(props);
}

export async function unmount(props) {
  const { container } = props;
  ReactDOM.unmountComponentAtNode(
    container
      ? container.querySelector("#root")
      : document.querySelector("#root")
  );
}

进阶配置和优化

应用间通信

qiankun 提供了多种应用间通信方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 主应用
import { initGlobalState } from "qiankun";

// 初始化全局状态
const actions = initGlobalState({
  user: "qiankun",
  token: "xxx",
});

// 监听全局状态变化
actions.onGlobalStateChange((newState, prev) => {
  console.log("主应用观察者:", newState, prev);
});

// 子应用中
export async function mount(props) {
  const { onGlobalStateChange, setGlobalState } = props;

  // 监听状态变化
  onGlobalStateChange((newState, prev) => {
    console.log("子应用观察者:", newState, prev);
  });

  // 修改全局状态
  setGlobalState({ user: "new user" });
}

预加载优化

1
2
3
4
5
6
7
8
9
10
11
12
13
import { registerMicroApps, start, prefetchApps } from 'qiankun';

// 注册微应用
registerMicroApps([...]);

// 启动qiankun
start();

// 手动预加载指定的微应用静态资源
prefetchApps([
  { name: 'app1', entry: '//localhost:7001' },
  { name: 'app2', entry: '//localhost:7002' },
]);

样式隔离配置

1
2
3
4
5
6
start({
  sandbox: {
    strictStyleIsolation: true, // 启用严格样式隔离
    experimentalStyleIsolation: true, // 实验性样式隔离
  },
});

常见问题和解决方案

1. 路由冲突问题

主应用和子应用都使用 hash 路由时可能出现冲突:

1
2
3
4
5
6
7
8
9
10
11
12
// 解决方案:子应用使用memory路由
// Vue Router
const router = new VueRouter({
  mode: window.__POWERED_BY_QIANKUN__ ? "abstract" : "hash",
  routes,
});

// React Router
const basename = window.__POWERED_BY_QIANKUN__ ? "/react" : "";
<BrowserRouter basename={basename}>
  <App />
</BrowserRouter>;

2. 公共依赖优化

避免重复打包公共依赖:

1
2
3
4
5
6
7
8
// webpack externals配置
module.exports = {
  externals: {
    react: "React",
    "react-dom": "ReactDOM",
    "react-router-dom": "ReactRouterDOM",
  },
};

3. 开发环境代理配置

1
2
3
4
5
6
7
8
9
10
11
// 主应用代理配置
module.exports = {
  devServer: {
    proxy: {
      "/api": {
        target: "http://localhost:3000",
        changeOrigin: true,
      },
    },
  },
};

部署相关

独立部署

每个子应用可以独立部署到不同的服务器:

1
2
3
4
5
6
7
8
9
10
11
const microApps = [
  {
    name: "vue-app",
    entry:
      process.env.NODE_ENV === "development"
        ? "//localhost:8081"
        : "//vue-app.example.com",
    container: "#subapp-container",
    activeRule: "/vue",
  },
];

Nginx 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server {
    listen 80;
    server_name main-app.example.com;

    location / {
        try_files $uri $uri/ /index.html;
    }
}

server {
    listen 80;
    server_name vue-app.example.com;

    location / {
        add_header Access-Control-Allow-Origin *;
        try_files $uri $uri/ /index.html;
    }
}

性能监控和调试

性能监控

1
2
3
4
5
6
7
8
9
// 监控微应用加载性能
registerMicroApps(microApps, {
  beforeLoad: (app) => {
    console.time(`${app.name} load`);
  },
  afterMount: (app) => {
    console.timeEnd(`${app.name} load`);
  },
});

调试技巧

  1. 开启沙箱日志
1
2
3
4
start({
  sandbox: { loose: true },
  singular: false,
});
  1. 使用 qiankun devtools
1
2
3
4
// 开发环境下启用
if (process.env.NODE_ENV === "development") {
  window.__QIANKUN_DEVELOPMENT__ = true;
}
This post is licensed under CC BY 4.0 by the author.