基于 qiankun 的微前端应用实践

基于 qiankun 的微前端应用实践

业务背景

大鹏地图可视化大屏项目是一个集地图应用视图应用为一体的大屏应用,通过头部菜单可切换视图应用,视图应用包括左右两边的内容展示区域,视图应用可以和地图应用通信和交互。项目采用Vue3 搭建,核心问题在于,3D 地图如果使用iframe 方式集成,那么性能和用户体验会大幅降低,为了解决这个问题,我们采用微前端服务框架 qiankun 成功将地图 h5 应用和 Vue3 视图应用以 DOM 的方式嵌入同一个页面中,这些嵌入的应用就称为微应用,下图中的地图应用和视图应用均为微应用。

架构图1.png

为什么采用微前端方案

  • 1.技术栈无关 - 支持接入任意技术栈的应用,支持未来任何技术栈

    • 试想一下,5 年前的 Angular.js 在当时也是非常火的技术栈,许多大型项目都在用,然而技术每年都会迭代更新,每年前端都只会学习使用最新的技术栈,如今的 Angular.js 已经几乎无人问津,而当初用这在当时很热门的技术栈搭建的项目,现在却已是没人想去改、去优化、去移植、甚至没人会修改的地步。
  • 2.可独立开发、测试、部署 - 不同团队或人员维护对应应用,职责拆分,从巨石解耦,加快构建和开发

    • 你能想象把百度和谷歌放在一个页面里同时运行吗?甚至是把 qq 音乐、网易云音乐、酷狗音乐放在一个页面里运行?没错,微前端他实现了,你可以随时把一个应用单独拿出来开发、部署,同时也能在一个基座中将这些单独的应用集成进来组合成新的应用。
  • 3.增量升级 - 不用打包全部代码更新升级,快速且更有针对性

    • 抛开以前传统应用的整体打包升级方式,微前端方案是将细粒度更小的应用组合成一个大的应用,因此只需要小应用升级即可,好比你有一套房,只需要对其中一个房间进行升级改造,其余房间丝毫不用动。
  • 4.独立运行时

    • 每个微小应用都拥有自己的独立运行时上下文,也就是说他们的 js、css 环境是互相不受影响的,比如 A 应用修改了 window.aB 应用也修改了 window.a,但他们的 window 不是同一个 window对象,故不会造成变量污染。

方案实践

基座应用改造

基座采用 vue-cli 搭建,子应用也一样用 vue-cli 搭建,技术栈统一为 Vue3,后续接入子应用可以接入其他技术栈的应用。

安装 qiankun

1
npm install qiankun -P

路由配置

/home 作为整个基座和子应用的共同主路由

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
// router/index.ts
const Login = () => import("../views/login.vue");
const Home = () => import("../views/home.vue");
const NotFound = () => import("@/components/exception/not-found.vue");

// 首次必然要加载的路由
const routes: Array<any> = [
// 默认进入首页
{
path: "",
redirect: {
name: "home",
},
},
{
path: "/",
redirect: {
name: "home",
},
},
{
path: "/home",
name: "home",
component: Home,
},
// 全不匹配的情况下,返回404
{
path: "/:catchAll(.*)",
component: NotFound,
},
];

export default routes;

基座通过 createWebHashHistory 创建 hash 路由,使用 historymemory 路由也可以

1
2
3
4
5
6
7
8
9
10
11
// main.ts
import routes from "./router";

function render() {
router = createRouter({
history: createWebHashHistory(),
routes,
});

instance.use(router);
}

配置导航菜单

点击菜单要切换视图子应用,因此菜单每个选项都应该包含一个应用信息,包括目录 id,名称、入口、挂载容器 id,用于切换视图微应用。

