Skip to content

记录一次微前端实践

 at 07:41(Updated)

技术选型

项目搭建

主应用配置

  1. 安装 qiankun
  2. 增加子应用加载入口组件 micro-app.component.ts
    import { Component, ElementRef, OnInit } from '@angular/core';
    import { ActivatedRoute } from '@angular/router';
    import { MicroApp, loadMicroApp } from 'qiankun';
    
    @Component({
      selector: 'micro-app',
      template: `
        <div id="haydnSccMicroApp"></div>
      `,
    })
    export class MicroAppComponent {
      public loading = true;
      public microApp: MicroApp;
    
      public port = this.route.snapshot.data.port;
      public name = this.route.snapshot.data.name;
    
      constructor(
        private el: ElementRef,
        private route: ActivatedRoute
      ) {}
    
      ngAfterViewInit() {
        this.microApp = loadMicroApp(
          {
            name: this.name,
            entry: this.getSubAppEntry(),
            container: this.el.nativeElement,
          },
          {
            singular: true,
          },
          {
            afterMount: () => {
              this.loading = false;
              return Promise.resolve();
            },
          }
        );
      }
    
      ngOnDestroy() {
        this.microApp.unmount();
      }
    
      getSubAppEntry() {
        // 开发模式加载子应用
        if (location.hostname === 'localhost') {
          const { hostname } = location;
          return `//${hostname}:${this.port}`;
        }
    
        // 线上环境根据项目部署方式调整
      }
    }
    
  3. 路由配置
    import { MicroAppComponent } from './micro-app.component';
    
    const defaultRoutes: Routes = [
     {
       path: 'sub-app',
       canActivateChild: [],
       children: [
         {
           path: '**',
           component: MicroAppComponent,
           data: { port: '4200', name: 'sub-app' }, // name值与子应用package.json中name对应
         },
       ],
     },
    ]
    
  4. 在入口启动 qiankun
    import { start } from 'qiankun';
    
    // start();
    // 这里踩坑了,具体问题参考 https://github.com/single-spa/single-spa-angular/issues/529#issuecomment-2503746846
    start({ urlRerouteOnly: false });
    

子应用配置

  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__;
    }
    
  2. 修改 main.ts
    // 以下方式从实践来看存在变更检测的问题
    // import './public-path';
    // import { NgModuleRef, enableProdMode } 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, { ngZone: (window as any).ngZone })
    //     .catch(err => {});
    // }
    
    // if (!(window as any).__POWERED_BY_QIANKUN__) {
    //   render();
    // }
    
    // export async function bootstrap(props: any) {}
    
    // export async function mount(props: any) {
    //   render();
    // }
    
    // export async function unmount(props: any) {
    //   if (app) {
    //     app.destroy();
    //   }
    // }
    
    import './public-path';
    import { enableProdMode, NgZone } from '@angular/core';
    import { Router } from '@angular/router';
    import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
    import { singleSpaAngular, getSingleSpaExtraProviders } from 'single-spa-angular';
    
    import { AppModule } from './app/app.module';
    import { environment } from './environments/environment';
    
    if (environment.production) {
      enableProdMode();
    }
    
    if (!(window as any).__POWERED_BY_QIANKUN__) {
      platformBrowserDynamic()
        .bootstrapModule(AppModule)
        .catch(err => {});
    }
    
    const { bootstrap, mount, unmount } = singleSpaAngular({
      bootstrapFunction: singleSpaProps => {
        return platformBrowserDynamic(getSingleSpaExtraProviders()).bootstrapModule(AppModule);
      },
      template: '<sub-app-root />',
      Router,
      NgZone,
    });
    
    export { bootstrap, mount, unmount };
    
  3. 修改 src\polyfills.ts,注释掉 zone.js 的引入
  4. 修改 webpack 配置
    const appName = require('../package.json').name;
    const DefinePlugin = require('webpack/lib/DefinePlugin');
    const { merge } = require('webpack-merge');
    
    module.exports = (angularWebpackConfig, options) => {
      const config = {
        devServer: {
          headers: {
            'Access-Control-Allow-Origin': '*',
          },
        },
        output: {
          library: `${appName}-[name]`,
          libraryTarget: 'umd',
          chunkLoadingGlobal: `webpackJsonp_${appName}`,
        },
      };
    
      const mergedConfig = merge(angularWebpackConfig, config);
      return mergedConfig;
    };
    
  5. 修改路由配置
    import { NgModule } from '@angular/core';
    import { Routes, RouterModule } from '@angular/router';
    import { LayoutEmptyComponent } from '../layout/empty/empty.component';
    
    const routes: Routes = [
      {
        path: 'sub-app', // 此处需要定义与主项目中相同的路由
        children: [],
      },
      {
        path: '**', // 必须要定义,否则匹配不到路由时会报错
        component: LayoutEmptyComponent,
      },
    ];
    
    @NgModule({
      imports: [
        RouterModule.forRoot(routes, {
          useHash: true,
          scrollPositionRestoration: 'top',
        }),
      ],
      exports: [RouterModule],
      // 为什么不采用下面的方式,而是重复定义与主项目相同的路由名称?
      // 通过 routerLink 跳转时会自动带上 /sub-app 前缀,要跳转到非当前子应用路由时路径会错误,而使用上面的方法可以直接使用routerLink跳转到非当前子应用定义的路由页面
      providers: [
       { provide: APP_BASE_HREF, useValue: window.__POWERED_BY_QIANKUN__ ? '/sub-app' : '/' },
     ],
    })
    export class RouteRoutingModule {}