关于微前端实现原理与 ngx-planet(二)

本贴最后更新于 1396 天前,其中的信息可能已经时移世异

道标

准备好源码,然后跟着文章去看代码,在每个代码块的第一行,我都把 filename 写上了,并且打开了 gitalk。

项目结构

- packages/planet
|--src
    |--application
        |--planet-application-loader.spec.ts
        |--planet-application-loader.ts # 应用加载器
        |--planet-application-ref.spec.ts
        |--planet-application-ref.ts # 应用的引用
        |--planet-application.service.spec.ts
        |--planet-application.service.ts # 应用逻辑处理Service
        |--portal-application.spec.ts
        |--portal-application.ts  # Portal应用
    |--component
        |--planet-component-loader.spec.ts
        |--planet-component-loader.ts # 组件加载器
        |--planet-component-ref.ts # 组件引用
        |--plant-component.config.ts # 组件配置
    |--empty
        |--empty.component.spec.ts
        |--empty.component.ts   # 空组件
    |--testing
        |--app1.module.ts # 测试用例
        |--app2.module.ts
        |--applications.ts
        |--index.ts
        |--utils.ts
    |--assets-loader.spec.ts
    |--assets-loader.ts    # 静态资源加载器
    |--global-event-dispatcher.spec.ts
    |--global-event-dispatcher.ts   # 全局事件调度器
    |--global-planet.spec.ts
    |--global-planet.ts     # 一些函数
    |--helper.spec.ts
    |--helper.ts    # 一些util函数
    |--module.spec.ts
    |--module.ts    # packager module
    |--planet.class.ts  # 注入配置和InjectToken
    |--planet.spec.ts
    |--planet.ts    # planet 对象,包含了注册,启动设置信息,实质为service
    |--public-api.ts  # 桶
    |--test.ts # 测试
|--karma.config.js  # test config
|--ng-package.json  # packager scheme
|--pakcage.json     # npm
|--tsconfig.lib.json    # compiler config
|--tsconfig.lib.prod.json # env=prod compiler config
|--tsconfig.spec.json   # test copiler config
|--tslint.json  # code lint rules

从 Portal 的 AppComponent 开始

// appcomponent.ts
    // 首先注入了 planet 对象
    constructor(
        private planet: Planet,
    ) {}
// appcomponent.ts
    // 初始化AppComponent中配置Portal和Applications
   ngOnInit() {
       ...
   }
// appcomponent.ts
// 设置PlanetApplicationLoader应用加载器的options
this.planet.setOptions({
  // switchMode
  switchMode: SwitchModes.coexist,
  // Application资源加载错误处理回调函数
  errorHandler: (error) => {
    // thy组件库的通知组件,理解成alert吧
    this.thyNotify.error(`错误`, "加载资源失败");
  },
});
// planet.class.ts
// SiwtchModes枚举类,切换子应用的模式,默认切换会销毁,设置 coexist 后只会隐藏
export enum SwitchModes {
  default = "default",
  coexist = "coexist",
}
// planet-application-loader.ts
    // Injectable直接注到模块里了(唯一),项目启动会通过Factory自行创建,所以不需要初始化
    @Injectable({
        providedIn: 'root'
    })
    export class PlanetApplicationLoader {
    private firstLoad = true;

    private startRouteChangeEvent: PlanetRouterEvent;

    // 这里是ApplicationLoader中的option
    private options: PlanetOptions

    ......

    }
// appcomponent.ts
// 向planet注册了两个应用
this.planet.registerApps([
  {
    // 子应用名
    name: "app1",
    // 应用渲染的容器元素, 指定子应用显示在哪个元素内部
    hostParent: "#app-host-container",
    // 宿主元素的 Class,也就是在子应用启动组件上追加的样式
    hostClass: appHostClass,
    // 子应用路由路径前缀,根据这个匹配应用
    routerPathPrefix: /\/app1|app4/,
    // 脚本和样式文件路径前缀,多个脚本可以避免重复写同样的前缀
    resourcePathPrefix: "/static/app1/",
    // 是否启用预加载,启动后刷新页面等当前页面的应用渲染完毕后预加载子应用
    preload: settings.app1.preload,
    // 切换子应用的模式,默认切换会销毁,设置 coexist 后只会隐藏
    switchMode: settings.app1.switchMode,
    // 是否串行加载脚本静态资源
    loadSerial: true,
    // 样式前缀
    stylePrefix: "app1",
    // 脚本资源文件
    scripts: ["main.js"],
    // 样式资源文件
    styles: ["styles.css"],
    // 应用程序打包后的脚本和样式文件替换
    manifest: "/static/app1/manifest.json",
    // 附加数据,主要应用于业务,比如图标,子应用的颜色,显示名等个性化配置
    extra: {
      name: "应用1",
      color: "#ffa415",
    },
  },
  .......
]);

在看 planet.registerApps 之前先看一下 GlobalPlanet

// global-planet.ts
// 浏览器window对象
declare const window: any;

// interface
export interface GlobalPlanet {
  // 这里是一个Map,key是String类型的,Value为应用的引用
  apps: { [key: string]: PlanetApplicationRef };
  // 基座模式,只需要一个Portal
  portalApplication?: PlanetPortalApplication;
  // 应用加载器
  applicationLoader?: PlanetApplicationLoader;
  // service,函数
  applicationService?: PlanetApplicationService;
}

// 全局变量,如果window.planet不为空那么就拿window.planet的值,不然就初始化一个app的map出来
export const globalPlanet: GlobalPlanet = (window.planet = window.planet || {
  apps: {},
});

// 先看一眼,下面会在回来看
export function defineApplication(
  name: string,
  options: BootstrapAppModule | BootstrapOptions
) {
  if (globalPlanet.apps[name]) {
    throw new Error(`${name} application has exist.`);
  }
  if (isFunction(options)) {
    options = {
      template: "",
      bootstrap: options as BootstrapAppModule,
    };
  }
  const appRef = new PlanetApplicationRef(name, options as BootstrapOptions);
  globalPlanet.apps[name] = appRef;
}

为了继续看 defineApplication 先看一个用例,这个用例是 app1main.ts 中的

// app1的 main.ts
// 调用defineApplication 传入
// 1. app1,
// 2.函数返回 ModuleRef 启动的模块
defineApplication('app1', {

    // template 为一个dom字符串
    template: `<app1-root class="app1-root"></app1-root>`,

    // bootstrap 应用的加载点
    bootstrap: (portalApp: PlanetPortalApplication) => {

    // platformBrowserDynamic 传入 Provider, Provider同于 app.module中的providers 提供服务用的,作者在这里插入了两个 值或对象
    >>>>  export declare const platformBrowserDynamic: (extraProviders?: StaticProvider[] | undefined) => PlatformRef;
        return platformBrowserDynamic([
            // 其实提供服务也可以理解成有一个Context,把Value注入到Context中了,在项目中的Context中可以通过Inject方式来提取使用这个服务了
            {
                provide: PlanetPortalApplication,
                // 注意这里传入的是 defineApplication 函数 第二参数options中 BootstrapAppModule中的回调参数
                useValue: portalApp
            },
            ...
        ])
        // 引导AppModule模块,创建@NgModule实例也就是创建一个AppModule出来
            .bootstrapModule(AppModule)
            .then(appModule => {
                // 最后返回Promise<NgModule>
                // 至此,第二参中的BootstrapAppModule结束
                return appModule;
            })
            .catch(error => {
                console.error(error);
                return null;
            });
    }
});