image.png
导航目录的数据结构如下,真实场景是通过接口获取的,便于动态修改

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
// menu.ts
export default [{
"id": 1, // 目录id
"name": "蓝天", // 目录名称
"app": {
"name": "vue3-air-app", // 应用名称
"entry": {
"dev": "//localhost:8081/", // 开发版应用入口
"product": "http://182.48.115.108:8887/vue3-air-app/" // 线上版应用入口
},
"container": "#microapp" // 挂载容器id
},
"active": false
}, {
"id": 2,
"name": "碧水",
"app": {
"name": "vue3-water-app",
"entry": {
"dev": "//localhost:8082/",
"product": "http://182.48.115.108:8887/vue3-water-app/"
},
"container": "#microapp"
},
"active": false
},
……
]

当菜单切换时,通过 loadMicroApp 加载菜单中的应用信息即可完成视图应用的切换,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { loadMicroApp } from "qiankun";
import MENU from "./menu";
const isProd = process.env.NODE_ENV === "production"; // 是否开发环境

// 菜单切换
function onChangeMenu(id) {
const currentApp = MENU.find((menu) => menu.id === id).app;
loadMicroApp({
name: currentApp.name,
entry: currentApp.entry[isProd ? "dev" : "product"],
container: currentApp.container,
props: {},
});
}

加载微应用

基座加载微应用有两种方式,一种是通过 registerMicroApps 注册子应用信息包括子应用的名称(name)、入口(entry)、挂载容器 id(container)、路由匹配规则(activeRule),注册后的应用会根据浏览器url 的变化来匹配对应的子应用并加载,第二种是通过 loadMicroApp 来手动加载子应用,也是需要传入子应用的名称、入口、挂载容器 id,不过是少了路由匹配规则,他能让你的子应用立即挂载,无须匹配任何路由规则,本项目采用的是 loadMicroApp,因为要同时加载地图应用和视图应用

  • loadMicroApp 使用说明
    • 作用:通过 qiankunloadMicroApp 函数实现在基座中挂载/卸载子应用。
    • 优点:在一个页面中可以同时挂载多个微应用,比如可以同时挂载地图应用和视图应用。
    • 缺点:无法根据路由匹配规则来挂载应用,因为一个路由只能匹配一个应用。
    • 适用场景:当需要在一个页面中同时挂载 2 个以上子应用,并且子应用的挂载不需要通过路由匹配来实现。
  • demo 演示

image.png

  • 代码实现
1
2
3
4
5
6
7
<!-- template -->
<template>
<div class="container">
<div id="micro-app1"></div>
<div id="micro-app2"></div>
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// main.ts
import { loadMicroApp } from "qiankun";
loadMicroApp({
name: "app1", // 应用唯一名称
entry: "//localhost:8088/", // 应用唯一HTML入口,可以省略index.html
container: "#micro-app1", // 基座挂载该应用的容器DOM的id
props: {}, // 基座传递给微应用的参数,微应用通过mount生命周期函数的参数props获取
});

loadMicroApp({
name: "app2",
entry: "//localhost:8089/",
container: "#micro-app2",
props: {},
});

注意:loadMicroApp 重复挂载 name 和 container 一样的应用是会出错的,比如下面操作

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// main.ts
import { loadMicroApp } from "qiankun";
loadMicroApp({
name: "app1", // 应用唯一名称
entry: "//localhost:8088/", // 应用唯一HTML入口,可以省略index.html
container: "#micro-app1", // 基座挂载该应用的容器DOM的id
props: {}, // 基座传递给微应用的参数,微应用通过mount生命周期函数的参数props获取
});

loadMicroApp({
name: "app2",
entry: "//localhost:8088/",
container: "#micro-app2",
props: {},
});

// 这里重复挂载上面的应用后,页面会变成空白
loadMicroApp({
name: "app2",
entry: "//localhost:8089/",
container: "#micro-app2",
props: {},
});

