道标
准备好源码,然后跟着文章去看代码,在每个代码块的第一行,我都把 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
先看一个用例,这个用例是 app1
的 main.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.ts
的 defineApplication
第二参数的 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
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于