好,现在返回 defineApplication 中去看看用两个参数做了些什么呢

// planet-applicaiton.ref.ts

// 这是第二参的类型
export interface BootstrapOptions {
  template: string;
  bootstrap: BootstrapAppModule;
}

// 第二参数中 bootstrap 要传入portalApp然后返回NgModuldeRef
export type BootstrapAppModule = (
  portalApp?: PlanetPortalApplication
) => Promise<NgModuleRef<any>>;

上面已经讲过怎么用了,下面看当调用时,包内发生了什么?

// global-planet.ts
export function defineApplication(
  name: string,
  options: BootstrapAppModule | BootstrapOptions
) {
  // 首先,看看globalPlanet的app map中有没有加入过这个项目,也就是说,如果有10个应用,调用defineApplication时传入相同的第一参name,就会发生应用重复,无法分别当前需要加载的是哪个应用,作者在这里判断并抛出异常
  if (globalPlanet.apps[name]) {
    throw new Error(`${name} application has exist.`);
  }
  // 其次,看options第二参数,是带template的Bootstrap函数还是直接就是bootstrap函数
  if (isFunction(options)) {
    // 如果是函数,那么template为空,第二参数转化类型,options备用
    options = {
      template: "",
      bootstrap: options as BootstrapAppModule,
    };
  }
  // 这里比较关键了,通过name和options new了一个PlanetApplicationRef对象
  const appRef = new PlanetApplicationRef(name, options as BootstrapOptions);
  // 将appRef也就是子applicaiton的引用加入globalPlanet.apps的map中,也就是window.planet.apps中,至于为什么,前面有提到
  globalPlanet.apps[name] = appRef;
}

看看如何 new 一个子应用引用对象吧~

// planet-application-ref.ts

export class PlanetApplicationRef{
    ...

private innerSelector: string;
// 这里 将 innerSelector调用方式改为了 app.selector因为是私有变量嘛
public get selector() {
    return this.innerSelector;
}

// 首先,构造函数两个参数由defineApplication传递过来
 constructor(name: string, options: BootstrapOptions) {
     // 传递一些值给当前对象的属性
        this.name = name;
        if (options) {
            this.template = options.template;
            // 如果template被传入了 getTagNameByTemplate,由于是util不在赘述,这里拿到了标签名称,稍后看一个应用demo
            this.innerSelector = this.template ? getTagNameByTemplate(this.template) : null;
            // 将Promise<NgModuleRef>也就是bootstrap Instance也传进来
            this.appModuleBootstrap = options.bootstrap;
    }
 }

作者每一个属性名和变量的词语都非常精准,所以导致看代码的时候非常好看,非常干净,这也是 clean code 第一章所追求的,我所追求的代码风格,非常棒

看一个 selecor 应用 demo

// planet-application-loader.ts
// 首先通过name获取ApplicationRef引用
  private hideApp(planetApp: PlanetApplication) {
        const appRef = getPlanetApplicationRef(planetApp.name);
        // 获取插入的整体dom节点的document,通过selector
        const appRootElement = document.querySelector(appRef.selector || planetApp.selector);
        // 隐藏dom元素
        if (appRootElement) {
            appRootElement.setAttribute('style', 'display:none;');
        }
    }

好了,至此注册内容结束,总体来说就是把 name 和 template 放到 window 下的对象中的 map 里,在提供一个 boostrap 和 angular 的 main.ts 结合起来,将 instance 也存入 map 中

再次回到 Portal 的 appcomponent,这次看细致一些

// portal appcomponent.ts
// 注册了planet
    constructor(
        private planet: Planet,
    ) {}

看看初始化 plant 的时候,plant 内部是什么样子的

// planet.ts
// injectable -> context
@Injectable({
    providedIn: 'root'
})
export class Planet {
    ...

// 看这里,当factory向context注入Planet时,发生了什么
    constructor(
        private injector: Injector,
        private router: Router,
        // angular 提供InjectToken方式注入值
        @Inject(PLANET_APPLICATIONS) @Optional() planetApplications: PlanetApplication[]
        // 为了解释,插入一段module.ts的代码
---NgxPlanetModule : module.ts
// 放出NgxPlanetModule
export class NgxPlanetModule {
    // 引入模块既可以直接引入也可以NgxPlanetModule.forRoot(apps)
    // apps为静态的PlanetApplication集合,其实就是appcomponent中register的那个集合
    // 标识为PLANET_APPLICATIONS理解成字符串就好了
    // Inject(PLANET_APPLICATIONS)也就是拿到外部模块引用forRoot函数中传入的结合
    static forRoot(apps: PlanetApplication[]): ModuleWithProviders<NgxPlanetModule> {
        return {
            ngModule: NgxPlanetModule,
            providers: [
                {
                    provide: PLANET_APPLICATIONS,
                    useValue: apps
                }
            ]
        };
    }
}
---
// 回到planet.ts
    ) {
        // 通过injector从context里提取出injectable过的service,注意,全局唯一service
        if (!this.planetApplicationService) {
            // 注意,这里service给了谁了,给了global下的applicationService,也就是window下的那个对象,这里可以点setApplicationService去看,前面看过globalPlanet了
            setApplicationService(this.injector.get(PlanetApplicationService));
        }
        // 如果forRoot传入了apps,那么注册
        if (planetApplications) {
            // 调用注册函数
            this.registerApps(planetApplications);
        }
    }


    // 由于construct中用injector获得了全局planetApplicationService实例,所以这里直接可以用了
    registerApps<TExtra>(apps: PlanetApplication<TExtra>[]) {
        // 调用service的注册,将apps在向下传一层
        this.planetApplicationService.register(apps);
    }

}

看一下 service 中的代码

// planet-application.service.ts
@Injectable({
  providedIn: "root",
})
export class PlanetApplicationService {
  // PlanetApplication集合
  private apps: PlanetApplication[] = [];
  // PlanetApplication map
  private appsMap: { [key: string]: PlanetApplication } = {};

  constructor(private http: HttpClient, private assetsLoader: AssetsLoader) {
    // 首先检测 global下的applicationService是不是已经有了,有了就抛异常,因为这里存的是apps列表,所以不能刷新他的引用使它变成新的对象,不然存储的注册application就没有了
    if (getApplicationService()) {
      throw new Error(
        "PlanetApplicationService has been injected in the portal, repeated injection is not allowed"
      );
    }
  }

