沧州市网站建设_网站建设公司_API接口_seo优化
2026/1/10 13:48:04 网站建设 项目流程

在聊AngularSSR之前,有必要把视角稍微拉远一点:浏览器到底在做什么,以及SSR在浏览器渲染链路里究竟改变了哪几件关键事情。

浏览器拿到一个HTML文档后,会经历一条相当固定的路径:解析HTML构建DOM树,解析CSS构建CSSOM,把两者合成渲染树,计算布局,绘制,再合成到屏幕。CSR(纯客户端渲染)的问题不在于浏览器不会渲染,而在于浏览器最开始拿到的HTML往往只有一个空壳容器,真正的内容要等JavaScript下载、解析、执行完毕,再由框架把DOM生成出来。结果就是用户看到首屏内容的时间被推迟,搜索引擎抓取与社交分享的meta信息也更容易出现缺失或不稳定。

AngularSSR解决的核心矛盾很直接:把Angular的首屏DOM生成这件事,从浏览器挪到服务器端去做,让浏览器一开始就拿到带内容的HTML,随后再把交互能力补回来,这个补回交互的过程就是Hydration(水合)。官方对Hydration的描述非常清晰:它要复用服务器端已经渲染出来的DOM结构、恢复应用状态、把服务器端获取的数据转交给客户端,避免重复请求等。(Angular)

下面按你关心的点来拆解:Angular用了哪些技术实现SSR,这些技术在架构里分别扮演什么角色,以及在真实项目里如何组合它们。


Angular SSR在今天的定位:Hybrid Rendering而不是单一SSR

Angular的官方文档体系看,SSR现在被放进了更大的概念Hybrid Rendering(混合渲染)里:同一个应用里,你可以对不同路由选择SSRSSG(预渲染)或CSR,以便在SEO、首屏性能、个性化和服务器成本之间做更细颗粒度的权衡。官方给出的三种RenderMode也很明确:Server(按请求服务器渲染)、Client(浏览器端渲染)、Prerender(构建期生成静态HTML)。(Angular)

更重要的是,现在启用这套能力的路径已经非常产品化:新项目可以用ng new --ssr,已有项目用ng add @angular/ssr,这在官方Hybrid Rendering指南里就是推荐做法。(Angular)


技术栈总览:Angular SSR由哪几层组成

Angular SSR拆成工程可落地的组件,通常会落到七层:

  1. 构建与产物层Angular的新构建系统负责把同一套源码编译成浏览器产物与服务器产物,并把SSRPrerender的工作流整合进CLI
  2. 服务器运行时层:最常见是Node.js,也可以是非Node.js的运行时(只要能提供类FetchRequest/Response语义)。
  3. 服务器端渲染引擎层:把Angular应用启动起来并渲染为字符串HTML,典型代表是renderApplicationCommonEngine
  4. 路由与渲染模式编排层:对不同路由选择SSR/SSG/CSR,并控制构建期或请求期的渲染策略。
  5. 客户端恢复层Hydration,负责把服务器端HTML变成可交互应用,包含事件回放、增量水合等能力。
  6. 数据传递与缓存层:把服务器端请求结果带到客户端,避免Hydration后二次HTTP请求,核心是HttpClient传输缓存与TransferState思路。
  7. 跨平台兼容层:解决window/document等浏览器专属对象缺失的问题,包含平台判断、延后执行钩子、必要时的DOM模拟(如domino)。

接下来逐层展开。


构建与产物层:新构建系统esbuild+Vite,把SSR变成一等公民

Angular 17起,Angular的新构建系统逐步稳定并成为主流路径。官方对这套系统的描述里,有几句非常关键:它使用ESM输出格式、引入esbuildVite等现代工具,并且集成了SSRPrerendering能力。(Angular)

这意味着两件事:

  • SSR不再是额外拼装的脚手架,而是编译链路原生支持的目标之一。
  • 应用在构建后会形成面向浏览器与面向服务器的两份运行产物,CLI可以直接用这些产物执行SSRSSG

