ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Angular启动机制解密:platform-browser与platform-browser-dynamic的区别

Angular启动机制解密:platform-browser与platform-browser-dynamic的区别 1. main.ts 里那两行 import启动代码为什么版本之间不一样1.1 你在脚手架里天天见的两种入口每个用脚手架创建过 Angular 项目的人应该都见过 main.ts 里这两行代码import { platformBrowserDynamic } from angular/platform-browser-dynamic; platformBrowserDynamic() .bootstrapModule(AppModule) .catch(err console.error(err));我指的是现代 Angular也就是 Angular 2 之后的版本不是 AngularJS。如果你新建的项目用的是 Standalone Component入口又会变成这样import { bootstrapApplication } from angular/platform-browser; import { appConfig } from ./app/app.config; import { AppComponent } from ./app/app.component; bootstrapApplication(AppComponent, appConfig) .catch((err) console.error(err));同样是启动应用一个从angular/platform-browser-dynamic里拿方法一个从angular/platform-browser里拿方法。如果不看文档很容易把这两个包当成同一件事的两种写法。它们真正的分工是angular/platform-browser负责浏览器运行时angular/platform-browser-dynamic负责在浏览器里做 JIT 即时编译。两个包一前一后回答了 Angular 应用怎么在浏览器里跑起来这个问题的两个阶段。1.2 从 JIT 时代到 AOT 时代入口方法为什么没跟着变Angular 2 到 Angular 8 那段时间CLI 生成的项目 main.ts 统一是platformBrowserDynamic().bootstrapModule(AppModule)。开发时用 JIT模板字符串在浏览器里编译生产构建时则用 AOT提前把模板编译成静态代码。但生产构建的入口代码和开发模式长得一模一样都是platformBrowserDynamic()因为构建器会在后台处理掉编译差异。Angular 9 之后 Ivy 成为默认编译器AOT 逐渐成为默认策略Standalone 组件稳定后又出现了bootstrapApplication。可很多老项目的 main.ts 还是platformBrowserDynamic()。这就造成了一个局面入口函数名没变但背后的编译策略早就变了。你在源码里看到什么入口并不直接等于项目用了什么编译策略。1.3 记住一条主线一个管运行一个管先编译再运行如果只记一句话那就是angular/platform-browser是 Angular 应用在浏览器里运行的运行时包angular/platform-browser-dynamic是让浏览器拥有 JIT 编译能力的入口包。前者回答怎么运行后者回答怎么编译。两者不是平行的两个选择而是叠加关系——动态编译平台建立在浏览器平台之上先拿到编译能力再完成启动。往深里看这两个包牵出了 Angular 整个平台抽象设计和编译器架构。下面我把这条线完整拆开。2. Angular 为什么要专门做一个平台层2.1 平台是宿主环境适配层不是操作系统那个平台Angular 里提到 platform很多人第一反应是 Windows、Linux 这类的平台。实际上 Angular 的 PlatformRef 指的是宿主环境的适配层。浏览器、服务端 Node.js、Web Worker这些环境的底层 API 完全不同浏览器有document、window服务端没有Node 里有文件系统浏览器没有。Angular 不可能针对每个环境写一套应用代码所以它在应用和宿主之间插入了一层 platform。这一层负责提供平台级服务管理平台注入器并决定应用最终用什么方式渲染。platformBrowser()创建的就是浏览器平台的实例platformServer()创建服务端平台实例Web Worker 场景还有专门平台。应用代码不直接碰document而是通过平台提供的抽象服务去操作。2.2 platform-browser 是浏览器宿主的最小运行时集合angular/platform-browser的核心职责是把 Angular 应用接到浏览器这个宿主上。它内部注册了一大批浏览器专用 Provider比如DomRendererFactory2、EventManager、DomSanitizer、TransferState等。这些服务合起来构成了Angular 应用在浏览器里跑起来所需要的最小环境。可以这么理解platform-browser相当于把浏览器环境里那些乱七八糟的 API 整理成 Angular 认识的一整套服务接口。组件代码里写的[class.active]、(click)、{{ value }}到了运行时都要通过这些服务真正落到 DOM 上。没有这一层Angular 的组件系统就失去了和浏览器打交道的通道。2.3 平台工厂的嵌套core、browser、browserDynamicAngular 的平台不是一块铁板它由底层基础平台逐层叠加而来。底层是angular/core里的核心平台然后是浏览器平台再往上才是动态编译平台。createPlatformFactory这个工具函数就是用来做这种叠加的。platformBrowser()大致可以理解为基于核心平台叠加浏览器专属 Provider生成一个浏览器平台工厂platformBrowserDynamic()则在浏览器平台的基础上再叠加 JIT 编译器相关 Provider生成一个带编译能力的平台工厂。所以platform-browser-dynamic天然包含platform-browser的能力这也是为什么 JIT 入口也能跑通大部分应用流程。用依赖方向来看angular/core核心 DI、组件系统、应用启动机制angular/compiler模板编译器本身不依赖浏览器angular/platform-browser浏览器运行时依赖 core不依赖 compilerangular/platform-browser-dynamic浏览器 JIT 编译入口同时依赖 browser 和 compiler2.4 一个页面只能有一个 Angular 平台PlatformRef 是全局单例同一个页面里用createPlatform或平台工厂创建过一次之后再调用会复用已有实例。这点平时不太容易感知但如果你在同一个页面里手动调了两次platformBrowserDynamic()大概率会遇到奇怪的服务状态问题。我在一个微前端项目里踩过这个坑。子应用挂载时调了一次platformBrowserDynamic()卸载时想清理又调了一次结果本来应该独立的应用实例复用了同一个平台注入器导致服务状态互相污染。后来改成在整体启动流程里只创建一次平台各子应用各自处理自己的 NgModuleRef问题才消失。理解平台的单例特性排查这类问题会快很多。3. 浏览器运行时到底在替开发者管哪些脏活累活3.1 从模板到 DOMRenderer2 与 DomRendererFactory2 的分工Angular 组件模板不会直接生成一段innerHTML扔进页面。它在运行时通过渲染器来创建、修改、删除 DOM 节点。platform-browser提供了DomRendererFactory2为每个视图创建对应的Renderer2实例。你在自定义指令里写renderer.setStyle()、renderer.listen()用的就是这个服务。import { Directive, ElementRef, Renderer2, AfterViewInit } from angular/core; Directive({ selector: [appHighlight] }) export class HighlightDirective implements AfterViewInit { constructor(private elementRef: ElementRef, private renderer: Renderer2) {} ngAfterViewInit(): void { this.renderer.setStyle(this.elementRef.nativeElement, background, #fdf6e3); } }这里有个容易被忽略的点直接操作elementRef.nativeElement和通过renderer操作虽然最终结果一样但意义不同。直接操作 nativeElement 就把代码绑死在浏览器环境里通过Renderer2Angular 可以在 SSR、Web Worker 等场景切换不同渲染器实现而你的指令代码不用改。这就是运行时抽象层的价值。样式封装也由渲染管线处理。默认的 ViewEncapsulation.Emulated 模式Angular 会给组件模板里的元素加上_ngcontent-xxx之类的属性再生成带属性选择器的样式规则。这套逻辑同样在运行时由渲染器完成对组件代码透明。3.2 BrowserModule 不只给根模块刷存在感BrowserModule从angular/platform-browser导出是根模块才应该 import 的模块。它内部导出CommonModule和ApplicationModule并提供ApplicationRef、DomSanitizer、EventManager、TransferState等一批关键服务。服务/API运行时职责ApplicationRef应用视图树的根引用负责启动组件、触发变更检测DomSanitizer处理innerHTML、URL 等敏感绑定防止 XSSEventManager统一事件监听机制支持click.enter这类事件修饰符TransferStateSSR 场景下把服务端数据传给浏览器端避免重复请求Renderer2对 DOM 操作的统一抽象DomSanitizer是个典型例子。你用[innerHTML]绑一段 HTML 内容Angular 运行时不会直接塞进去而是先经过安全审查。平台层把这个安全检查放在运行时是因为最终面对的是真实的浏览器 DOM安全边界只能在运行时执行。3.3 组件里感受不到的边界浏览器 API 还是 Angular 封装写组件时模板里的*ngFor、[class]、(click)看起来像魔法。它们其实都走了一套平台提供的运行时机制事件通过EventManager统一注册属性绑定通过渲染器写入变更检测由ApplicationRef驱动。你很少直接感知到这些服务的存在但它们一直在底层运转。一个比较有体感的场景是 SSR。组件里如果直接写if (window) ...服务端渲染阶段会因为找不到window直接报错。但如果你把读取窗口宽度这类操作封装成服务并让服务通过 Angular 的PLATFORM_ID判断当前环境代码就能在浏览器和服务端都能跑。这就是运行时抽象在真实项目里的价值。platform-browser做的事情正是把浏览器 API 变成可替换的 Angular 服务让应用代码不直接依赖某个具体宿主。3.4 为什么运行时包不能顺手把编译器也带上一个很自然的疑问既然platform-browser已经管了这么多运行时的事为什么不能把 JIT 编译器也塞进去省得搞出两个包原因很简单体积。Angular 的编译器代码量非常可观一旦platform-browser依赖它任何基于platform-browser的应用都没法通过 tree-shaking 把它去掉。AOT 编译的核心收益之一就是运行时不需要编译器产物体积能大幅下降。如果为了省事把编译器和运行时绑死等于把 AOT 最大的优势直接废掉。还有一层原因编译器本身不依赖浏览器。angular/compiler在 Node 环境也能运行AOT 构建就是在构建机上调用它把模板提前编译好。浏览器运行时只需要消费编译产物没必要在浏览器里维护一份编译器。独立成包各端按需引入才是合理的架构。4. platform-browser-dynamic 那头JIT 编译器是怎么在浏览器里工作的4.1 动态编译平台到底给平台加了什么料platformBrowserDynamic()创建的不仅是一个浏览器平台还在平台注入器里注册了编译器相关 Provider。核心的有CompilerFactory、JIT 编译器实例、以及编译器需要的资源加载器等。这些 Provider 让平台具备了在浏览器里现场读取装饰器元数据、现场编译模板的能力。代码层面可以大致理解成这样import { createPlatformFactory, platformCore } from angular/core; import { COMPILER_OPTIONS, CompilerFactory } from angular/core; export const platformBrowserDynamic createPlatformFactory( platformBrowser, browserDynamic, [ // 提供 JIT 编译器实例 { provide: CompilerFactory, ... }, // 全局编译选项 { provide: COMPILER_OPTIONS, ... }, ] );虽然真实源码比这复杂但结构就是在浏览器平台之上追加编译能力。所以platform-browser-dynamic不是platform-browser的替代品而是它的增强版本专门服务那些需要在运行时进行编译的场景。4.2 一次 bootstrapModule 调用浏览器里发生了什么当你调用platformBrowserDynamic().bootstrapModule(AppModule)时流程大致是平台注入器取出CompilerFactory创建 JIT 编译器编译器读取AppModule的NgModule元数据分析它依赖了哪些组件、指令、管道把组件模板字符串编译成可执行的视图定义代码创建模块实例和注入器初始化应用ApplicationRef启动根组件渲染到浏览器 DOM这个流程最耗时的就是第 3 步。模板编译是 CPU 密集型操作而且发生在用户打开页面之后。JIT 模式下首屏启动慢根本原因就在这里。开发时 JIT 有它的优势——改一行模板代码浏览器重新编译一次不需要完整重新构建生产环境这种开销就很难接受了。4.3 AOT 如何绕过这套编译流程AOT 构建把第 3 步挪到了构建阶段。angular/compiler-cli里的 ngtsc 在编译.ts文件时直接读取模板字符串产出 Ivy 指令格式的代码。你写的是{{ value }}构建产物里直接就是ɵɵtextInterpolate这类指令调用。浏览器运行的时候组件已经是一个编译好的定义不再需要编译器介入。在 View Engine 时代AOT 入口更直观那时候有显式的模块工厂import { platformBrowser } from angular/platform-browser; import { AppModuleNgFactory } from ./app/app.module.ngfactory; platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);bootstrapModuleFactory接收的AppModuleNgFactory就是ngc预先编译产物。这段代码现在很少见了因为 Ivy 之后不再生成.ngfactory.ts文件但它非常好地说明了预先编译好再启动和启动时现编译再启动的区别。理解了它再看现在的bootstrapApplication逻辑就通了。4.4 没有 JIT 包时典型报错长什么样动态编译依赖angular/compiler。如果构建产物里没有编译器但运行时又真的去编译了某个组件Angular 会抛出类似这样的错误Angular JIT compilation failed: angular/compiler not loaded!我在排查一个老项目时第一次见到这个报错。当时我们的 main.ts 是标准的platformBrowserDynamic().bootstrapModule(AppModule)但构建配置里明确开了 AOT。按道理不应该触发 JIT结果有个第三方库的动态组件在运行时注册了未编译的模板硬生生把 JIT 路径拉起来了。这个报错是平台与编译策略不匹配最典型的信号。排查方向不是急着换入口函数而是找到哪个组件在运行时进入了 JIT 编译路径。可能的原因包括模板字符串在运行时拼装、某些动态组件定义不完整、第三方库携带了未编译的装饰器元数据。理解了编译策略的分工这类问题才不会一头雾水。5. Ivy 与 Angular 9 之后分工被重新画了一次5.1 从 View Engine 到 Ivy编译产物形态完全不同Angular 9 之前是 View EngineAOT 构建会生成辅助工厂文件模板的编译结果和组件类定义分离。Ivy 之后编译器把编译结果直接内联到组件类的静态字段上通过ɵcmp这样的属性挂载。运行时拿到组件类就拿到了完整的编译信息不需要额外查找工厂文件。这个变化让分离编译器这件事变得更加彻底。View Engine 时代平台、编译器、渲染器之间纠缠很多Ivy 时代渲染路径被大幅简化编译产物更扁平platform-browser和angular/compiler之间的耦合被进一步切断。这也是为什么 Angular 官方敢在文档里说AOT 产物在运行时不需要编译器。5.2 bootstrapApplication 的登场意味着什么Standalone Component 稳定之后CLI 新项目的 main.ts 变成了bootstrapApplication(AppComponent, appConfig)。注意这个函数来自angular/platform-browser而不是angular/platform-browser-dynamic。它默认面向已经完成 AOT 编译的代码内部创建一个浏览器平台直接启动根组件。这是一个信号在现代 Angular 里platform-browser就是那个正常的启动入口platform-browser-dynamic更像是兼容模式和特殊场景的工具。bootstrapApplication的设计思路和 JIT 完全解耦它不假设代码里还有未编译的装饰器元数据需要现场翻译。5.3 现在还需要 platform-browser-dynamic 吗分场景看场景推荐入口是否需要 JIT 包新项目 StandalonebootstrapApplication不需要老项目 NgModule AOT可继续platformBrowserDynamic或逐步迁移生产构建通常不需要开发阶段调试 JIT 行为platformBrowserDynamic需要单元测试环境TestBed 初始化需要老项目如果 main.ts 还在用platformBrowserDynamic不用急着改。只要构建配置开启 AOT最终产物并不会因为入口函数名带Dynamic就强制打包编译器。构建器会根据实际是否需要运行时编译来做 tree-shaking。当然新项目没必要再刻意绕道直接用bootstrapApplication更符合现在的主流实践。5.4 一个容易被忽略的地方测试工具链仍在依赖动态编译Angular 单测环境里有一个很固定的配置import { getTestBed } from angular/core; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting, } from angular/platform-browser-dynamic/testing; getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting() );这段代码在 angular.json 的test配置或src/test.ts里都能看到。单元测试跑的是组件源码没有经过构建期 AOT 编译TestBed 必须用 JIT 编译器现场把模板编译成视图。所以platform-browser-dynamic/testing这个包直到今天还在活跃使用。这也解释了为什么 Angular 没有彻底删除 JIT 路径测试场景需要它。6. 日常开发里的选型与排查经验6.1 三步判断你的项目走的是哪条编译路线第一步看angular.json或angular.json里构建目标的配置确认是否存在aot: false。新版本 Angular CLI 默认开启 AOT老项目可能手动关掉过。第二步看 main.ts 入口。bootstrapApplication基本可以断定是 AOT 路线platformBrowserDynamic()则还需结合构建配置判断。第三步看构建产物。生产构建完成后搜索产物里是否有angular/compiler相关 chunk。没有就说明运行时没有编译器走的是完整 AOT。这个检查方法最直接不被源码表象误导。# 构建产物目录里搜 compiler 关键字 grep -r angular/compiler dist没有输出基本可以放心。6.2 三个容易踩的认知误区误区一看到platformBrowserDynamic就认为项目没有开 AOT。实际上很多老项目的入口一直是它但构建配置早就开了 AOT运行时并不需要 JIT。源码入口名只是历史遗留。误区二AOT 就必须写bootstrapModuleFactory。那是 View Engine 时代的写法Ivy 之后已经没有.ngfactory.ts文件CLI 也封装了这些细节。现在你不需要亲手处理模块工厂。误区三platform-browser和platform-browser-dynamic是二选一。二者是叠加关系动态编译平台本身就建立在浏览器平台之上。JIT 运行时同时拥有两者AOT 运行时只需要前者。搞清这个关系调试启动流程时思路会清晰很多。6.3 几条实操建议新项目直接走 Standalone 模式main.ts 用bootstrapApplication默认 AOT简洁少坑。老项目不必为了看起来规范强行改入口重点确认生产构建的 AOT 选项处于开启状态并定期检查产物里有没有多余的 compiler chunk。做 SSR 时格外注意服务端渲染入口不要引入 JIT 依赖。服务端应用应该消费 AOT 产物通过angular/platform-server渲染。如果服务端 bundle 里混入 JIT 编译器不仅体积变大还可能在 Node 环境触发意想不到的编译行为。排查启动报错时先区分是运行时问题还是编译期问题。凡是出现JIT compilation failed、angular/compiler not loaded优先级最高的是找出谁在运行时触发编译而不是先怀疑版本冲突或依赖缺失。6.4 我对这套设计的一点体会拆开platform-browser和platform-browser-dynamic这件事短期看只是多了一个包长期看其实决定了 Angular 的运行策略能灵活演进。编译器提前到构建期运行时保持轻量平台层抽象让渲染逻辑不绑定某个具体宿主。这个取舍最初可能会让初学者困惑但踩过一轮坑之后会发现问题定位路径其实是清晰的。我在实际项目里最后的建议是源码层面的入口跟着项目自身的历史走不要为了统一而统一但构建配置和产物分析一定要定期确认 AOT 确实生效。前者影响代码风格后者直接影响线上性能和首屏体验。把这两个包的分工真正放在心上遇到启动阶段的各种疑难杂症排查起来会顺手很多。
返回列表