  // 注册
  register<TExtra>(
    appOrApps: PlanetApplication<TExtra> | PlanetApplication<TExtra>[]
  ) {
    // 单个application转数组,多个application直接返回数组
    const apps = coerceArray(appOrApps);
    apps.forEach((app) => {
      // 判断是否注册过了application了
      if (this.appsMap[app.name]) {
        throw new Error(`${app.name} has be registered.`);
      }
      // 将application加入apps数组和map中
      this.apps.push(app);
      this.appsMap[app.name] = app;
    });
  }
}

至此,模块启动实例(bootstrap|@NgModule)和注册从 main.ts,portal 的 appcomponent.ts 解析完毕
无论是启动实例或是应用列表,都存在了 global 下面,前一种存到了 apps 下,后一个,存到了 applicationService 的数组和 map 中

第三次回到 portal 的 appcomponent

register 下一行,调用了 start

// appcomponent.ts
// 调用planet的start
this.planet.start();

看看如何写的,这里我加入了一些 console 通过输出的内容分析一下

// plant.ts
start() {
        this.subscription = this.router.events
            .pipe(
                filter(event => {
                    return event instanceof NavigationEnd;
                }),
                map((event: NavigationEnd) => {
                    return event.urlAfterRedirects || event.url;
                }),
                startWith(location.pathname),
                distinctUntilChanged(),
            )
            .subscribe((url: string) => {
                console.log(url)
                this.planetApplicationLoader.reroute({
                    url: url
                });
            });
    }

以下是输出内容

// construct是我在planetApplicationLoader构造函数里加入的console
// 通过顺序验证了,当factory创建好了planetApplicationLoader后会立即执行构造函数中的内容,所以construct排第一
construct
// subscript真正订阅到的路由字符串
planet.ts:103 /about

然后使用 planetApplicationLoader.reroute({}) 去改变了 planetApplicationLoader 中的 private routeChange$ = new Subject<PlanetRouterEvent>();

由于 construct 先运行,所以先来看 planetApplicationLoader 的构造函数进行了什么操作

// planet-application-loader.ts


 constructor(
     // assetsLoader先不考虑,我猜大概是对应了angular静态资源目录进行操作的一个对象
        private assetsLoader: AssetsLoader,
        // planetApplicationService 前面看到的service
        // 里面有register和app列表
        private planetApplicationService: PlanetApplicationService,
        // ngZone https://hijiangtao.github.io/2020/01/17/Angular-Zone-Concepts/ 可以看这篇帖子,主要是关于变更检测机制的一个东西
        // 简单来说 run runOutsideAngular两个函数,一个是进行变更检测,一个在zone之外运行不触发变更检测
        private ngZone: NgZone,
        // 路由
        router: Router,
        // injector
        injector: Injector,
        // 对页面上运行的angular应用程序的引用
        applicationRef: ApplicationRef
    ) {
        // 这里类似planetApplicationService的检测,检测global中是否已经存在了loader加载器
        // 那么ApplicationLoader什么时候被创建的呢,看看之前planetApplicationService在哪里创建的,它上面就是setApplicationLoader
        if (getApplicationLoader()) {
            throw new Error(
                'PlanetApplicationLoader has been injected in the portal, repeated injection is not allowed'
            );
        }

        // 设置一些参数
        this.options = {
            // 应用是隐藏还是销毁
            switchMode: SwitchModes.default,
            // 错误处理回调函数
            errorHandler: (error: Error) => {
                console.error(error);
            }
        };
        // 将zone传递给portalApp
        this.portalApp.ngZone = ngZone;
        // 当前application的引用传递给portalApp
        this.portalApp.applicationRef = applicationRef;
        // 路由传递
        this.portalApp.router = router;
        // 这里我理解为把注入器传过来了,也就可以用injector来拿当前context的对象了
        // 可能理解有误,请在下方留言告知
        this.portalApp.injector = injector;
        // 一会儿在看如何共享事件调用的
        this.portalApp.globalEventDispatcher = injector.get(GlobalEventDispatcher);
        // 将portalApp给global对象了
        globalPlanet.portalApplication = this.portalApp;
        // 调用了一个关于路由的函数
        this.setupRouteChange();
    }

先看 this.portalApp 是什么

//  planet-application-loader.ts
    private portalApp = new PlanetPortalApplication();

new 了一个 portalApplication 对象对吧,这就是我们的基座 application 了,看看里面有什么

// portal-application.ts
export class PlanetPortalApplication<TData = any> {
  // 当前应用的引用
  applicationRef: ApplicationRef;
  // 注入器
  injector: Injector;
  // 路由
  router: Router;
  // 变更检测
  ngZone: NgZone;
  // 事件分发器
  globalEventDispatcher: GlobalEventDispatcher;
  // 额外数据?
  data: TData;

  // 跳转函数,返回了一个Promise<boolean>
  navigateByUrl(
    url: string | UrlTree,
    extras?: NavigationExtras
  ): Promise<boolean> {
    return this.ngZone.run(() => {
      return this.router.navigateByUrl(url, extras);
    });
  }

  // ngZone.run 在变更检测区域内运行函数,这里不知道为何不 object.ngZone.run而是要写一个函数来包装
  run<T>(fn: (...args: any[]) => T): T {
    return this.ngZone.run<T>(() => {
      return fn();
    });
  }

  // 全局性调用变化检测
  // applicationRef可以通过attachView()将视图包含到变化检测中
  // 可以用detachView()将视图移除变化检测
  tick() {
    this.applicationRef.tick();
  }
}

好了,接下来看加载应用的核心函数 setupRouteChange 这个函数有点长

// planet-application-loader.ts

