qiankun微前端项目搭建 Angular基座+Angular子应用 基座、子应用配置全流程 基座子应用通信简例

阚英睿
2023-12-01

拆分流程

base

npm i qiankun -S

在app.component.ts中

import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'reactApp',
    entry: '//localhost:3000',
    container: '#container',
    activeRule: '/app-react',
  },
  {
    name: 'vueApp',
    entry: '//localhost:8080',
    container: '#container',
    activeRule: '/app-vue',
  },
  {
    name: 'angularApp',
    entry: '//localhost:4200',
    container: '#container',
    activeRule: '/app-angular',
  },
]);
// 启动 qiankun
start();

npm i qiankun-ng-common -S

在app-routing.module.ts中

const routes: Routes = [
  {
    path: '**',
    component: EmptyComponent // 配置默认路由,避免路由到子项目报错
  }
];

subapp1

  1. src 目录新增 public-path.js 文件,内容为:
if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
  1. 设置 history 模式路由的 basesrc/app/app-routing.module.ts 文件:
+ import { APP_BASE_HREF } from '@angular/common';

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
  // @ts-ignore
+  providers: [{ provide: APP_BASE_HREF, useValue: window.__POWERED_BY_QIANKUN__ ? '/app-angular' : '/' }]
})
  1. 修改入口文件,src/main.ts 文件。
import './public-path';
import { enableProdMode, NgModuleRef } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

let app: void | NgModuleRef<AppModule>;
async function render() {
  app = await platformBrowserDynamic()
    .bootstrapModule(AppModule)
    .catch((err) => console.error(err));
}
if (!(window as any).__POWERED_BY_QIANKUN__) {
  render();
}

export async function bootstrap(props: Object) {
  console.log(props);
}

export async function mount(props: Object) {
  render();
}

export async function unmount(props: Object) {
  console.log(props);
  // @ts-ignore
  app.destroy();
}
  1. 修改 webpack 打包配置

npm i @angular-builders/custom-webpack@9 -D

先安装 @angular-builders/custom-webpack 插件,注意:angular 9 项目只能安装 9.x 版本,angular 10 项目可以安装最新版

npm i @angular-builders/custom-webpack@9.2.0 -D

在根目录增加 custom-webpack.config.js ,内容为:

