微前端之 qiankun 实现方案
条评论距离上一篇 微前端之 @worktile/planet 实现方案 已经过去一年了,正好产品要做技术升级,所以记录下 qiankun 的实现方案。
不过话说 qiankun 已经2年多没有更新了,这个时候升级 qiankun,是否必要呢?
说明:本文中的主应用、微应用都是 Angular@19 版本,都是使用 history 的路由模式。
开始之前,由于 qiankun 官网介绍的部署的目录结构和我期望的不一致,所以我们重新定义部署的目录结构和 Nginx 转发配置。
另外 qiankun 要求:activeRule 不能和微应用的真实访问路径一样,否则在主应用页面刷新会直接变成微应用页面。
基于此,我们规定 activeRule 为 /应用名称,微应用的真实访问路径为 /app_应用名称/。
我期望的目录结构如下:
1 | // Nginx 静态资源目录 |
这样看起来比较清楚,更新也比较方便。
对应的 Nginx 的配置如下:
1 | # 公共路径变量,按照实际路径进行替换 |
一、初始化工程并安装相关依赖
1 | ng new portal --routing -S -g --style=less |
二、主应用配置
注册微应用并启动:
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// src/app/app.component.ts
export class AppComponent implements OnInit {
ngZone = inject(NgZone);
ngOnInit() {
// 将 ngZone 实例挂载到 window 上共享给微应用
(window as any).ngZone = this.ngZone;
registerMicroApps([
{
name: 'app1',
entry: isDevMode() ? '//localhost:3000/app_app1/' : '/app_app1/', // 配置微应用的真实访问路径
container: '#app-host-container',
activeRule: '/app1',
},
{
name: 'app2',
entry: isDevMode() ? '//localhost:3100/app_app2/' : '/app_app2/',
container: '#app-host-container',
activeRule: '/app2',
},
]);
// 启动 qiankun
start();
}
}配置路由:
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// src/app/app.routes.ts
export const routes: Routes = [
{
path: 'login',
component: LoginComponent
},
{
path: 'app1', // 由于我们规定了 activeRule 为 '/应用名称',所以微应用的路由也应该是 '应用名称'
component: EmptyComponent, // 和 @worktile/planet 一样,当进入微应用路由时,主应用其实是没有对应的路由信息的,所以需要导航到空白页面,然后由微应用接管路由显示对应的页面。
children: [
{
path: '**',
component: EmptyComponent
}
]
},
{
path: 'app2',
component: EmptyComponent,
children: [
{
path: '**',
component: EmptyComponent
}
]
},
];配置微应用菜单、微应用容器、router-outlet
1
2
3
4
5
6
7// src/app/app.component.html
<nav>
<a [routerLink]="['/app1']" routerLinkActive="active">应用1</a>
<a [routerLink]="['/app2']" routerLinkActive="active">应用2</a>
</nav>
<router-outlet />
<div id="app-host-container"></div>
三、微应用配置
- 我们对于
publicPath没有特殊要求,所以跳过官网的第一步。 - 设置
history模式路径的base,projects/app1/src/app/app.config.ts文件: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
32import { APP_BASE_HREF } from '@angular/common';
import { ApplicationConfig, NgZone, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
const providers = [];
// 集成环境时,使用主应用共享的 ngZone 实例
if ((window as any).__POWERED_BY_QIANKUN__) {
providers.push(
{
provide: NgZone,
useValue: (window as any).ngZone
}
);
}
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
{
provide: APP_BASE_HREF,
// 由于我们规定了 activeRule 为 '/应用名称',所以在集成环境下,微应用的 baseHref 配置应该为 '/应用名称/'
// 这样微应用中类似 '/user' 这样的路由才会被解析为 '/app1/user'
// 微应用单独访问时,配置微应用的真实访问路径
useValue: (window as any).__POWERED_BY_QIANKUN__ ? '/app1/' : '/app_app1/'
},
...providers,
]
}; - 修改入口文件,
projects/app1/src/main.ts文件: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
27import { ApplicationRef } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
let app: void | ApplicationRef;
async function render() {
app = await bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));
}
// 如果不允许单独访问微应用,将这里注释即可
if (!(window as any).__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap(props: Object) {
}
export async function mount(props: Object) {
render();
}
export async function unmount(props: Object) {
// @ts-ignore
app.destroy();
} - 修改
webpack打包配置
在微应用根目录增加custom-webpack.config.js,内容为:修改1
2
3
4
5
6
7
8
9
10
11
12module.exports = {
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
},
},
output: {
library: 'app1-[name]',
libraryTarget: 'umd',
jsonpFunction: 'webpackJsonp_app1', // webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
},
};angular.json:1
2
3
4
5
6
7
8
9
10
11- "builder": "@angular-devkit/build-angular:application",
+ "builder": "@angular-builders/custom-webpack:browser",
"options": {
+ "customWebpackConfig": {
+ "path": "projects/app1/custom-webpack.config.js"
+ },
+ "deployUrl": "/app_app1/", // 配置微应用的真实访问路径,这将告诉 Angular 在生成 HTML 时将静态资源引用为 /app_app1/
- "browser": "projects/app1/src/main.ts",
+ "main": "projects/app1/src/main.ts",
}1
2
3"serve": {
- "builder": "@angular-devkit/build-angular:dev-server",
+ "builder": "@angular-builders/custom-webpack:dev-server", - 修改根
selector:1
2
3<!-- projects/app1/src/index.html -->
- <app-root></app-root>
+ <app-app1></app-app1>1
2
3// projects/app1/src/app/app.component.ts
- selector: 'app-root',
+ selector: 'app-app1',
至此,主应用、微应用所有配置已完成。
四、启动测试
分别启动主应用、微应用,测试在主应用下加载微应用、单独访问微应用是否正常。
1 | npm start |
五、关于 zone.js 的解释
虽然 Angular 18 正式支持 Zone-less(无Zone)模式,详见provideExperimentalZonelessChangeDetection,但 Angular 19 仍默认依赖 zone.js 触发变更检测。在此传统模式下,Angular 依赖 Zone 拦截所有异步操作(setTimeout/Promise/事件等),异步完成时触发 onMicrotaskEmpty 钩子,进而执行变更检测,保证视图与数据同步。
以下内容来自豆包:
- 而 qiankun 沙箱(快照 / 代理沙箱)会重写全局异步 API(setTimeout/fetch/addEventListener 等)。
- Angular 部分内部 API(如
HttpClient底层、Animation动画)会主动调用runOutsideAngular优化性能。- 但 qiankun 沙箱拦截异步 API 后,即使调用
runOutsideAngular执行异步操作,qiankun 重写后的 API 会将回调重新包装到 Angular Zone 中,导致操作仍处于 Angular Zone 的上下文内,会抛出错误:ERROR RuntimeError: NG0909: Expected to not be in Angular Zone, but it is!。