 private setupRouteChange() {
        // 当路由变化时,先思考,application-loader比start调用先创建,所以start后,页面定到/about路由,所以这里可以接收到/about
        this.routeChange$
            .pipe(
                distinctUntilChanged((x, y) => {
                    return (x && x.url) === (y && y.url);
                }),
                // 和其他打平操作符的主要区别是它具有取消效果。在每次发出时,会取消前一个内部 observable (你所提供函数的结果) 的订阅,然后订阅一个新的 observable
                switchMap(event => {
                    return of(event).pipe(
                        // 卸载应用程序并返回应加载的应用程序
                        map(() => {
                            // 这里拿到{url:'/about'}
                            this.startRouteChangeEvent = event;
                            // 通过url找app ,PlanetApplication类型,用service中的app列表和 注册过的app的routerPathPrefix和url进行匹配
                            const shouldLoadApps = this.planetApplicationService.getAppsByMatchedUrl(event.url);
                            // 这里拿到了需要卸载的应用,下面有讲
                            const shouldUnloadApps = this.getUnloadApps(shouldLoadApps);
                            // 设置加载和卸载的application,这里传入了加载应用和卸卸载应用,并且放出了ob对象,估计是作者提供hook函数,想提供给我们在loading时做一些操作的自定义函数的hook
                            // 使用的时候 planet.appsLoadingStart.subscribe就可以拿到了
                            this.appsLoadingStart$.next({
                                shouldLoadApps,
                                shouldUnloadApps
                            });
                            // 卸载,下面有讲
                            this.unloadApps(shouldUnloadApps, event);
                            return shouldLoadApps;
                        }),
                        // 加载静态资源
                        switchMap(shouldLoadApps => {
                            let hasAppsNeedLoadingAssets = false;
                            const loadApps$ = shouldLoadApps.map(app => {
                                // 获取当前app的状态
                                const appStatus = this.appsStatus.get(app);
                                //  如果没有加载过,或者曾经加载错了
                                // 设置需要加载状态后使用assetsLoader去加载静态资源,稍后讲assetsLoader
                                if (
                                    !appStatus ||
                                    appStatus === ApplicationStatus.assetsLoading ||
                                    appStatus === ApplicationStatus.loadError
                                ) {
                                    hasAppsNeedLoadingAssets = true;
                                    return this.ngZone.runOutsideAngular(() => {
                                        return this.startLoadAppAssets(app);
                                    });
                                } else {
                                    return of(app);
                                }
                            });
                            if (hasAppsNeedLoadingAssets) {
                                this.loadingDone = false;
                            }
                            return loadApps$.length > 0 ? forkJoin(loadApps$) : of([] as PlanetApplication[]);
                        }),
                        // 引导启动或展示application
                        map(apps => {
                            const apps$: Observable<PlanetApplication>[] = [];
                            apps.forEach(app => {
                                // 获取app状态
                                const appStatus = this.appsStatus.get(app);
                                // 如果引导过了,也就是拿到过context了
                                if (appStatus === ApplicationStatus.bootstrapped) {
                                    apps$.push(
                                        of(app).pipe(
                                            tap(() => {
                                                // 展示app
                                                this.showApp(app);
                                                // 获取app引用
                                                const appRef = getPlanetApplicationRef(app.name);
                                                // 路由跳转 router是从 applicationRef也就是子application中 inject出来的router,所以路由表完备
                                                appRef.navigateByUrl(event.url);
                                                // 设置app状态为激活状态
                                                this.setAppStatus(app, ApplicationStatus.active);
                                                // 加载结束
                                                this.setLoadingDone();
                                            })
                                        )
                                    );
                                } else if (appStatus === ApplicationStatus.assetsLoaded) {
                                    // 如果appStatus状态为静态资源加载完毕
                                    apps$.push(
                                        of(app).pipe(
                                            switchMap(() => {
                                                // 引导app获取app context中的一些对象
                                                return this.bootstrapApp(app).pipe(
                                                    map(() => {
                                                        // 设置app模式为激活状态
                                                        this.setAppStatus(app, ApplicationStatus.active);
                                                        // 加载完毕
                                                        this.setLoadingDone();
                                                        return app;
                                                    })
                                                );
                                            })
                                        )
                                    );
                                    // 如果应用未激活状态
                                } else if (appStatus === ApplicationStatus.active) {
                                    apps$.push(
                                        of(app).pipe(
                                            tap(() => {
                                                // 获取引用
                                                const appRef = getPlanetApplicationRef(app.name);
                                                // 获取当前路径
                                                const currentUrl = appRef.getCurrentRouterStateUrl? appRef.getCurrentRouterStateUrl(): '';
                                                // 如果当前url和 事件url不一致
                                                if (currentUrl !== event.url) {
                                                    // 路由跳转
                                                    appRef.navigateByUrl(event.url);
                                                }
                                            })
                                        )
                                    );
                                } else {
                                    throw new Error(
                                        `app(${app.name})'s status is ${appStatus}, can't be show or bootstrap`
                                    );
                                }
                            });

                            if (apps$.length > 0) {
                                // 切换到应用后会有闪烁现象,所以使用 setTimeout 后启动应用
                                setTimeout(() => {
                                    // 此处判断是因为如果静态资源加载完毕还未启动被取消,还是会启动之前的应用,虽然可能性比较小,但是无法排除这种可能性,所以只有当 Event 是最后一个才会启动
                                    if (this.startRouteChangeEvent === event) {
                                        this.ngZone.runOutsideAngular(() => {
                                            // 将apps中的ob们合并到一起执行后
                                            forkJoin(apps$).subscribe(() => {
                                                // 设置加载完成状态
                                                this.setLoadingDone();
                                                // 然后去搞预加载,注册时传入的参数preload=tue/false
                                                this.ensurePreloadApps(apps);
                                            });
                                        });
                                    }
                                });
                            } else {
                                // 设置
                                this.ensurePreloadApps(apps);
                                this.setLoadingDone();
                            }
                        }),
                        // 使用自定义异常hook function
                        catchError(error => {
                            this.errorHandler(error);
                            return [];
                        })
                    );
                })
            )
            .subscribe();
    }

然后看 unload,bootstrap,destory,preload,show,hide,卸载,加载,销毁,预加载,展示,隐藏相关的这几个函数

获取卸载 app

// planet-applicaiton-loader.ts
// 传入激活的application列表
 private getUnloadApps(activeApps: PlanetApplication[]) {
        const unloadApps: PlanetApplication[] = [];
        this.appsStatus.forEach((value, app) => {
            // application状态为激活,但是激活列表中没有这个application
            if (value === ApplicationStatus.active && !activeApps.find(item => item.name === app.name)) {
                // 那么放入待卸载应用集合里
                unloadApps.push(app);
            }
        });
        return unloadApps;
    }

卸载

// planet-applicaiton-loader.ts
// 卸载applications,第一参为application集合,第二个参数类似{url:'/about'}
private unloadApps(shouldUnloadApps: PlanetApplication[], event: PlanetRouterEvent) {
        const hideApps: PlanetApplication[] = [];
        const destroyApps: PlanetApplication[] = [];
        // 需要卸载的app看看是隐藏模式还是销毁模式
        shouldUnloadApps.forEach(app => {
            // app.SwitchMode参数的两个值,在注册的时候设置的
            // coexist模式
            if (this.switchModeIsCoexist(app)) {
                hideApps.push(app);
                // 隐藏应用
                this.hideApp(app);
                // 设置状态
                this.setAppStatus(app, ApplicationStatus.bootstrapped);
                // default模式,销毁模式
            } else {
                destroyApps.push(app);
                // 销毁之前先隐藏,否则会出现闪烁,因为 destroy 是延迟执行的
                // 如果销毁不延迟执行,会出现切换到主应用的时候会有视图卡顿现象
                this.hideApp(app);
                //
    ---
    // application应用状态有5个
    // 资源加载状态分别是,我估计这里和预加载或者做判断有用到
>>  assetsLoading = 1,
    // 资源被加载状态
    assetsLoaded = 2,
    // 正在引导启动状态
    bootstrapping = 3,
    // 已经被引导启动状态
    bootstrapped = 4,
    // 激活状态
    active = 5,
    // 加载失败
    loadError = 10
    ---
                this.setAppStatus(app, ApplicationStatus.assetsLoaded);
            }
        });

        // 如果隐藏列表或销毁列表中有值
        if (hideApps.length > 0 || destroyApps.length > 0) {
            // 从其他应用切换到主应用的时候会有视图卡顿现象,所以先等主应用渲染完毕后再加载其他应用
            // 此处尝试使用 this.ngZone.onStable.pipe(take(1)) 应用之间的切换会出现闪烁
            setTimeout(() => {
                // 这里还不太理解
                hideApps.forEach(app => {
                    const appRef = getPlanetApplicationRef(app.name);
                    if (appRef) {
                        appRef.navigateByUrl(event.url);
                    }
                });
                // 销毁app
                destroyApps.forEach(app => {
                    this.destroyApp(app);
                });
            });
        }
    }

销毁

// planet-application-loader.ts
// 传入需要销毁的application
 private destroyApp(planetApp: PlanetApplication) {
     // 得到applicationRef的引用
        const appRef = getPlanetApplicationRef(planetApp.name);
        if (appRef) {
            // 销毁
            appRef.destroy();
        }
        // 删除 document节点
        const container = getHTMLElement(planetApp.hostParent);
        const appRootElement = container.querySelector((appRef && appRef.selector) || planetApp.selector);
        if (appRootElement) {
            container.removeChild(appRootElement);
        }
    }

显示和隐藏

// planet-application-loader.ts
    private hideApp(planetApp: PlanetApplication) {
        const appRef = getPlanetApplicationRef(planetApp.name);
        // 拿到dom节点
        const appRootElement = document.querySelector(appRef.selector || planetApp.selector);
        if (appRootElement) {
            // css隐藏
            appRootElement.setAttribute('style', 'display:none;');
        }
    }