解决方案:通过 loadMicroApp 进一步封装了 switchMicroApp 函数,实现根据应用的挂载情况来决定如何切换应用,首次挂载应用时直接调用 loadMicroApp 加载应用,非首次挂载应用时,则需要先卸载之前挂载的应用后才挂载新的应用。

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
import { LoadableApp, loadMicroApp } from "qiankun";

// loadMicroApp的实例对象
const contentApp: any = ref(null);

/**
* 切换微应用
* @param runningMicroApp qiankun规定的微应用配置对象
*/
function switchMicroApp(runningMicroApp: LoadableApp<any>) {
const microApp = runningMicroApp;

// 切换微应用时,先卸载前一个微应用
if (microApp && contentApp?.value?.getStatus() === "MOUNTED") {
// 卸载前一个应用
contentApp.value.unmount();
// 卸载完前一个应用后紧接着加载新的应用,这里用qiankun的loadMicroApp来加载微应用,返回一个实例,可以通过实例上的unmount方法卸载自身。
contentApp.value = loadMicroApp(microApp);

return;
}

// 如果微应用是初次加载,那么不用先卸载之前挂载的应用直接加载
contentApp.value = loadMicroApp(microApp);
}

switchMicroApp({
name: "app2",
entry: "//localhost:8088/",
container: "#micro-app2",
props: {},
});

// 这里重复挂载上面的应用,正常加载
switchMicroApp({
name: "app2",
entry: "//localhost:8089/",
container: "#micro-app2",
props: {},
});

Layout 组件

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
<template>
<div class="ths-main">
<!-- 导航菜单 -->
<t-header :data="headerNavs" @click="onClickHeader"></t-header>

<!-- 视图微应用容器 -->
<div class="content" id="microapp"></div>

<div class="map">
<div class="map-mask"></div>
<!-- 地图微应用容器 -->
<div id="map-app"></div>
</div>
</div>
</template>
<script lang="ts">
export default defineComponent({
name: 'Home',
setup() {
// 当前头部菜单配置
const headerNavs = ref<HeaderNavItems>([]);
// 当前选中的应用id
const curAppId = ref<number>(0);

// 默认要加载的子应用,地图+首页
const BASE_MICRO_APPS: BaseMicroApps = {
map: {
id: 99,
name: 'h5-map-app',
entry: !isProd ? '//localhost:8088/' : `${webUrl}h5-map-app/`,
container: '#map-app',
props: {
baseUrl,
experimentalStyleIsolation: true,
},
},
home: {
id: 0,
name: 'vue3-home-app',
entry: !isProd ? '//localhost:8090/' : `${webUrl}vue3-home-app/`,
container: '#microapp',
props: {
baseUrl,
experimentalStyleIsolation: false,
},
},
};

/**
* 点击菜单切换子应用
* @param id 应用id
*/
onClickHeader(id: number) {
// 点击的是当前选中的则返回
if (curAppId.value === id) {
return;
}

// 当前选中的应用id
curAppId.value = id;
// 根据点击的应用id,选中加载哪个应用
headerNavs.value.forEach((item) => {
if (item.id === id) {
switchMicroApp(item.id, {
...item.app,
entry: item.app.entry[isProd ? 'product' : 'dev'],
});
}
});
}

onBeforeMount(() => {
// 默认加载地图应用
loadMicroApp(BASE_MICRO_APPS.map);
// 默认加载首页应用
loadMicroApp(BASE_MICRO_APPS.home);
});

onMounted(() => {
// 请求导航菜单数据,返回上面‘配置导航菜单’中的MENU对象
getHeaderMenu('dapeng-header-menu').then((data: HeaderNavItems) => {
if (isEmpty(data)) {
headerNavs.value = [];
return false;
}
headerNavs.value = data;
return true;
});
});

return {
onClickHeader,
};
},
});
</script>

刷新路由保存应用状态