在团队协作里,这个变化的价值非常现实:以前SSR常常意味着你要维护一套特殊的webpack配置、单独的服务器编译流程、以及跟主工程不同步的构建参数;现在大多数项目能把这些复杂度交还给Angular CLI


服务器端渲染引擎层:renderApplicationCommonEngine是两块核心积木

renderApplication:最底层的SSR能力

从 API 语义上看,renderApplication做的事情非常直白:引导启动一个Angular应用实例,并把它渲染成字符串HTML。(Angular)

你可以把它理解成Angular在服务器端的bootstrapApplication + serialize DOM的组合。它解决的是SSR的最核心问题:在没有真实浏览器的环境里,如何把组件树跑一遍、生成首屏DOM,并把结果序列化成HTML

CommonEngine:面向Node.js应用的通用渲染引擎

在真实项目里,你更常见到的是CommonEngine。官方 API 把它定义为A common engine to use to server render an application,并提供render方法返回渲染好的HTML。(Angular)

CommonEngine的优势在于它把很多工程细节封装好了:文档模板路径、目标url、静态资源publicPath、平台级providers注入等,适合跟Express或其它Node框架整合。


服务器运行时层:Node.js默认方案,以及非Node.js@angular/ssr

Node.js+Express:最常见的落地组合

官方SSR指南里给了一个非常典型的server.ts架构:用Express托管静态资源,对普通路由调用CommonEngine.render,把生成的HTML返回给浏览器。文档还点出一个容易被忽视的细节:从Angular 17开始,ng serve不再依赖server.ts,开发服务器会直接使用main.server.ts执行服务器端渲染。(v19.angular.dev)

这件事对开发体验的影响很大:你不需要每次都把Express服务器跑起来才看得到SSR的效果,CLI会把SSR融进常规的开发工作流。

一个更贴近现代Angular风格(ESM、更少脚手架依赖)的server.ts通常会长这样(示例代码用单引号,避免引入英文双引号):

import{APP_BASE_HREF}from'@angular/common';import{CommonEngine}from'@angular/ssr/node';importexpressfrom'express';import{dirname,join,resolve}from'node:path';import{fileURLToPath}from'node:url';importbootstrapfrom'./src/main.server';exportfunctionapp():express.Express{constserver=express();constserverDistFolder=dirname(fileURLToPath(import.meta.url));constbrowserDistFolder=resolve(serverDistFolder,'../browser');constindexHtml=join(serverDistFolder,'index.server.html');constengine=newCommonEngine();server.set('view engine','html');server.set('views',browserDistFolder);server.get('*.*',express.static(browserDistFolder,{maxAge:'1y'}));server.get('*',(req,res,next)=>{const{protocol,originalUrl,headers}=req;engine.render({bootstrap,documentFilePath:indexHtml,url:`${protocol}://${headers.host}${originalUrl}`,publicPath:browserDistFolder,providers:[{provide:APP_BASE_HREF,useValue:req.baseUrl}],}).then(html=>res.send(html)).catch(next);});returnserver;}

你会发现它本质上是三段式:

  • 静态资源交给Express
  • 动态路由走CommonEngine.render
  • 通过providers把请求上下文(如APP_BASE_HREF)注入到Angular的依赖注入系统里。

这段整体结构与官方示例一致。(v19.angular.dev)

Node.js@angular/ssrWeb APIRequest/Response语义做适配

当你把SSR部署到一些更偏Edge的运行环境,比如支持Fetch标准的Serverless平台时,Node.jsreq/res并不是天然存在的。官方Hybrid Rendering指南明确提到:@angular/ssr提供了在非Node.js平台做服务器端渲染的关键 API,并基于标准Web APIRequestResponse对象来集成。(Angular)

这个设计背后的思路很像浏览器内核的演进路线:尽量围绕标准化的Web Platform API做抽象层,减少对某一种服务器实现的绑定。对架构师来说,这等于给部署形态留下了更大的弹性空间。