    private showApp(planetApp: PlanetApplication) {
        const appRef = getPlanetApplicationRef(planetApp.name);
        // 拿到dom节点
        const appRootElement = document.querySelector(appRef.selector || planetApp.selector);
        // 去除隐藏样式
        if (appRootElement) {
            appRootElement.setAttribute('style', '');
        }
    }

预加载

// planet-applicatin-loader.ts
 private ensurePreloadApps(activeApps?: PlanetApplication[]) {
        // 第一次加载的时候 对app进行预加载
        if (this.firstLoad) {
            this.preloadApps(activeApps);
            this.firstLoad = false;
        }
    }

private preloadApps(activeApps?: PlanetApplication[]) {
        setTimeout(() => {
            // 过滤preload为true的application
            const toPreloadApps = this.planetApplicationService.getAppsToPreload(activeApps ? activeApps.map(item => item.name) : null);
            // 加载
            const loadApps$ = toPreloadApps.map(preloadApp => {
                return this.preloadInternal(preloadApp);
            });
            // 对加载过程使用hooks错误处理
            forkJoin(loadApps$).subscribe({
                error: error => this.errorHandler(error)
            });
        });
}


// 第一参数application,第二参是否直接加载
private preloadInternal(app: PlanetApplication, immediate?: boolean): Observable<PlanetApplicationRef> {
        // 获取app状态
        const status = this.appsStatus.get(app);
        // 如果没有状态或者加载错误
        if (!status || status === ApplicationStatus.loadError) {
            return this.startLoadAppAssets(app).pipe(
                switchMap(() => {
                    // 如果是直接加载
                    if (immediate) {
                        // 在隐藏模式下加载
                        return this.bootstrapApp(app, 'hidden');
                    } else {
                        // 如果不是直接加载
                        // 在状态监测之外
                        return this.ngZone.runOutsideAngular(() => {
                            // 加载applicaiton
                            return this.bootstrapApp(app, 'hidden');
                        });
                    }
                }),
                map(() => {
                    // 返回application的引用
                    return getPlanetApplicationRef(app.name);
                })
            );
            // 如果在application属于其他状态
        } else if (
            [ApplicationStatus.assetsLoading, ApplicationStatus.assetsLoaded, ApplicationStatus.bootstrapping].includes(
                status
            )
        ) {
            return this.appStatusChange.pipe(
                filter(event => {
                    return event.app === app && event.status === ApplicationStatus.bootstrapped;
                }),
                take(1),
                map(() => {
                    // 返回引用
                    return getPlanetApplicationRef(app.name);
                })
            );
        } else {
            // 异常
            const appRef = getPlanetApplicationRef(app.name);
            if (!appRef) {
                throw new Error(`${app.name}'s status is ${ApplicationStatus[status]}, planetApplicationRef is null.`);
            }
            return of(appRef);
        }
    }

bootstrap

// planet-application-loader.ts
 private bootstrapApp(
        app: PlanetApplication,
        defaultStatus: 'hidden' | 'display' = 'display'
    ): Observable<PlanetApplicationRef> {
        // 设置状态正在加载中
        this.setAppStatus(app, ApplicationStatus.bootstrapping);
        // 获取app的引用
        const appRef = getPlanetApplicationRef(app.name);
        // 如果注册了且boostrap函数不为空
        if (appRef && appRef.bootstrap) {
            // 获取 宿主dom
            const container = getHTMLElement(app.hostParent);
            let appRootElement: HTMLElement;
            // 进行显示隐藏,加入前缀,宿主class的操作
            if (container) {
                appRootElement = container.querySelector(appRef.selector || app.selector);
                if (!appRootElement) {
                    if (appRef.template) {
                        appRootElement = createElementByTemplate(appRef.template);
                    } else {
                        appRootElement = document.createElement(app.selector);
                    }
                    appRootElement.setAttribute('style', 'display:none;');
                    if (app.hostClass) {
                        appRootElement.classList.add(...coerceArray(app.hostClass));
                    }
                    if (app.stylePrefix) {
                        appRootElement.classList.add(...coerceArray(app.stylePrefix));
                    }
                    container.appendChild(appRootElement);
                }
            }
            // 加载
            let result = appRef.bootstrap(this.portalApp);
            if (result['then']) {
                result = from(result) as Observable<PlanetApplicationRef>;
            }
            // 最后返回app引用
            return result.pipe(
                tap(() => {
                    this.setAppStatus(app, ApplicationStatus.bootstrapped);
                    if (defaultStatus === 'display' && appRootElement) {
                        appRootElement.removeAttribute('style');
                    }
                }),
                map(() => {
                    return appRef;
                })
            );
        } else {
            throw new Error(
                `[${app.name}] not found, make sure that the app has the correct name defined use defineApplication(${app.name}) and runtimeChunk and vendorChunk are set to true, details see https://github.com/worktile/ngx-planet#throw-error-cannot-read-property-call-of-undefined-at-__webpack_require__-bootstrap79`
            );
        }
    }

app.Ref.bootstrap()

// planet-application-ref
// 加载应用获取application的引用后返回自身  PlanetApplicationRef类型
 bootstrap(app: PlanetPortalApplication): Observable<this> {
        if (!this.appModuleBootstrap) {
            throw new Error(`app(${this.name}) is not defined`);
        }
        this.portalApp = app;
        // 使用main.ts中传入的bootstrap来加载app,然后拿到模块引用
        return from(
            this.appModuleBootstrap(app).then(appModuleRef => {
                // 传递引用,然后复制一些自己想要的信息从appModuleRef中
                this.appModuleRef = appModuleRef;
                // 传递name
                this.appModuleRef.instance.appName = this.name;
                this.syncPortalRouteWhenNavigationEnd();
                // 返回
                return this;
            })
        );
    }

this.appModuleBootstrap 是通过 main.tsdefineApplication 第二参数的 bootstrap 传入的,之前看过一下

// global-planet.ts
export function defineApplication(
  name: string,
  options: BootstrapAppModule | BootstrapOptions
) {
  if (globalPlanet.apps[name]) {
    throw new Error(`${name} application has exist.`);
  }
  if (isFunction(options)) {
    options = {
      template: "",
      bootstrap: options as BootstrapAppModule,
    };
  }
  // 这里new了一个PlanetApplicationRef然后将引用传入了map中
  const appRef = new PlanetApplicationRef(name, options as BootstrapOptions);
  globalPlanet.apps[name] = appRef;
}

this.appModuleBootstrap

// planet-application-ref.ts
export class PlanetApplicationRef {
    ...
    // 这里是global-planet new的对象时候传入的
    private appModuleBootstrap: (app: PlanetPortalApplication) => Promise<NgModuleRef<any>>;
    ...
    constructor(name: string, options: BootstrapOptions) {
        this.name = name;
        if (options) {
            this.template = options.template;
            this.innerSelector = this.template ? getTagNameByTemplate(this.template) : null;
            // 具体传入
            this.appModuleBootstrap = options.bootstrap;
        }
    }

至此,bootstrap 结束

assetsLoader

之前在核心的 setupRouteChange 走第三步之前,中间有一个加载 assets 的阶段,毕竟子应用的 main.ts 应该是不能主动注册过来的,需要加载静态文件来获取,我是这么猜测的

// planet-application-loader.ts
switchMap((shouldLoadApps) => {
  let hasAppsNeedLoadingAssets = false;
  const loadApps$ = shouldLoadApps.map((app) => {
    const appStatus = this.appsStatus.get(app);
    if (
      !appStatus ||
      appStatus === ApplicationStatus.assetsLoading ||
      appStatus === ApplicationStatus.loadError
    ) {
      hasAppsNeedLoadingAssets = true;
      return this.ngZone.runOutsideAngular(() => {
          // 核心调用,开始加载application的assets
        return this.startLoadAppAssets(app);
      });
    } else {
      return of(app);
    }
  });
  if (hasAppsNeedLoadingAssets) {
    this.loadingDone = false;
  }
  return loadApps$.length > 0
    ? forkJoin(loadApps$)
    : of([] as PlanetApplication[]);
});