由于通过 loadMicroApp 方式加载的应用无法匹配路由,所以当路由变化时就无法刷新或保持状态,一旦刷新路由,那么基座中加载的应用都会重置成首次加载的应用,例如大气、水环境的应用刷新后都会重新渲染成首页应用。

  • 解决方案
    • 每个应用对应菜单的一个 id,所以通过 localStorage 的方式缓存切换的菜单 id,路由刷新后再根据 localStorage 中的 id 切换对应应用即可。

预加载微应用

在获取到包含所有子应用信息的菜单数据后,可以预先请求其余子应用的html、js、css等静态资源,等切换子应用时,可以直接从缓存中读取这些静态资源,从而加快渲染子应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { prefetchApps } from "qiankun";

getHeaderMenu("dapeng-header-menu").then((data: HeaderNavItems) => {
if (isEmpty(data)) {
headerNavs.value = [];
return false;
}
headerNavs.value = data;
// 预加载其余微应用
prefetchApps(
headerNavs.value.map((nav) => ({
name: nav.app.name,
entry: nav.app.entry[isProd ? "product" : "dev"],
}))
);
return true;
});

打造 qiankun 子应用

我们基于公司 fe-cli 创建一个 Vue3 项目应用,由上述的流程描述,我们知道子应用得向外暴露一系列生命周期函数供 qiankun 调用,在 index.js 文件中进行改造:

增加 public-path.ts 文件

目录外层添加 public-path.ts 文件,当子应用挂载在主应用下时,如果我们的一些静态资源沿用了 publicPath=/ 的配置,我们拿到的域名将会是主应用域名,这个时候就会造成资源加载出错,好在 Webpack 提供了 __webpack_public_path__ 动态更改 publicPath 的修改方式,window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ 等同于 location.host + location.pathnamehttp://localhost:8081/http://182.48.115.108:8887/vue3-air-home/ ,如下:

1
2
3
4
// public-path.ts
if ((window as any).__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = (window as any).__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

路由 base 设置

试想一下,当基座的路由 base不再是本地的 '/',而是线上的 '/microFE-dapeng-base/' ,而子应用的路由 base设置还是’/‘,会发生什么,没错,答案是无法匹配到 '/microFE-dapeng-base/' 路由,导致本地子应用路由无法匹配,资源无法加载。注意 createWebHistory 的第一个参数就是设置的路由 base,那么通过如下配置即可解决子应用 base 设置问题:

1
2
3
4
5
6
7
8
9
const isProd = process.env.NODE_ENV === "production"; // 是否开发环境
const BASE_PREFIX = isProd ? "/microFE-dapeng-base/" : "/"; // 根据环境设置base
router = createRouter({
// 根据是否qiankun环境设置base
history: createWebHistory(
(window as any).__POWERED_BY_QIANKUN__ ? BASE_PREFIX : "/"
),
routes,
});

可见,只要是开发环境,不管子应用是 qiankun 环境下或者独立运行,base 始终都是 '/',因为本地开发基座应用都不会设置域名二级目录;而线上环境的话,如果子应用是独立运行,那么 base 就是 '/',相对于当前根路径;如果是 qiankun 环境下,那么 base 就是 '/microFE-dapeng-base/' 相对于基座路由。

增加生命周期函数

子应用的入口文件加入生命周期函数初始化,方便主应用调用资源完成后按应用名称调用子应用的生命周期

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

/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
console.log("mount", props);
render(props); // 执行createApp创建instance并挂载子应用DOM
}

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

注意:所有的生明周期函数都必须是 Promise

子应用独立运行配置

在上述的生命周期 mount 钩子中挂载了子应用的实例的 DOM,那么当子应用要单独运行,是不是也要挂载一次实例的 DOM 呢?通过 !window.__POWERED_BY_QIANKUN__ 判断如果不是 qiankun 环境的话,就立即挂载实例的 DOM。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function render(props) {
const container = props.container || null;
const isProd = process.env.NODE_ENV === 'production'; // 是否开发环境
const BASE_PREFIX = isProd ? '/microFE-dapeng-base/' : '/'; // 根据环境设置base
router = createRouter({
history: createWebHistory(window.__POWERED_BY_QIANKUN__ ? BASE_PREFIX : '/'),
routes,
});
instance = createApp(App);
instance.use(router);
instance.mount(container ? container.querySelector('#app') : '#app');
}