顺带一提,从npm的版本信息可以看到@angular/ssr在 2025 年底仍在快速迭代(截至 2026 年 1 月,最新版已到21.x)。(npm)


路由与渲染模式编排层:把SSRSSGCSR做成可配置策略

Hybrid Rendering最有工程价值的一点,是把渲染策略提升为路由级别的配置,而不是全站一刀切。官方文档用RenderMode来描述不同路由的渲染模式:ServerClientPrerender。(Angular)

把它翻译成真实世界的产品逻辑,大概是这样:

  • 营销落地页、活动页:内容相对稳定,追求极致首屏与缓存命中,适合PrerenderSSG)。
  • 商品详情、文章详情:内容变化频繁但又强依赖SEO,适合Server(请求期SSR)。
  • 登录后控制台、内部管理界面:SEO不重要,更在意交互与开发效率,ClientCSR)很合理。

官方还提到一个配置点:默认情况下Angular会对整个应用执行Prerender并生成 server 文件;若你要生成完全静态站点,可以设置outputModestatic。(Angular)

这类能力一旦落到团队开发,会显著减少争论:不是讨论SSR要不要上,而是讨论某一组路由到底用SSR还是SSG,边界清晰很多。


客户端恢复层:Hydration、事件回放与增量水合

如果把SSR比作把菜提前端上桌,那么Hydration就是把桌上的菜变成可以吃的状态:绑定事件、恢复状态、让组件树重新接管DOM

provideClientHydration:水合的入口与默认能力集合

AngularprovideClientHydration是启用水合的核心 API。官方说明它默认启用一组推荐特性,包含DOM的协调式水合(reconciling)以及服务器端运行时的HttpClient响应缓存并传递给客户端,避免重复请求。(Angular)

这两点非常关键:

  • 协调式水合意味着客户端不会粗暴地丢掉服务器端DOM重建,而是尽量复用已有结构,减少首屏闪烁与重排。
  • HttpClient传输缓存意味着你在服务器端SSR时请求过的数据,客户端水合阶段可以直接复用,减少瀑布流请求带来的LCP波动。

事件回放withEventReplay:解决水合窗口期的交互丢失

现实体验里,一个常见问题是:页面已经有HTML了,用户看到按钮就会点,但此时水合尚未完成,事件监听器还没挂上。AngularwithEventReplay就是为这个窗口期设计的:在水合完成前捕获用户事件(比如click),等水合完成后再回放执行。(Angular)

这在电商类站点特别实用:用户往往在首屏刚出来就点筛选、点加入购物车,事件回放能显著减少SSR页面给人的假可点体验。

增量水合withIncrementalHydration:把水合从一次性变成按需

当应用很大、组件树很深时,全量水合会带来明显的主线程压力。Angular提供了Incremental Hydration,让水合按一定策略分批进行。官方文档还提到:增量水合依赖并会自动启用事件回放,如果你已经启用withEventReplay,开启增量水合后可以移除前者。(Angular)

把这点放到浏览器内核视角,它的意义在于:减少一次性JS执行与事件绑定造成的长任务,让交互恢复更平滑,避免把首屏可交互时间推迟太多。


数据传递与缓存层:Http Transfer Cache与可控的缓存策略

SSR的另一个高频痛点是:服务器端渲染时请求了一遍数据,客户端启动后又请求一遍,既浪费带宽也影响性能。Angular官方给出的路径更偏框架级方案:通过水合体系自带的HttpClient缓存,把服务器端响应带到客户端。(Angular)

你可以用withHttpTransferCacheOptions来控制缓存策略,比如包含哪些请求头、是否缓存POST、通过filter决定哪些请求进入缓存。(Angular)

一个实战化的配置示例(仍然避免英文双引号):

import{provideClientHydration,withHttpTransferCacheOptions}from'@angular/platform-browser';exportconstappConfig={providers:[provideClientHydration(withHttpTransferCacheOptions({includeHeaders:['X-Trace-Id'],includePostRequests:false,filter:req=>req.method==='GET'&&req.url.includes('/api/'),}),),],};