 private startLoadAppAssets(app: PlanetApplication) {
        if (this.inProgressAppAssetsLoads.get(app.name)) {
            return this.inProgressAppAssetsLoads.get(app.name);
        } else {
            // assetsLoader.loadAppAssets 核心函数,调用完毕后看是否放入map中
            const loadApp$ = this.assetsLoader.loadAppAssets(app).pipe(
                tap(() => {
                    this.inProgressAppAssetsLoads.delete(app.name);
                    // 设置加载完毕状态
                    this.setAppStatus(app, ApplicationStatus.assetsLoaded);
                }),
                map(() => {
                    return app;
                }),
                catchError(error => {
                    this.inProgressAppAssetsLoads.delete(app.name);
                    // 加载失败状态
                    this.setAppStatus(app, ApplicationStatus.loadError);
                    throw error;
                }),
                share()
            );
            // 放入map中
            this.inProgressAppAssetsLoads.set(app.name, loadApp$);
            // 设置状态为assetsLoading状态,这里要知道返回的ob所以写代码的时候Loaded在Loading上面,真正订阅subscribe的时候,顺序就对了
            this.setAppStatus(app, ApplicationStatus.assetsLoading);
            return loadApp$;
        }
    }

调用了构造器注入的 assetsLoader 看一眼 loader 里咋写的吧

关于 mainfest.json 看一看 https://jonny-huang.github.io/angular/training/19_pwa/ 有关于 PWA 方面的内容

{
  "main.js": "main-es2015.js",
  "main.js.map": "main-es2015.js.map",
  "polyfills-es5.js": "polyfills-es5-es2015.js",
  "polyfills-es5.js.map": "polyfills-es5-es2015.js.map",
  "polyfills.js": "polyfills-es2015.js",
  "polyfills.js.map": "polyfills-es2015.js.map",
  "styles.css": "styles.css",
  "styles.css.map": "styles.css.map"
}
// load结果的接口类型
export interface AssetsLoadResult {
  src: string;
  hashCode: number;
  loaded: boolean;
  status: string;
}

// context注入,全剧唯一
@Injectable({
  providedIn: "root",
})
export class AssetsLoader {
  // 加载过的sources
  private loadedSources: number[] = [];

  // 获取http客户端
  constructor(private http: HttpClient) {}

  // 加载script
  loadScript(src: string): Observable<AssetsLoadResult> {
    // hashCode
    const id = hashCode(src);
    // 通过hashCode判断是否加载过了这个js
    if (this.loadedSources.includes(id)) {
      return of({
        src: src,
        hashCode: id,
        loaded: true,
        status: "Loaded",
      });
    }
    // 然后构建<script type="text/javascript" src=""> 插入dom
    return new Observable((observer: Observer<AssetsLoadResult>) => {
      const script: HTMLScriptElement = document.createElement("script");
      script.type = "text/javascript";
      script.src = src;
      script.async = true;
      if (script["readyState"]) {
        // IE
        script["onreadystatechange"] = () => {
          if (
            script["readyState"] === "loaded" ||
            script["readyState"] === "complete"
          ) {
            script["onreadystatechange"] = null;
            observer.next({
              src: src,
              hashCode: id,
              loaded: true,
              status: "Loaded",
            });
            observer.complete();
            this.loadedSources.push(id);
          }
        };
      } else {
        // Others
        script.onload = () => {
          observer.next({
            src: src,
            hashCode: id,
            loaded: true,
            status: "Loaded",
          });
          observer.complete();
          this.loadedSources.push(id);
        };
      }
      script.onerror = (error) => {
        observer.error({
          src: src,
          hashCode: id,
          loaded: false,
          status: "Error",
          error: error,
        });
        observer.complete();
      };
      document.body.appendChild(script);
    });
  }

  // 加载style  和script一个道理
  loadStyle(src: string): Observable<AssetsLoadResult> {
    const id = hashCode(src);
    if (this.loadedSources.includes(id)) {
      return of({
        src: src,
        hashCode: id,
        loaded: true,
        status: "Loaded",
      });
    }
    return new Observable((observer: Observer<AssetsLoadResult>) => {
      const head = document.getElementsByTagName("head")[0];
      const link = document.createElement("link");
      link.rel = "stylesheet";
      link.type = "text/css";
      link.href = src;
      link.media = "all";
      link.onload = () => {
        observer.next({
          src: src,
          hashCode: id,
          loaded: true,
          status: "Loaded",
        });
        observer.complete();
        this.loadedSources.push(id);
      };
      link.onerror = (error) => {
        observer.error({
          src: src,
          hashCode: id,
          loaded: true,
          status: "Loaded",
          error: error,
        });
        observer.complete();
      };
      head.appendChild(link);
    });
  }