// 当不是qiankun环境时候,立即执行render挂载DOM
if (!window.__POWERED_BY_QIANKUN__) {
render();
}

// 生命周期钩子函数
export async function bootstrap() {...}
// 当是qiankun环境时候,才会在父容器挂载完成后执行mount钩子,然后执行render挂载DOM
export async function mount(props) { render(props) }
export async function unmount() {...}

修改打包配置

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
function isProd() {
return process.env.NODE_ENV === "production";
}
// 图片公用地址前缀,请配置成微应用部署在三方服务器的完整根地址
const publicPath = isProd()
? `http://182.48.115.108:8887/${name}/`
: `http://localhost:${port}`;

module.exports = {
// 本地服务配置
devServer: {
host: "localhost",
hot: true,
disableHostCheck: true,
port,
overlay: {
warnings: false,
errors: true,
},
headers: {
// 设置本地资源允许跨域,部署后服务端也需要设置允许跨域,因为基座是通过 fetch 来拉取子应用资源的,跨域才能拉
"Access-Control-Allow-Origin": "*",
},
},
publicPath, // 注意这里很重要,设置所有静态资源的加载路径为绝对路径
configureWebpack: (config) => {
return {
output: {
// 微应用的包名,这里与主应用中注册的微应用名称一致,比如name = 'vue3-home-app'
library: `${name}-[name]`,
// 将你的 library 暴露为所有的模块定义下都可运行的方式
libraryTarget: "umd",
// 按需加载相关,设置为 webpackJsonp_VueMicroApp 即可
jsonpFunction: "webpackJsonp_VueMicroApp",
},
};
},
chainWebpack: (config) => {
// 替换打包后的字体资源地址为绝对地址或压缩成base64
config.module
.rule("fonts")
.use("url-loader")
.loader("url-loader")
.options({
limit: 4096, // 小于4kb将会被打包成 base64
fallback: {
loader: "file-loader",
options: {
name: "fonts/[name].[hash:8].[ext]",
// 将图片相对地址替换为本地或线上完整地址,防止相对地址会相对于基座地址来查找静态资源
// 如 'http://localhost:8082/'或'http://182.48.115.108:8887/vue3-air-app/'
publicPath,
},
},
})
.end();
// 替换打包后的图片资源地址为绝对地址或压缩成base64
config.module
.rule("images")
.use("url-loader")
.loader("url-loader")
.options({
limit: 4096, // 小于4kb将会被打包成 base64
fallback: {
loader: "file-loader",
options: {
name: "img/[name].[hash:8].[ext]",
// 同上
publicPath,
},
},
});
},
};

注意:配置的修改为了达到三个目的,一个是暴露生命周期函数给主应用调用,第二点是允许跨域访问,第三点是将图片等静态资源的相对路径地址修改为绝对路径从而解决资源相对于基座路径的问题,修改的注意点可以参考代码的注释。

  • 暴露生命周期: UMD 可以让 qiankun 按应用名称匹配到生命周期函数
  • 跨域配置: 主应用是通过 Fetch 获取资源,所以为了解决跨域问题,必须设置允许跨域访问

项目中遇到的问题

1、子应用未成功加载

如果项目启动完成后,发现子应用系统没有加载,我们应该打开控制台分析原因:

  • 控制台无报错:子应用未加载,检查子应用导出的生命周期 mount 中是否调用了 render 挂载 DOM
  • **挂载容器未找到**:检查容器 DIV 是否在 loadMicroApp 时一定存在,如不能保证需设法在 DOM 挂载后执行。

2、基座应用路由模式

基座路由配置