真实项目里,这样的过滤策略通常会配合接口分层:

  • /api/public/*:适合缓存并传递;
  • /api/user/*:含鉴权或个性化信息,谨慎传递,甚至直接禁用;
  • /api/checkout/*:强一致性与安全优先,一般不做传递缓存。

跨平台兼容层:浏览器不是服务器,服务器也不该假装成浏览器

Angular官方在SSR文档里专门强调了一个事实:在服务器上不能使用windowdocumentnavigatorlocation这类浏览器全局对象,也不能依赖某些HTMLElement属性。(Angular)

这条规则看似简单,但它对应的坑非常真实:你在组件构造函数里读了window.innerWidth,本地CSR完全正常,一上SSR就直接ReferenceError

平台判断:isPlatformBrowser是最基础的开关

官方提供的isPlatformBrowser用来判断当前平台是否为浏览器。(Angular)

一种工程上更可维护的写法,是把平台判断封装到服务里,组件只依赖服务,而不是到处散落if。这样做的好处是将来如果你切到Zoneless或者引入Edge SSR,改动面更小。

import{Injectable,inject}from'@angular/core';import{PLATFORM_ID}from'@angular/core';import{isPlatformBrowser}from'@angular/common';@Injectable({providedIn:'root'})exportclassPlatformService{privatereadonlyplatformId=inject(PLATFORM_ID);getisBrowser():boolean{returnisPlatformBrowser(this.platformId);}}

延后到浏览器阶段执行:afterNextRender等钩子更贴合SSR语义

很多逻辑并不是非要写平台判断,而是它本质上就应该发生在浏览器首帧渲染之后,比如初始化图表、读取真实布局尺寸、绑定第三方DOM插件。Angular提供了afterNextRender这类 API,并明确指出它只会在浏览器平台运行。(Angular)

这类 API 的思路很像浏览器渲染管线里的post paint任务:把必须依赖真实DOM的操作推迟到正确的时间点执行,而不是在SSR阶段硬凑一个window出来。


DOM模拟层:当你必须在服务器上满足某些DOM依赖时,domino是典型工具

有些第三方库写得很强势:加载时就直接访问documentwindow,根本不给你在业务代码里做平台判断的机会。这个时候,工程上常见的补丁方案就是DOM模拟库,比如domino

社区教程对domino的定位很明确:在Angular SSR场景下,如果你需要访问DOMAPI,可以使用domino来提供querySelector等能力。(danywalls.com)

但这里要强调一个架构层面的判断:domino更像是兼容性垫片,而不是理想解。原因很简单:模拟DOM永远不可能完整覆盖真实浏览器的布局与渲染行为,它只能让代码不崩,不能保证视觉与交互逻辑一致。实践里更推荐的路线通常是:

  • 能替换库就替换成SSR friendly的版本;
  • 不能替换就做平台隔离与懒加载;
  • 真的没办法,再上domino,并把它控制在尽量小的范围内。

Angular Universal在今天的角色:从核心方案降级为兼容路径

很多团队仍然在用Angular UniversalExpress引擎。@nguniversal/express-enginenpm上的描述很直接:这是一个Express Engine,用于在服务器上运行Angular应用以实现SSR。(npm)

把它放到今天的语境里,可以把Angular SSR的演进理解为:

  • 早期:Angular Universal是主力方案,ng add @nguniversal/express-engine负责把SSR脚手架搭起来。
  • 现在:SSR能力更多被@angular/ssr与新构建系统吸收,Universal在不少项目里承担的是迁移期兼容与历史包袱承接。

这对架构决策的影响是:新项目更倾向直接走@angular/ssr与官方的Hybrid Rendering流程;存量项目则根据成本选择渐进迁移。


真实世界案例:一个电商站点如何用Angular SSR把性能与成本压到合理区间

假设你在做一个跨境电商站点,业务目标有三条:

  • 商品详情页要有稳定的SEO与社交分享卡片;
  • 首屏要快,LCP要稳;
  • 服务器成本不能炸,不能所有页面都请求期SSR

一套常见而有效的组合是:

  • //campaign/*/about:用PrerenderSSG)。这些页面变化不频繁,适合直接上CDN,命中率极高。(Angular)
  • /product/:id/category/:id:用Server(请求期SSR)。需要对不同商品动态生成内容,同时对搜索引擎友好。(Angular)
  • /account/*/admin/*:用ClientCSR)。对SEO没要求,优先交互体验与开发效率。(Angular)

客户端水合层面,开启:

  • provideClientHydration:基础水合与HttpClient传输缓存。(Angular)
  • withEventReplay或直接上withIncrementalHydration:避免用户在首屏点了按钮却没反应。(Angular)
  • withHttpTransferCacheOptions:只缓存商品与类目接口,过滤掉用户态接口。(Angular)

跨平台兼容层面:

  • 所有依赖window的逻辑都放在浏览器阶段执行,要么用isPlatformBrowser保护,要么用afterNextRender延迟。(Angular)

你会发现,这套方案的本质是一种工程化的分层渲染:把最贵的请求期SSR留给真正需要的页面,把能静态化的页面尽量静态化,把交互恢复做得足够平滑,避免SSR变成新的体验问题。


什么时候SSR反而不该用:一条务实的判断线

再强调一次:SSR不等于无脑更好,它会带来服务器运行成本、构建复杂度、缓存策略复杂度、以及第三方库兼容成本。哪怕是很早期的Angular SSR文章也反复提醒过:只有在确实需要SSR优势时才值得引入这份复杂度。(ANGULARarchitects)

一个比较务实的判断线是:

  • SEO、强分享卡片、首屏必须稳SSRSSG几乎必选。
  • 页面高度个性化且强实时:倾向请求期SSR,但要做缓存分层与降级策略。
  • 登录后系统、内部工具:多数情况下CSR足够,SSR的收益很难覆盖成本。

小结:回答Angular用了哪些技术实现SSR

把全文收束成清单,你问的Angular用了哪些技术实现SSR,可以概括为这些关键点:

  • 框架级服务器渲染能力@angular/platform-server提供renderApplication把应用渲染成字符串HTML。(Angular)
  • 面向服务器的渲染引擎封装CommonEngine作为通用渲染引擎,负责把url、模板、静态资源路径、依赖注入等工程要素串起来。(Angular)
  • 运行时与适配层:默认Node.js+Express,并通过@angular/ssr支持基于Web API Request/Response的非Node.js平台集成。(v19.angular.dev)
  • 构建系统支撑:新构建系统引入esbuildVite,并原生集成SSRPrerender工作流。(Angular)
  • 混合渲染编排:用RenderMode在路由级别选择SSR/SSG/CSR,并通过outputMode等配置控制生成形态。(Angular)
  • 客户端恢复与体验增强provideClientHydration提供水合能力,配合HttpClient传输缓存;withEventReplay解决水合窗口期事件丢失;withIncrementalHydration把水合拆成增量过程。(Angular)
  • 数据传递与缓存控制withHttpTransferCacheOptionsHttpTransferCacheOptions提供可配置缓存策略,减少重复请求。(Angular)
  • 跨平台兼容与必要的DOM模拟:官方明确禁止在服务器使用window等对象,需要用平台判断与延迟执行;在极端情况下可用dominoDOMAPI 垫片。(Angular)
  • 历史兼容路径Angular Universal@nguniversal/express-engine仍在大量存量项目中使用,更多承担迁移与兼容角色。(npm)

如果你愿意,我也可以按一个你熟悉的项目类型(比如企业后台、内容站、跨境电商、B2B门户)给出一份更落地的SSR架构蓝图:路由渲染模式划分、缓存分层、数据预取策略、以及如何在不牺牲开发体验的前提下把SSR纳入日常迭代。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询