读懂 Angular 里的 @angular/platform-server 与 @angular/ssr:它们各自解决什么问题,为什么经常同时出现
你在 package.json 里同时看到 @angular/platform-server 和 @angular/ssr,这几乎可以直接推断:这个 Angular 应用已经不满足于纯 CSR(Client Side Rendering,浏览器端渲染),而是在引入 SSR(Server Side Rendering,服务端渲染)或更细粒度的 Hybrid Rendering(混合渲染:按路由选择 CSR / SSR / SSG)。官方文档把这种方向称为 Server and hybrid rendering,并明确给出了 ng new --ssr 与 ng add @angular/ssr 作为启用入口。 (Angular)
下面我用一条严谨的推理链,把这两个依赖的职责边界拆开,并解释为什么它们会一起出现。
步骤 1:从 package.json 反推应用要解决的核心矛盾
Angular 默认把应用作为 CSR 来交付:服务器只负责吐出静态资源,浏览器下载 JS,再由框架跑起来把页面绘制出来。官方在 SSR 指南里点得很直白:纯 CSR 的代价往往是更慢的首屏、更差的指标表现,以及更多计算压力落到用户设备上。 (Angular)
一旦你希望做到下面任意一点,SSR / Hybrid Rendering 就会变得现实且常见:
- 首屏更快,把可见内容的
HTML提前在服务器生成再返回(用户先看到内容,再逐步变得可交互) SEO更稳,让爬虫直接拿到完整HTML- 静态化一部分路由(
SSG),把构建时产出的HTML直接当静态文件发出去 - 对不同路由做差异化渲染策略(同一站点内混用
CSR、SSR、SSG)
而这四类诉求,对应的是两层能力:
- 底层运行时能力:Angular 必须能在服务器环境里启动、跑变更检测、把组件树渲成字符串
HTML - 工程化与框架级封装:CLI 要能帮你生成
server.ts、配置构建与开发服务器、提供路由级渲染模式、对接Node.js或其他运行时
这也正好对应 @angular/platform-server 与 @angular/ssr 的分工。
步骤 2:@angular/platform-server 是什么——它提供 SSR 的底层运行时平台
@angular/platform-server 的定位可以一句话概括:让 Angular 能在服务器侧启动一个 platform,并把应用渲染为 HTML。
官方 API 对 platformServer() 的描述非常直接:它创建一个服务端的 Angular platform,并用于执行 Angular 应用的服务端渲染。 (Angular)
从能力上看,它更像一组底层积木,主要解决这几个问题:
- 在服务器上创建 Angular 平台实例
浏览器侧用platformBrowser,服务器侧需要platformServer。没有这层,Angular 就缺少对应的运行时适配。 (Angular) - 把应用渲染成字符串
HTML
你会在它的 API 里看到renderApplication():它会引导启动一个 Angular 应用实例,并把结果渲染成字符串返回。 (Angular)
这件事本质上就是:在服务器进程中执行一次渲染流程,得到完整的document片段或整页HTML。 - 提供服务端渲染所需的一组 providers 与服务
比如服务端的PlatformState、序列化前钩子、以及provideServerRendering等(不同 Angular 版本里用法会有演进,但其目标就是把服务端渲染需要的依赖注入装配好)。 (Angular)
你可以把 @angular/platform-server 理解为:能跑,但不一定好用。它负责让引擎在服务器跑起来,并把组件树变成 HTML,却不负责把你的工程结构、路由渲染策略、开发与构建链路一并“打包成一套顺手方案”。
步骤 3:@angular/ssr 是什么——它提供 Hybrid Rendering 的框架级封装与工程化入口
@angular/ssr 的角色更偏向“把 SSR 做成可配置、可落地、可维护的一整套方案”。
官方指南明确写到:你可以用 ng add @angular/ssr 给既有项目添加服务端与混合渲染能力,并且默认会做 prerender 与生成 server 文件等工作。 (Angular)
更关键的是,它把“按路由选择渲染模式”这件事产品化了:通过 ServerRoute 与 RenderMode,你能非常清晰地声明哪些路由走 CSR,哪些路由走 SSG,哪些路由走 SSR。官方示例大概长这样: (Angular)
// app.routes.server.tsimport{ RenderMode,typeServerRoute}from'@angular/ssr';exportconst serverRoutes: ServerRoute[]=[{ path:'', renderMode: RenderMode.Client },{ path:'about', renderMode: RenderMode.Prerender },{ path:'profile', renderMode: RenderMode.Server },{ path:'**', renderMode: RenderMode.Server },];然后把它挂到服务端配置里:
// app.config.server.tsimport{ provideServerRendering, withRoutes }from'@angular/ssr';import{ serverRoutes }from'./app.routes.server';exportconst serverConfig ={ providers:[provideServerRendering(withRoutes(serverRoutes)),],};这些 API 的文档也明确了语义:
provideServerRendering()用来配置 Angular 应用的服务端渲染能力,并可组合withRoutes、withAppShell等特性。 (Angular)withRoutes()注册服务端路由配置,用于启用特定路径的服务端渲染或预渲染,从而改善首屏与SEO。 (Angular)
再往下,@angular/ssr 还分出了 @angular/ssr/node:它是面向 Node.js 环境的扩展,提供 AngularNodeAppEngine、createNodeRequestHandler、writeResponseToNodeResponse 等更贴近 Express 中间件风格的工具。官方在指南里直接给了 Express 例子。 (Angular)
这就解释了一个常见现象:
- 你在依赖里看到
@angular/ssr,往往意味着项目在用 Angular 新一代的SSR/Hybrid Rendering方案,而不只是“能在服务端渲一下”那么简单。 (Angular)
步骤 4:为什么两个包经常一起出现——一层是引擎,一层是脚手架与策略
把前面两段合在一起,你会得到一个非常稳定的结论:
@angular/platform-server:提供服务端渲染所需的运行时平台与渲染原语(platform、渲染为字符串、服务端相关 providers)。 (Angular)@angular/ssr:把渲染能力接入工程化体系,并提供路由级渲染策略与运行时适配封装(RenderMode、ServerRoute、withRoutes、Node.js扩展等)。 (Angular)
两者关系有点像:
@angular/platform-server是发动机与传动系统@angular/ssr是整车的控制台、档位选择、以及让你能在城市道路上合法上路的那套配置与工具链
少了前者,后者没法真正把页面渲出来;少了后者,你往往得自己拼装很多工程细节,维护成本会明显上升。
步骤 5:结合 RxJS 的视角,看 SSR 带来的真实编码差异
你说你精通 RxJS,这点在 SSR 场景非常实用,因为 Angular 的 HttpClient、路由解析、数据流组合,本质都离不开 Observable。
SSR 会让同一段组件代码在两个地方执行:服务器渲一次,浏览器 hydration 再接管一次。官方 hydration 指南把 hydration 定义为:在客户端恢复服务端渲染出来的应用,包括复用服务端已经生成的 DOM 结构与状态等。 (Angular)
这会带来几类非常典型的 RxJS 代码习惯变化:
1) 避免服务端渲染阶段出现“永不结束”的流
服务器渲染通常需要在有限时间内产出 HTML。如果你的渲染依赖某个永不 complete 的流(例如长连接、interval 没有 takeUntil,或某些状态流被当作首屏依赖),服务端可能一直等不到稳定态。
实战里常见做法是把“首屏必须的数据”与“用户交互后才需要的数据”分流:
- 首屏数据:
this.http.get(...).pipe(take(1)),或用firstValueFrom拿一次性值 - 交互数据:保留长生命周期流,但让它只在浏览器侧真正启用(例如在
isPlatformBrowser分支里订阅)
2) 避免服务端与浏览器重复请求:用传输缓存与 hydration 配合
官方 SSR 指南提到可以配置 HTTP 响应在服务端渲染时缓存,并在 hydration 阶段复用。 (Angular)
这背后的直觉非常简单:服务端已经把数据取回并渲染进 HTML 了,浏览器再进来如果又打一遍同样的接口,请求就重复了。
3) 处理浏览器专属对象:让 Observable 的副作用只发生在正确的平台
在 SSR 里直接读 window、document、localStorage 会踩坑是老生常谈。更隐蔽的问题是:你可能把这些访问藏在 tap()、map() 里,一旦服务器执行到这条链就直接报错。
解决方式通常是:把副作用包在平台判断里,或把只属于浏览器的流延后到浏览器生命周期再订阅。
步骤 6:给你一个“看到依赖就能定位工程结构”的例子
假设你在 package.json 里看到:
@angular/platform-server@angular/ssr- 可能还会看到
@angular/ssr/node
那么你大概率能在项目里找到这些文件或同类结构:
server.ts:Express或其他Node.jsserver,把请求交给 Angular 的 server engine 处理(常见AngularNodeAppEngine)。 (Angular)app.routes.server.ts:声明RenderMode,把路由划分为CSR、SSR、SSG。 (Angular)app.config.server.ts:调用provideServerRendering(withRoutes(...))注入服务端渲染 providers。 (Angular)
这套结构的价值是:当你排查问题时,能快速判断是“渲染引擎层”问题(通常看 platform-server 相关报错),还是“渲染策略与工程集成层”问题(通常看 @angular/ssr、server routes、构建产物与 server handler)。
结论:一句话把两个依赖讲清楚
@angular/platform-server解决的是:Angular 在服务器上运行并把应用渲染成HTML的底层能力。 (Angular)@angular/ssr解决的是:把服务端与混合渲染能力接入 Angular 工程体系,提供路由级渲染模式、app shell、以及面向Node.js等运行时的封装。 (Angular)
如果你愿意贴一段该项目的 package.json(只要依赖部分即可)以及 angular.json 的 build 配置片段,我也可以据此进一步反推:它到底在用 SSR、SSG,还是更现代的路由级 Hybrid Rendering,以及它的服务端入口更可能落在哪个文件里。