1
2
3
4
5
exports routes = [{
path: '/home',
name: 'home',
component: Home,
}];
  • 基座应用项目是 hash 模式路由,子路由是 history 模式

子应用配置路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// routes.ts
export default [
{
path: "/", // 这里必须是 '/',不能变更pathname
name: "Home",
component: () => import("@/views/home.vue"),
},
];

// main.ts
router = createRouter({
// history 模式
history: createWebHistory(window.__POWERED_BY_QIANKUN__ ? BASE_PREFIX : "/"),
routes,
});
  • 基座应用项目是 hash 模式路由,子路由是 hash 模式

子应用配置路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// routes.ts
export default [
{
path: "/home", // 这里必须跟基座保持一致
name: "Home",
component: () => import("@/views/home.vue"),
},
];

// main.ts
router = createRouter({
// hash 模式
history: createWebHashHistory(
window.__POWERED_BY_QIANKUN__ ? BASE_PREFIX : "/"
),
routes,
});

3、CSS 样式错乱

由于默认情况下 qiankun 并不会开启 CSS 沙箱进行样式隔离,当主应用和子应用产生样式错乱时,有两种样式隔离配置:

  • strictStyleIsolation - boolean

    • 这个时候会用 Shadow Dom 节点包裹子应用,相信大家看到这个也很熟悉,和 Ionic 中组件的样式隔离方案一致。
    1
    2
    3
    4
    5
    loadMicroApp(microApp, {
    sandbox: {
    strictStyleIsolation: true,
    },
    });
    • 优点
      • 完全隔离 CSS 样式
    • 缺点
      • 在使用一些弹窗组件的时候(弹窗很多情况下都是默认添加到了 document.body )这个时候它就跳过了阴影边界,跑到了主应用里面,样式就丢了
  • experimentalStyleIsolation - boolean

    • 会在运行时劫持应用 style 的规则,并且添加前缀来控制隔离,比如 .title,劫持后输出 div[data-qiankun-app] .title
    1
    2
    3
    4
    5
    loadMicroApp(microApp, {
    sandbox: {
    experimentalStyleIsolation: true,
    },
    });
    • 优点
      • 支持大部分样式隔离需求
      • 解决了 Shadow DOM 方案导致的丢失根节点问题
    • 缺点 - 运行时重新加载样式,会有一定性能损耗
      具体如何样式隔离的原理可以参考 这篇文章

4、H5 微应用静态资源 404

h5 子应用如果没有通过 webpack 等工具打包,没有在打包的时候将静态资源相对地址替换成 publicPath,那么还是那个问题,应用被转换成 DOM 后 append 到基座 html 中,相对路径其实已经从原来应用的 url 变为了当前页面也就是基座的 url,通过设置 head 中的 base 标签 href 属性解决相对路径问题:

1
2
3
4
<!-- 本地 -->
<base href="http://localhost:8088/" />
<!-- 线上 -->
<base href="http://182.48.115.108:8887/h5-map-app/" />

5、异步加载的 js 中再异步加载了其他 js,loadMicroApp 加载的应用全局作用域会错乱

qiankun 在加载 js 时,会根据加载的 js 来匹配对应所属的微应用,并开启对应沙箱隔离 js,如果异步加载的 js 中再次异步加载了 js,那么最后异步加载的 js 对应的应用就无法正确匹配到属于哪个微应用,就会造成无法开启正确的沙箱进行隔离,导致 js 全局作用域污染。

  • 解决方案
    • 先只加载有多重异步引入 js 的应用,让所有异步的 js 只能匹配该应用的沙箱,加载完后再通知基座开始加载其余正常应用。

6、配置线上和本地环境的 publicPath 设置资源加载的默认路径

微应用不能使用相对路径的资源,因此需设置资源加载路径为绝对路径,并且区分线上和本地环境。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const isProd = process.env.NODE_ENV === "production";
const publicPath = isProd
? `http://182.48.115.108:8887/${name}/`
: `http://localhost:${port}`;