  // 加载script集合
  loadScripts(
    sources: string[],
    serial = false
  ): Observable<AssetsLoadResult[]> {
    if (isEmpty(sources)) {
      return of(null);
    }
    const observables = sources.map((src) => {
      return this.loadScript(src);
    });
    if (serial) {
      const a = concat(...observables).pipe(
        map((item) => {
          return of([item]);
        }),
        concatAll()
      );
      return a;
    } else {
      return forkJoin(observables).pipe();
    }
  }

  // 加载style集合
  loadStyles(sources: string[]): Observable<AssetsLoadResult[]> {
    if (isEmpty(sources)) {
      return of(null);
    }
    return forkJoin(
      sources.map((src) => {
        return this.loadStyle(src);
      })
    );
  }

  // 双重加载
  loadScriptsAndStyles(
    scripts: string[] = [],
    styles: string[] = [],
    serial = false
  ) {
    return forkJoin([
      this.loadScripts(scripts, serial),
      this.loadStyles(styles),
    ]);
  }

  // 加载static资源
  loadAppAssets(app: PlanetApplication) {
    if (app.manifest) {
      // 通过httpClient找到json,通过json找到script路径,最后插入到dom中
      return this.loadManifest(
        `${app.manifest}?t=${new Date().getTime()}`
      ).pipe(
        switchMap((manifestResult) => {
          // 获取了css和js的全路径 app1/{json中的js和css}
          const { scripts, styles } = getScriptsAndStylesFullPaths(
            app,
            manifestResult
          );
          // 然后加载
          return this.loadScriptsAndStyles(scripts, styles, app.loadSerial);
        })
      );
    } else {
      const { scripts, styles } = getScriptsAndStylesFullPaths(app);
      return this.loadScriptsAndStyles(scripts, styles, app.loadSerial);
    }
  }

  // 加载mainfest
  loadManifest(url: string): Observable<{ [key: string]: string }> {
    return this.http.get(url).pipe(
      map((response: any) => {
        return response;
      })
    );
  }
}

到此,javascript 和 stylesheet 加载完毕

组件注册

直接看文件然后找个例子看看

// planet-component-ref.ts
// 引用
export class PlanetComponentRef<TComp = any> {
  // 包装元素
  wrapperElement: HTMLElement;
  // 组件类型
  componentInstance: TComp;
  // 组件引用
  componentRef: ComponentRef<TComp>;
  // 处理函数
  dispose: () => void;
}
// planet-component-config.ts
// 组件配置
export class PlantComponentConfig<TData = any> {
  // 目标容器
  container: HTMLElement | ElementRef<HTMLElement | any>;
  // 包装class
  wrapperClass?: string;
  // 初始化状态
  initialState?: TData | null = null;
}
const componentWrapperClass = 'planet-component-wrapper';

export interface PlanetComponent<T = any> {
    name: string;
    component: ComponentType<T>;
}

@Injectable({
    providedIn: 'root'
})
export class PlanetComponentLoader {
    private domPortalOutletCache = new WeakMap<any, DomPortalOutlet>();

    private get applicationLoader() {
        return getApplicationLoader();
    }

    private get applicationService() {
        return getApplicationService();
    }

    constructor(
        // 当前页面应用的引用
        private applicationRef: ApplicationRef,
        // 模块的引用
        private ngModuleRef: NgModuleRef<any>,
        // 变更检测
        private ngZone: NgZone,
        // document
        @Inject(DOCUMENT) private document: any
    ) {}

    // 获取Planet存储的Application的引用,通过name,返回一个ob<引用>对象
    private getPlantAppRef(name: string): Observable<PlanetApplicationRef> {
        if (globalPlanet.apps[name] && globalPlanet.apps[name].appModuleRef) {
            return of(globalPlanet.apps[name]);
        } else {
            const app = this.applicationService.getAppByName(name);
            return this.applicationLoader.preload(app, true).pipe(
                map(() => {
                    return globalPlanet.apps[name];
                })
            );
        }
    }

    // 传入模块和组件的引用,创建injector
    private createInjector<TData>(
        appModuleRef: NgModuleRef<any>,
        componentRef: PlanetComponentRef<TData>
    ): PortalInjector {
        const injectionTokens = new WeakMap<any, any>([[PlanetComponentRef, componentRef]]);
        const defaultInjector = appModuleRef.injector;
        return new PortalInjector(defaultInjector, injectionTokens);
    }

    // 获取container内component的HTML元素
    private getContainerElement(config: PlantComponentConfig): HTMLElement {
        if (!config.container) {
            throw new Error(`config 'container' cannot be null`);
        } else {
            if ((config.container as ElementRef).nativeElement) {
                return (config.container as ElementRef).nativeElement;
            } else {
                return config.container as HTMLElement;
            }
        }
    }

    // 创建包装元素
    private createWrapperElement(config: PlantComponentConfig) {
        const container = this.getContainerElement(config);
        // 创建div
        const element = this.document.createElement('div');
        // 拿到应用PlantApplication
        const subApp = this.applicationService.getAppByName(this.ngModuleRef.instance.appName);
        // 加入 planet-component-wrapper 在classList中
        element.classList.add(componentWrapperClass);
        // 加入 attribute planet-inline
        element.setAttribute('planet-inline', '');
        // 如果设置中配置了wrapperClass
        if (config.wrapperClass) {
            // 加入
            element.classList.add(config.wrapperClass);
        }
        // 如果注册的时候加入了stylePrefix前缀
        if (subApp && subApp.stylePrefix) {
            // 加入到classList里
            element.classList.add(subApp.stylePrefix);
        }
        // container插入element
        container.appendChild(element);
        return element;
    }

    // 附加组件在页面上
    private attachComponent<TData>(
        plantComponent: PlanetComponent,
        appModuleRef: NgModuleRef<any>,
        config: PlantComponentConfig
    ): PlanetComponentRef<TData> {
        // 创建一个planetComponent引用
        // 注意里面有实例 componentInstance: TComp;
        // 有引用 componentRef: ComponentRef<TComp>;
        const plantComponentRef = new PlanetComponentRef();
        // 组件工厂解析器, 组件工厂解析器可以产生一个组件工厂(ComponentFactory)
        const componentFactoryResolver = appModuleRef.componentFactoryResolver;
        const appRef = this.applicationRef;
        // 创建一个自定义注入器,
        // 向入口组件提供自定义注入token时要使用的自定义注入器
        const injector = this.createInjector<TData>(appModuleRef, plantComponentRef);
        // 创建包装后的element
        const wrapper = this.createWrapperElement(config);
        // 如果有了相同的dom出口
        let portalOutlet = this.domPortalOutletCache.get(wrapper);
        if (portalOutlet) {
            // 那么卸载
            portalOutlet.detach();
        } else {
            // DomPortalOutlet 我理解成,容器?出口?站位dom元素。不知道对不对
            // 传递了element,组件工厂,模块引用,注入器
            portalOutlet = new DomPortalOutlet(wrapper, componentFactoryResolver, appRef, injector);
            // 存储此次添加的 dom出口
            this.domPortalOutletCache.set(wrapper, portalOutlet);
        }
        // 创建一个 填入出口 的 portalComponent 也就是去填占位符的ComponentPortal<自定义组件实例>
        const componentPortal = new ComponentPortal(plantComponent.component, null);
        // ComponentPortal<自定义组件实例> 填入出口中
        const componentRef = portalOutlet.attachComponentPortal<TData>(componentPortal);
        // 如果有初始化state
        if (config.initialState) {
            // 那么混入component的实例中去
            Object.assign(componentRef.instance, config.initialState);
        }
        // 传递一些引用
        plantComponentRef.componentInstance = componentRef.instance;
        plantComponentRef.componentRef = componentRef;
        plantComponentRef.wrapperElement = wrapper;
        // 注册卸载函数
        plantComponentRef.dispose = () => {
            // 删除缓存的出口element
            this.domPortalOutletCache.delete(wrapper);
            // 从dom中清除出口/占位符?
            portalOutlet.dispose();
        };
        // 返回 填上出口的 component引用 PlanetComponentRef 类型
        return plantComponentRef;
    }

