Angular13多版本混用时window.ng.ɵcompilerFacade被覆盖报错
经排查确认根因:Angular 13 加载组件/模块时会读取全局window.ng.ɵcompilerFacade对象,且要求该对象完全符合v13版本的格式规范;如果加载过程中引入了基于Angular Elements构建的低版本Web Component(比如Angular 11版本打包的组件),低版本Angular运行时会覆盖全局的window.ng.ɵcompilerFacade对象,导致后续Angular 13的编译逻辑读取不到对应属性,最终触发路由报错。
当前场景为:壳应用基于Angular 13开发,需要内嵌加载不同Angular版本构建的微件,属于多版本Angular混用场景。
项目中通过路由守卫(RouterGuard)实现组件激活前加载对应外部脚本的逻辑,该逻辑在Angular 7至Angular 11版本中均运行正常,升级到Angular 13后出现异常。
路由守卫核心实现代码如下:
canActivate(route: ActivatedRouteSnapshot): Promise<boolean> { const result: Promise<boolean> = new Promise((resolve) => { if (!myElementExists) { this.loadApp(appObj).then(() => { // 脚本加载成功 resolve(true); }, () => { // 加载错误:文件不存在/加载失败 this.router.navigate(['/error']); resolve(true); }); } else { // 脚本已存在,直接放行 resolve(true); } }); return result; } private async loadApp(url): Promise<void> { await Utils.loadScript(`../${url}`); }
升级Angular 13后触发如下报错:
core.mjs:6494 ERROR Error: Uncaught (in promise): TypeError: Cannot read properties of undefined (reading 'Directive') TypeError: Cannot read properties of undefined (reading 'Directive') at XXXComponent.get (core.mjs:24383:1) at getFactoryDef (core.mjs:1406:1) at configureViewWithDirective (core.mjs:10484:1) at instantiateRootComponent (core.mjs:10191:1) at createRootComponent (core.mjs:12297:1) at ComponentFactory.create (core.mjs:21654:1) at ViewContainerRef.createComponent (core.mjs:22918:1) at RouterOutlet.activateWith (router.mjs:2521:40) at ActivateRoutes.activateRoutes (router.mjs:2161:40) at router.mjs:2111:18 at resolvePromise (zone.js:1211:1) at resolvePromise (zone.js:1165:1) at zone.js:1278:1 at _ZoneDelegate.invokeTask (zone.js:406:1) at Object.onInvokeTask (core.mjs:25595:1) at _ZoneDelegate.invokeTask (zone.js:405:1) at Zone.runTask (zone.js:178:1) at drainMicroTaskQueue (zone.js:585:1) at ZoneTask.invokeTask [as invoke] (zone.js:491:1) at invokeTask (zone.js:1648:1)
进一步排查Angular 13的core.mjs源码发现,addDirectiveFactoryDef()方法调整了从facade获取编译器的逻辑,执行如下代码时compiler.factoryTarget返回undefined:
const compiler = getCompilerFacade({ usage: 0 /* Decorator */, kind: 'directive', type });
测试验证:如果在canActivate()中跳过脚本加载逻辑直接返回Promise,应用可正常运行;上述报错有时也会在getCompilerFacade处理'NgModule'类型时触发,初步怀疑问题与Zone.js相关,寻求可行解决方案。
该问题和Zone.js无直接关联,本质是多版本Angular运行时共享全局命名空间导致的对象覆盖问题:旧版本Angular的ɵcompilerFacade结构和v13差异较小,覆盖后仍可兼容读取,Angular 13调整了编译器facade的结构定义,被低版本覆盖后就会出现属性读取为undefined的报错,可通过以下方案修复:
- 加载脚本前后做全局对象备份恢复
在加载外部低版本Angular微件脚本前,深拷贝当前Angular 13对应的window.ng对象缓存,脚本加载完成后立即将缓存的对象回写到window.ng,避免低版本运行时污染宿主环境。核心实现:
若运行环境不支持private async loadApp(url): Promise<void> { // 加载前缓存宿主Angular全局对象 const hostNgRef = window.ng ? structuredClone(window.ng) : null; await Utils.loadScript(`../${url}`); // 加载完成后恢复宿主全局对象 if (hostNgRef) window.ng = hostNgRef; }structuredClone,可替换为兼容的深拷贝实现,必须保证ɵcompilerFacade属性完整拷贝。 - 微件打包时配置独立全局命名空间
对所有Angular Elements构建的微件,修改构建配置将Angular运行时的全局挂载点从默认window.ng改为版本专属命名空间,比如window.ng_v11、window.ng_v12,从根源上避免不同版本运行时覆盖全局对象。 - 时序兜底处理
若存在脚本加载回调执行早于微件内部Angular初始化的时序问题,可在恢复全局对象后追加一个空的微任务调度,确保恢复逻辑在Angular路由实例化组件之前执行完成。
内容的提问来源于stack exchange,提问作者vp310