// vue.config.js
module.exports = {
// ...
publicPath,
};

// webpack
module.exports = {
// ...
assetsPublicPath: publicPath,
};

如果需要本地打包后能正常访问应用,需将 isProd 手动改为 false

7.线上子应用单独运行需修改路由 path

子应用打包部署后,为了能让子应用独立运行,则需要根据子应用部署地址的 path 来设置路由 path,比如子应用部署地址是 http://182.48.115.108:8887/vue3-air-app/,那么子应用路由的 path 就应该变成 '/vue3-air-app' + '/' 而不是 '/',为了统一,访问地址的 path 就是应用的名称 packageName

1
2
3
4
5
6
7
8
9
10
11
const packageName = require('../../package.json').name;
const basePath = '/';
// routes.ts
export default [
{
// 非qiankun环境即独立运行并且是正式版本的应用的 path 要加上应用名称,否则无法独立运行
path: !(window as any).__POWERED_BY_QIANKUN__ && process.env.NODE_ENV === 'production' ? `/${packageName}${basePath}` : basePath;,
name: 'Home',
component: () => import('@/views/home.vue'),
},
];

8、 另外,在接入过程中,总结了几个需要注意的点

  • 虽然 qiankun 支持 jQuery,但对多页应用的老项目接入不是很友好,需要每个页面都修改,成本也很高,这类老项目接入还是比较推荐 iframe
  • 因为 qiankun 的方式,是通过 HTML-Entry 抽取 JS 文件和 DOM 结构的,实际上和主应用共用的是同一个 Document,如果子应用和主应用同时定义了相同事件,会互相影响,如,用 onClickaddEventListener<body> 添加了一个点击事件,JS 沙箱并不能消除它的影响,还得靠平时的代码规范
  • 部署上有点繁琐,需要手动解决跨域问题
  • vue 中使用图片得用 require(相对/绝对路径).default 获取图片路径
  • 在子应用 js 中通过 function 或者 var 声明在 window 上的全局变量无法识别,原因在于 Proxy 沙箱将 window 替换成 Proxy 实例了,所以声明的变量无法保存在 proxy 对象上,如果要使用全局变量,可以用 (function(global){global.obj = {}, global.fn = function() {}}(window))
  • 像地图依赖的的三方 js 有很多不确定性,比如引入了 CesiumJs 和其他 qiankun 无法完美支持的 js 库等,以及其中引入了很多静态资源相对地址都无法在打包时替换为绝对地址,所以为了让这些三方 js 库能顺利集成,最好的方式是将他们在基座的 index.html 中加载,这样 qiankun 就不会劫持三方引入的 js 从而发生错误了。
  • 切换子应用前需要先卸载前一个子应用,否则会报错
1
2
3
app1 = loadMicroApp();
app1.unmount;
app2 = loadMicroApp();
  • 无法兼容 IE,在基座的 main.ts 中引入如下依赖解决
1
2
3
4
5
6
import "whatwg-fetch";
import "custom-event-polyfill";
import "core-js/stable/promise";
import "core-js/stable/symbol";
import "core-js/stable/string/starts-with";
import "core-js/web/url";

8、未来可能需要考虑一些问题

  • 自动化注入:每一个子应用改造的过程其实也是挺麻烦的事情,但是其实大多的工作都是标准化流程,在考虑通过脚本自动注册子应用,实现自动化

总结

其实写下来整个项目,最大的感受 qiankun 的开箱可用性非常强,需要更改的项目配置基本很少,当然遇到的一些坑点也肯定是踩过才能更清晰。

如果文章有什么问题或者错误,欢迎指正交流,谢谢!

完整的项目在线演示地址 点我查看

作者

李霖

发布于

2021-03-30

更新于

2021-08-04

许可协议

CC BY-NC-SA 4.0

评论