    private registerComponentFactory(componentOrComponents: PlanetComponent | PlanetComponent[]) {
        // 获取app名称
        const app = this.ngModuleRef.instance.appName;
        // 拿到当前应用
        this.getPlantAppRef(app).subscribe(appRef => {
--- registerComponentFactory
// 传入匿名函数,函数两个参数:组件名称,config.返回一个PlanetComponentRef
>export type PlantComponentFactory = <TData, TComp>(
    componentName: string,
    config: PlantComponentConfig<TData>
) => PlanetComponentRef<TComp>;
---
                // 将 componentOrComponents转成数组
                const components = coerceArray(componentOrComponents);
                // 找到 与componentName名称一样的组件
                const component = components.find(item => item.name === componentName);
                // 如果存在
                if (component) {
                    // 在变更检测范围内
                    return this.ngZone.run(() => {
                        // 附加组件,然后返回PlanetComponentRef匿名函数结束
                        // 此时,planet-application-ref的factory为注册组件的factory
                        // 然后进行组件附加,传入component,模块引用,组件设置
                        const componentRef = this.attachComponent<any>(component, appRef.appModuleRef, config);
                        return componentRef;
                    });
                } else {
                    throw Error(`unregistered component ${componentName} in app ${app}`);
                }
            });
        });
    }

    // 注册,
    register(components: PlanetComponent | PlanetComponent[]) {
        setTimeout(() => {
            this.registerComponentFactory(components);
        });
    }

    // 加载,传入app名称,组件名称,配置项
    load<TComp = unknown, TData = unknown>(
        app: string,
        componentName: string,
        config: PlantComponentConfig<TData>
    ): Observable<PlanetComponentRef<TComp>> {
        // 获取应用的引用
        const result = this.getPlantAppRef(app).pipe(
            //将源的发射延迟可观察到的时间间隔 由另一个 Observable 的发射确定。
            delayWhen(appRef => {
                if (appRef.getComponentFactory()) {
                    return of();
                } else {
                    // Because register use 'setTimeout',so timer 20
                    return timer(20);
                }
            }),
            map(appRef => {
                // 拿到引用中的组件工厂
                const componentFactory = appRef.getComponentFactory();
                if (componentFactory) {
                    // 这里传入了componentName和config,从而调用了入口出口填补组件的函数`attachComponent`,比较巧妙
                    return componentFactory<TData, TComp>(componentName, config);
                } else {
                    throw new Error(`${app}'s component(${componentName}) is not registered`);
                }
            }),
            finalize(() => {
                // 变更检测
                this.applicationRef.tick();
            }),
            shareReplay()
        );
        result.subscribe();
        return result;
    }
}

事件注册

简单过一下事件注册

// global-event-dispatcher.ts
export interface GlobalDispatcherEvent {
  name: string;
  payload: any;
}

const CUSTOM_EVENT_NAME = "PLANET_GLOBAL_EVENT_DISPATCHER";

@Injectable({
  providedIn: "root",
})
export class GlobalEventDispatcher {
  // ob
  private subject$: Subject<GlobalDispatcherEvent> = new Subject();
  // 是否加入了全局事件监听
  private hasAddGlobalEventListener = false;
  // 订阅总数
  private subscriptionCount = 0;

  private globalEventListener = (event: CustomEvent) => {
    this.subject$.next(event.detail);
  };

  // 加入全局事件监听
  private addGlobalEventListener() {
    this.hasAddGlobalEventListener = true;
    window.addEventListener(CUSTOM_EVENT_NAME, this.globalEventListener);
  }

  // 删除全局事件监听
  private removeGlobalEventListener() {
    this.hasAddGlobalEventListener = false;
    window.removeEventListener(CUSTOM_EVENT_NAME, this.globalEventListener);
  }

  constructor(private ngZone: NgZone) {}

  // 发射数据,你可以通过 name进行数据的区分,发射的应该是主动方
  dispatch<TPayload>(name: string, payload?: TPayload) {
    window.dispatchEvent(
        // 通过CUSTOM_EVENT_NAME 找到了 globalEventListener函数
        // 然后将detail传入函数,所以现在subject中接收到值了
      new CustomEvent(CUSTOM_EVENT_NAME, {
        detail: {
          name: name,
          payload: payload,
        },
      })
    );
  }

  // 注册事件 返回ob对象, 注册的应该是被动方
  register<T>(eventName: string): Observable<T> {
    return new Observable((observer) => {
      // 如果没有全局事件监听,那么先添加
      if (!this.hasAddGlobalEventListener) {
        this.addGlobalEventListener();
      }
      this.subscriptionCount++;
      // subscription 用来去掉订阅用的
      const subscription = this.subject$
        .pipe(
          filter((event) => {
            // 过滤出eventName一致的payload
            return event.name === eventName;
          }),
          map((event) => {
            // 返回payload
            return event.payload;
          })
        )
        .subscribe((payload) => {
          // 发射payload让订阅的地方得到值
          this.ngZone.run(() => {
            observer.next(payload);
          });
        });
      return () => {
        this.subscriptionCount--;
        subscription.unsubscribe();
        if (!this.subscriptionCount) {
          this.removeGlobalEventListener();
        }
      };
    });
  }

  getSubscriptionCount() {
    return this.subscriptionCount;
  }
}

看一个 demo

// 被动方,等待name=openADetail的event发射值
        this.globalEventDispatcher.register('openADetail').subscribe(event => {
            this.thyDialog.open(ADetailComponent);
        });
//  name=openADetail 发射空值,此时上一个函数的 this.thyDialog.open(ADetailComponent);将被调用
        this.globalEventDispatcher.dispatch('openADetail');

END

结束,鄙人技术有限,有些地方不太准确(太不准确),不过按照这个思路将项目走一遍可以对 ngx-planet 有一个比较清晰的认识,有问题可邮件联系,或在 gitalk 中直接回复

著名来源随意转载爬取 https://blog.eiyouhe.com/articles/2021/01/22/1611298349966.html

  • 前端

    前端技术一般分为前端设计和前端开发,前端设计可以理解为网站的视觉设计,前端开发则是网站的前台代码实现,包括 HTML、CSS 以及 JavaScript 等。

    247 引用 • 1348 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...