const appName = require('./package.json').name;
module.exports = {
  devServer: {
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
  output: {
    library: `${appName}-[name]`,
    libraryTarget: 'umd',
    jsonpFunction: `webpackJsonp_${appName}`,
  },
};

修改 angular.json,将 [packageName] > architect > build > builder[packageName] > architect > serve > builder 的值改为我们安装的插件,将我们的打包配置文件加入到 [packageName] > architect > build > options

- "builder": "@angular-devkit/build-angular:browser",
+ "builder": "@angular-builders/custom-webpack:browser",
  "options": {
+    "customWebpackConfig": {
+      "path": "./custom-webpack.config.js"
+    }
  }
- "builder": "@angular-devkit/build-angular:dev-server",
+ "builder": "@angular-builders/custom-webpack:dev-server",

修改architect下build以及serve的builder配置

  1. 解决 zone.js 的问题

Angular 运行依赖于 zone.js

qiankun 基于 single-spa 实现,single-spa 明确指出一个项目的 zone.js 只能存在一份实例,所以我们在主应用注入 zone.js

// micro-app-main/src/main.js

// 为 Angular 微应用所做的 zone 包注入
import "zone.js/dist/zone";

将微应用的 src/polyfills.ts 里面的引入 zone.js 代码删掉。

在微应用的 src/index.html 里面的 <head> 标签加上下面内容,微应用独立访问时使用。

<!-- 也可以使用其他的CDN/本地的包 -->
<script src="https://unpkg.com/zone.js" ignore></script>
  1. 修正 ng build 打包报错问题,修改 tsconfig.json 文件,参考issues/431
- "target": "es2015",
+ "target": "es5",
+ "typeRoots": [
+   "node_modules/@types"
+ ],
  1. 为了防止主应用或其他微应用也为 angular 时,<app-root></app-root> 会冲突的问题,建议给<app-root> 加上一个唯一的 id,

比如说当前应用名称。src/index.html :

- <app-root></app-root>
+ <app-root id="desk"></app-root>

src/app/app.component.ts :

- selector: 'app-root',
+ selector: '#desk app-root',

上述子应用创建走不通 用作参考

下述流程文档参考地址

https://juejin.cn/post/6844904158085021704#heading-13

结合着一起看

在主应用的工作完成后,我们还需要对微应用进行一系列的配置。首先,我们使用 single-spa-angular 生成一套配置,在命令行运行以下命令:

安装 single-spa

yarn add single-spa -S

添加 single-spa-angular

ng add single-spa-angular
npm i	安装需要的依赖

npm i @angular-builders/custom-webpack@9 -D

在生成 single-spa 配置后,我们需要进行一些 qiankun 的接入配置。我们在 Angular 微应用的入口文件 main.single-spa.ts 中,导出 qiankun 主应用所需要的三个生命周期钩子函数,代码实现如下:

main.single-spa.ts中需要加入

// 微应用单独启动时运行
if (!(window as any).__POWERED_BY_QIANKUN__) {
  platformBrowserDynamic()
    .bootstrapModule(AppModule)
    .catch((err) => console.error(err));
}

完整代码实现如下:

import { enableProdMode, NgZone } from '@angular/core';

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { Router, NavigationStart } from '@angular/router';
import { ɵAnimationEngine as AnimationEngine } from '@angular/animations/browser';

import { singleSpaAngular, getSingleSpaExtraProviders } from 'single-spa-angular';


import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { singleSpaPropsSubject } from './single-spa/single-spa-props';

if (environment.production) {
  enableProdMode();
}

// 微应用单独启动时运行
if (!(window as any).__POWERED_BY_QIANKUN__) {
  platformBrowserDynamic()
    .bootstrapModule(AppModule)
    .catch((err) => console.error(err));
}

const lifecycles = singleSpaAngular({
  bootstrapFunction: singleSpaProps => {
    singleSpaPropsSubject.next(singleSpaProps);
    return platformBrowserDynamic(getSingleSpaExtraProviders()).bootstrapModule(AppModule);
  },
  template: "<app-root id='desk' />",
  Router,
  NavigationStart,
  NgZone,
  AnimationEngine,
});

export const bootstrap = lifecycles.bootstrap;
export const mount = lifecycles.mount;
export const unmount = lifecycles.unmount;

在配置好了入口文件 main.single-spa.ts 后,我们还需要配置 webpack,使 main.single-spa.ts 导出的生命周期钩子函数可以被 qiankun 识别获取。

我们直接配置 extra-webpack.config.js 即可,代码实现如下:

// micro-app-angular/extra-webpack.config.js
const singleSpaAngularWebpack = require("single-spa-angular/lib/webpack")
  .default;
const webpackMerge = require("webpack-merge");

module.exports = (angularWebpackConfig, options) => {
  const singleSpaWebpackConfig = singleSpaAngularWebpack(
    angularWebpackConfig,
    options
  );

  const singleSpaConfig = {
    output: {
      // 微应用的包名,这里与主应用中注册的微应用名称一致
      library: "AngularMicroApp",	//这里要改成自己的应用名
      // 将你的 library 暴露为所有的模块定义下都可运行的方式
      libraryTarget: "umd",
    },
  };
  const mergedConfig = webpackMerge.smart(
    singleSpaWebpackConfig,
    singleSpaConfig
  );
  return mergedConfig;
};

我们需要重点关注一下 output 选项,当我们把 libraryTarget 设置为 umd 后,我们的 library 就暴露为所有的模块定义下都可运行的方式了,主应用就可以获取到微应用的生命周期钩子函数了。

extra-webpack.config.js 修改完成后,我们还需要修改一下 package.json 中的启动命令,修改如下:

// micro-app-angular/package.json
{
  //...
  "script": {
    //...
    // --disable-host-check: 关闭主机检查,使微应用可以被 fetch
    // --port: 监听端口
    // --base-href: 站点的起始路径,与主应用中配置的一致
    "start": "ng serve --disable-host-check --port 10300 --base-href /angular"
  }
}

为了防止主应用或其他微应用也为 angular 时,<app-root></app-root> 会冲突的问题,建议给<app-root> 加上一个唯一的 id,或者修改为其他名字

比如说当前应用名称。

src/app/app.component.ts :

- selector: 'app-root',
+ selector: '#desk app-root',
或
- selector: 'app-root',
+ selector: 'desk-root',

index.html、main.single-spa.ts也需要修改

index.html

<app-root id="desk"></app-root>

main.single-spa.ts

template: "<app-root id='desk' />",

删除empty-route文件夹,添加公共的空组件

app1/app-routing.module.ts

const routes: Routes = [
  {
    path: '**',
    component: EmptyComponent
  }
]

此时npm start就可以了

基座、子应用之间通信

主应用的工作

首先,我们在主应用中注册一个 MicroAppStateActions 实例并导出,代码实现如下:

// micro-app-main/src/shared/actions.ts
import { initGlobalState, MicroAppStateActions } from "qiankun";

const initialState = {};
const actions: MicroAppStateActions = initGlobalState(initialState);

export default actions;

在需要传输数据的地方发送即可

send(){
  actions.setGlobalState({'id': '123'});
}

子应用的工作

首先来改造我们的 子应用,首先我们设置一个 Actions 实例,代码实现如下:

// /src/shared/actions.ts
function emptyAction(...args) {
  // 警告:提示当前使用的是空 Action
  console.warn("Current execute action is empty!");
}

class Actions {
  // 默认值为空 Action
  actions = {
    onGlobalStateChange: emptyAction,
    setGlobalState: emptyAction
  };
  
  /**
   * 设置 actions
   */
  setActions(actions) {
    this.actions = actions;
  }

  /**
   * 映射
   */
  onGlobalStateChange(...args) {
    return this.actions.onGlobalStateChange(...args);
  }

  /**
   * 映射
   */
  setGlobalState(...args) {
    return this.actions.setGlobalState(...args);
  }
}

const actions = new Actions();
export default actions;

创建 actions 实例后,我们需要为其注入真实 Actions。我们在入口文件 main.single-spa.ts 中注入

加入以下代码

bootstrapFunction: singleSpaProps => {
  if (singleSpaProps) {
    // 注入 actions 实例
    actions.setActions(singleSpaProps);
  }
  singleSpaPropsSubject.next(singleSpaProps);
  return platformBrowserDynamic(getSingleSpaExtraProviders()).bootstrapModule(AppModule);
},

完整代码为

import { enableProdMode, NgZone } from '@angular/core';

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { Router, NavigationStart } from '@angular/router';
import { ɵAnimationEngine as AnimationEngine } from '@angular/animations/browser';

import { singleSpaAngular, getSingleSpaExtraProviders } from 'single-spa-angular';


import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { singleSpaPropsSubject } from './single-spa/single-spa-props';
import actions from "./shared/actions";

if (environment.production) {
  enableProdMode();
}

// 微应用单独启动时运行
if (!(window as any).__POWERED_BY_QIANKUN__) {
  platformBrowserDynamic()
    .bootstrapModule(AppModule)
    .catch((err) => console.error(err));
}

const lifecycles = singleSpaAngular({
  bootstrapFunction: singleSpaProps => {
    if (singleSpaProps) {
      // 注入 actions 实例
      actions.setActions(singleSpaProps);
    }
    singleSpaPropsSubject.next(singleSpaProps);
    return platformBrowserDynamic(getSingleSpaExtraProviders()).bootstrapModule(AppModule);
  },
  template: "<app-root id='desk' />",
  Router,
  NavigationStart,
  NgZone,
  AnimationEngine,
});

export const bootstrap = lifecycles.bootstrap;
export const mount = lifecycles.mount;
export const unmount = lifecycles.unmount;

在要使用到的地方注册观察者函数即可

import {Component, OnInit} from '@angular/core';
import actions from "../shared/actions";

@Component({
  selector: '#desk app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit{
  title = 'csdeskweb-desk';

  ngOnInit(): void {
    // 注册观察者函数
    // onGlobalStateChange 第二个参数为 true,表示立即执行一次观察者函数
    actions.onGlobalStateChange(state => {
      console.log('csdeskweb-desk获取到信息', state);
    }, true);
  }
}

上述信息共享方式是基于Actions通信 适用于较为简单的通信

若要使用到较为复杂的带状态的通信可以参考shared通信

文章地址

https://juejin.cn/post/6844904151231496200#heading-2

 类似资料: