- 前端
- UI组件
【免费下载链接】flex-layout
Provides HTML UI layout for Angular applications; using Flexbox and a Responsive API
导读
本文围绕@angular/flex-layout在 Angular Universal(SSR)场景下的官方解决方案展开,核心讲解FlexLayoutServerModule的引入方式、它在服务端将响应式指令产生的行内样式改写为静态@mediaCSS 的底层机制,以及三种可选配置方案(静态 CSS 生成 / 传统行内样式 / 完全禁用服务端样式)的适用场景与取舍。读完本文,你将能够为 Universal 应用正确配置 Flex Layout 的 SSR 支持,消除「服务端首屏视图」与「客户端水合视图」之间的响应式样式不一致,并理解ServerMatchMedia、StylesheetMap、SERVER_TOKEN等核心构件在服务端的工作方式。
本文以仓库内文档 Using-SSR-with-Flex-Layout.md 与 guides/SSR.md 为主体,并结合仓库源码进行原理级佐证。
一、为什么 SSR 需要 Flex Layout 的特殊处理
浏览器端的工作方式:动态 MatchMedia
在浏览器中,Flex Layout 依赖全局Window对象上的MatchMedia接口工作:当某个断点(breakpoint)被激活或停用时,底层服务会通知各个 Flex 指令,由指令以「行内样式(inline style)」的方式向元素注入相应的 CSS。
对应源码可见于 core/match-media/match-media.ts:MatchMedia.registerQuery()通过buildMQL(query)构造MediaQueryList,并在onMQLEvent回调中通过this._zone.run(() => this.source.next(new MediaChange(...)))把媒体查询变更发布给订阅者;MediaMarshaller再根据激活断点调用updateElement/clearElement更新元素样式(见 core/media-marshaller/media-marshaller.ts)。
服务端的问题:没有 MatchMedia
问题在于:服务端没有MatchMedia接口可用。当视图在服务端渲染时,任何响应式断点(例如fxFlex.sm、fxHide.gt-md)都无法被求值,最终导致两个问题:
- 服务端生成的首屏视图没有应用响应式样式;
- 客户端 bootstrap 后重新生成的视图应用了响应式样式,两者不一致,造成水合(hydration)阶段可见的样式跳动。
另外,@angular/platform-server的 DOM 实现并不具备getComputedStyle能力,这一点在 core/style-utils/style-utils.ts 的注释中也有明确说明。
解决方案的思路
Flex Layout 给出的解决方案分两步:
- 改静态:不再把响应式样式写成行内样式,而是在服务端把样式集中收集后,注入
<head>中的静态<style>标签; - 改用 CSS 媒体查询:用 CSS 的
@media断点接口替代动态的 JavaScriptMatchMedia接口,把「哪个断点激活」的判断交给浏览器自身的样式引擎。
这样,服务端输出的 HTML 自带完整的响应式样式,客户端水合时无需重新计算即可保持一致。
二、核心入口:FlexLayoutServerModule 与 server 入口点
独立入口点的设计意图
@angular/flex-layout为 SSR 提供独立入口点@angular/flex-layout/server,其打包配置见 projects/libs/flex-layout/server/ng-package.json,公开导出见 projects/libs/flex-layout/server/public-api.ts。
正如 projects/libs/flex-layout/server/README.md 所说明的:该入口点集中了在服务端运行 Flex Layout 的全部逻辑,由于它依赖 Node.js API,必须被分割为 server-only 的 bundle——这样做同时避免了把服务端代码打包进浏览器 bundle,减少客户端体积。
模块定义与导入方式
FlexLayoutServerModule的定义非常精简(见 projects/libs/flex-layout/server/module.ts):
import {NgModule} from '@angular/core'; import {SERVER_PROVIDERS} from './server-provider'; @NgModule({ providers: [SERVER_PROVIDERS] }) export class FlexLayoutServerModule {}也就是说,它自身不含任何声明与导入,全部能力来自SERVER_PROVIDERS提供的一组服务端 Provider(下一节详解)。
将它导入到服务端模块(通常是app.server.module.ts):
import {NgModule} from '@angular/core'; import {FlexLayoutServerModule} from '@angular/flex-layout/server'; @NgModule(({ imports: [ ... other imports here FlexLayoutServerModule, ] })) export class AppServerModule {}注意:该模块除了在 Angular 应用于服务端 bootstrap 之前完成全部样式处理/渲染外,还会把
MatchMedia替换为服务端兼容实现ServerMatchMedia。
仓库中的 Universal 演示应用正是这样做的(projects/apps/universal-demo-app/src/app/app.server.module.ts):
import {NgModule} from '@angular/core'; import {ServerModule} from '@angular/platform-server'; import {AppModule} from './app.module'; import {AppComponent} from './app.component'; import {FlexLayoutServerModule} from '@angular/flex-layout/server'; @NgModule({ imports: [ AppModule, ServerModule, FlexLayoutServerModule, ], bootstrap: [AppComponent], }) export class AppServerModule {}导入顺序约束
原文档明确要求:FlexLayoutServerModule的导入必须排在FlexLayoutModule(或任何间接导入了FlexLayoutModule的模块)之后。该约束在未来的版本中可能被放宽,但当前版本请务必遵守,以确保服务端 Provider 正确覆盖浏览器端行为。
三、原理剖析:服务端静态样式是怎么生成的
SERVER_PROVIDERS:三件套
SERVER_PROVIDERS在 projects/libs/flex-layout/server/server-provider.ts 中由三个 Provider 组成:
export const SERVER_PROVIDERS = [ { provide: BEFORE_APP_SERIALIZED, useFactory: FLEX_SSR_SERIALIZER_FACTORY, deps: [ StylesheetMap, MatchMedia, DOCUMENT, BREAKPOINTS, MediaMarshaller, ], multi: true, }, { provide: SERVER_TOKEN, useValue: true }, { provide: MatchMedia, useClass: ServerMatchMedia } ];三者各司其职:
| Provider | 作用 |
|---|---|
BEFORE_APP_SERIALIZED(multi) | 注册一个 Angular Universal 序列化前的钩子,在把应用渲染结果序列化为 HTML 之前执行FLEX_SSR_SERIALIZER_FACTORY生成的回调,把静态样式写入文档<head> |
SERVER_TOKEN = true | 告诉全库「当前运行在服务端且已加载 Server 模块」,相关指令据此走服务端样式收集路径 |
MatchMedia → ServerMatchMedia | 用服务端专用实现替换标准MatchMedia,支持手动激活/停用断点 |
序列化前钩子:FLEX_SSR_SERIALIZER_FACTORY
FLEX_SSR_SERIALIZER_FACTORY(server-provider.ts)返回一个回调函数,执行时:
- 调用
generateStaticFlexLayoutStyles(...)生成完整 CSS 文本; - 创建一个
<style>元素,加上flex-layout-ssr类(CLASS_NAME定义为'flex-layout-',见 core/browser-provider.ts); - 把生成的 CSS 文本写入该
<style>元素并追加到document.head。
也就是说,服务端最终输出的 HTML 中会包含一个携带全部响应式样式的<style>标签,浏览器端无需任何额外计算即可直接匹配@media规则。
核心算法:generateStaticFlexLayoutStyles
generateStaticFlexLayoutStyles(server-provider.ts)的工作流程如下:
- 从
StylesheetMap(虚拟样式表,见 core/stylesheet-map/stylesheet-map.ts)取出当前所有指令产生的默认样式,用generateCss生成一个作用于all媒体查询的样式块(即无媒体条件的基准样式); - 通过
mediaMarshaller.useFallbacks = false关闭回退样式查找逻辑(对应 core/media-marshaller/media-marshaller.ts:服务端会显式填充 "all" 段,无需再激进地寻找回退值); - 按断点优先级升序(
sortAscendingPriority)依次遍历所有已注册断点:- 先
serverSheet.clearStyles()清空虚拟样式表; - 再
mediaController.activateBreakpoint(bp)手动激活该断点——此时各指令基于「该断点匹配」重新计算并写入样式; - 将此时的虚拟样式表用
generateCss生成包在@media bp.mediaQuery中的 CSS 块并追加; - 最后
deactivateBreakpoint(bp)停用断点,继续下一个。
- 先
注意其中的细节:nextId = 0会在每次服务端渲染时重置,避免多次渲染导致类名序号持续递增;每个元素只会分配一个类名(getClassName通过classMap复用),既避免类名爆炸,也让同一元素在各断点的规则可以彼此独立。
generateCss(server-provider.ts)为每个带样式的元素生成形如.flex-layout-0 { display:flex; flex-direction:row; }的规则,并将其整体包裹进@media <mediaQuery> { ... }。
服务端 MatchMedia:ServerMatchMedia 与 ServerMediaQueryList
ServerMatchMedia(projects/libs/flex-layout/server/server-match-media.ts)继承自MatchMedia,是服务端专用实现:
buildMQL(query)不再调用window.matchMedia(),而是构造ServerMediaQueryList(一个实现了MediaQueryList接口的类,继承EventTarget);- 提供
activateBreakpoint(bp)/deactivateBreakpoint(bp)方法,在服务端渲染阶段手动把某个断点的matches置为true/false,并通知监听者; - 支持通过布局配置项
ssrObserveBreakpoints指定一组在服务端默认激活的断点别名(如['gt-sm', 'lt-md']),构造时若遇到未知别名会输出console.warn。
ServerMediaQueryList(server-match-media.ts)维护自己的监听器列表,activate()/deactivate()时以{matches, media}形式回调所有监听器;addEventListener、dispatchEvent等服务端用不到的方法均为空实现。
样式收集路径:StyleUtils 的服务端分支
在浏览器端,指令通过StyleUtils.applyStyleToElement直接写元素行内样式;在服务端且SERVER_TOKEN为true时,则改走虚拟样式表收集(见 core/style-utils/style-utils.ts):
if (isPlatformBrowser(this._platformId) || !this._serverModuleLoaded) { isPlatformBrowser(this._platformId) ? element.style.setProperty(key, value) : setServerStyle(element, key, value); } else { this._serverStylesheet.addStyleToElement(element, key, value); }即:只有「服务端 + 已加载 Server 模块」时,样式才被写入StylesheetMap虚拟样式表,最终由序列化钩子统一转成静态 CSS。而StyleUtils.lookupStyle在服务端读取样式时也优先查虚拟样式表(getStyleForElement),保证服务端渲染期间各指令能互相感知彼此写入的样式。
四、三种使用方案详解
原文档(Using-SSR-with-Flex-Layout.md)给出了三种方案,按推荐程度依次排列。
方案一(推荐):服务端生成静态 CSS
- 在服务端 bundle(一般为
app.server.module.ts)中导入FlexLayoutServerModule(代码见上文)。 - 确保其导入顺序在
FlexLayoutModule(或间接导入它的模块)之后。 - 完成。此时应用已切换到服务端实现:响应式样式会以静态
@mediaCSS 形式随服务端 HTML 输出。
这是唯一能保证「服务端首屏与客户端水合视图响应式一致」的方案。
方案二(传统方案):仅生成行内样式
不导入FlexLayoutServerModule即可。此时服务端仍按浏览器逻辑把样式写成行内样式,但无法应用任何断点规则。
你会收到一条启动警告,但不影响使用,且该警告不会在客户端打印。警告的来源在 projects/libs/flex-layout/module.ts:
constructor(@Inject(SERVER_TOKEN) serverModuleLoaded: boolean, @Inject(PLATFORM_ID) platformId: Object) { if (isPlatformServer(platformId) && !serverModuleLoaded) { console.warn('Warning: Flex Layout loaded on the server without FlexLayoutServerModule'); } }即:在服务端平台检测到SERVER_TOKEN为false(Server 模块未加载)时打印警告。
方案三:服务端完全不生成 Flex Layout 样式
- 不导入
FlexLayoutServerModule; - 但手动导入
SERVER_TOKEN并显式提供true:
import {SERVER_TOKEN} from '@angular/flex-layout'; {provide: SERVER_TOKEN, useValue: true}- 这样 Flex Layout 会跳过服务端样式的生成(指令不会收集任何样式到虚拟样式表)。
注意:如果同时提供了该 token 与
FlexLayoutServerModule,样式依然会被渲染。因为SERVER_PROVIDERS中的BEFORE_APP_SERIALIZED钩子仍会执行静态 CSS 生成。这一点在 core/tokens/server-token.ts 的注释中也有说明:SERVER_TOKEN是「告知是否已包含 Server 模块」的令牌,也可手动提供以在 SSR 时禁用样式。
该方案适合你打算完全自行处理服务端响应式样式(例如借助额外的 CSS 框架)的场景,但需要自行承担首屏一致性风险。
五、相关配置项:serverLoaded 与 ssrObserveBreakpoints
LayoutConfigOptions(见 core/tokens/library-config.ts)中有两个与服务端渲染直接相关的配置:
| 配置项 | 类型 | 默认值 | 作用 |
|---|---|---|---|
serverLoaded | boolean | false | 是否模拟「模块处于服务端模式」,配合FlexLayoutModule.withConfig({serverLoaded: true})在测试中模拟服务端行为 |
ssrObserveBreakpoints | string[] | [] | 服务端默认视为激活的断点别名列表,由ServerMatchMedia构造时解析 |
serverLoaded的用法可从测试用例窥见(如 flex/flex/flex.spec.ts 的FlexLayoutModule.withConfig({serverLoaded: true})):它让withConfig在提供配置的同时也提供{provide: SERVER_TOKEN, useValue: true}(见 projects/libs/flex-layout/module.ts),从而在浏览器测试环境中模拟服务端分支。
ssrObserveBreakpoints的解析逻辑在 server-match-media.ts:按别名在注册的断点中查找并加入_activeBreakpoints,之后buildMQL会把这些断点对应的查询标记为激活。示例:
FlexLayoutModule.withConfig({ ssrObserveBreakpoints: ['gt-sm', 'lt-lg'], });注意:ssrObserveBreakpoints只决定哪些断点在服务端渲染时「初始激活」,而静态 CSS 的生成(方案一)并不依赖它——generateStaticFlexLayoutStyles会遍历全部注册断点逐一激活并收集样式,二者的用途不同。
六、局限性:SSR 下的 DOM 能力不足
原文档明确指出 SSR 的一个固有缺陷:服务端缺乏能力完整的 DOM 渲染引擎,因此 Flex Layout 的部分功能会受损:
- 一些 Flex 指令会向上查找「带 flex 样式的父节点」,以避免覆盖父级样式;但如果这些样式定义在
<style>块、组件外部样式或独立样式表中,服务端无法找到它们(服务端没有getComputedStyle,StyleUtils.lookupStyle只能查行内样式与虚拟样式表,见 style-utils.ts)。
变通方案是:把所有与 Flex 相关的样式内联化。例如,若外部样式表中有一个设置flex-direction的 class,请把该样式直接内联到应用该 class 的元素上。
实际影响通常很小:因为这些样式的值在 bootstrap 时会被正确加载。但这是 SSR 与其服务端 DOM 实现带来的客观限制,需要在项目实践中注意。
七、在 Universal 应用中完整落地:参考 universal-demo-app
仓库自带一个完整的 SSR 演示应用universal-demo-app,可作为最佳实践范本(项目配置见 angular.json):
入口文件projects/apps/universal-demo-app/src/main.server.ts:导出AppServerModule供 Angular Universal 引导。
浏览器端模块projects/apps/universal-demo-app/src/app/app.module.ts:导入BrowserModule.withServerTransition({ appId: 'serverApp' })与FlexLayoutModule,是常规用法。
服务端模块projects/apps/universal-demo-app/src/app/app.server.module.ts:在AppModule之后导入ServerModule与FlexLayoutServerModule(顺序满足「Server 模块在后」的要求)。
Express 服务器projects/apps/universal-demo-app/server.ts:使用@nguniversal/express-engine的ngExpressEngine({ bootstrap: AppServerModule })渲染所有路由,并托管dist/universal-demo-app/browser下的静态资源,默认监听PORT环境变量或 4000 端口。
服务端编译配置projects/apps/universal-demo-app/tsconfig.server.json:entryModule指向app/app.server.module#AppServerModule,编译入口包含main.server.ts与server.ts。
构建与运行命令(来自 package.json):
# 先构建浏览器端,再构建服务端 bundle yarn build:universal-demo-app # 启动 SSR 开发服务器(@nguniversal/builders 的 ssr-dev-server) yarn serve:universal-demo-app此外,仓库还提供了 SSR 环境下的测试通道:yarn test:ssr通过 test/webpack-spec-ssr-bundle.js 与 test/jasmine-ssr.json 在服务端平台下运行全部*.spec.ts(测试入口见 projects/libs/flex-layout/test.ssr.ts),可用于验证指令在ServerTestingModule下的服务端行为。
八、小结与决策建议
| 场景 | 推荐做法 |
|---|---|
| 需要服务端首屏与客户端水合样式一致 | 导入FlexLayoutServerModule(方案一) |
| 旧项目、可接受行内样式与首屏不一致 | 不导入,仅依赖行内样式(方案二),注意启动警告 |
| 服务端完全不输出 Flex Layout 样式 | 提供{provide: SERVER_TOKEN, useValue: true}(方案三),切勿与 Server 模块同时使用 |
| 需要服务端初始激活特定断点 | 配置ssrObserveBreakpoints |
| 外部样式表中有 flex 相关样式 | 内联到对应元素,规避服务端样式查找限制 |
最后,本文所依据的完整指南还可见于仓库中的 guides/SSR.md(含更详细的背景与限制说明)以及入口点说明 projects/libs/flex-layout/server/README.md,建议与实际部署时结合阅读。
- 前端
- UI组件
【免费下载链接】flex-layout
Provides HTML UI layout for Angular applications; using Flexbox and a Responsive API
相关推荐
终极指南:3步解决Blender模型在Unity中的旋转错乱问题
终极指南:3步解决Blender模型在Unity中的旋转错乱问题 还在为Blender制作的精美模型导入Unity后方向错乱而烦恼吗?作为3D开发者,你一定经历
开发工具游戏开发res-downloader 十分钟上手:视频号、抖音视频资源采集实操指南
res downloader 十分钟上手:视频号、抖音视频资源采集实操指南 你有没有这样的经历:看中一条视频素材,要手动找下载入口、逐个粘贴链接、再等它一个个保
桌面应用网络音视频Angular SSR 服务端渲染完全指南:从 Angular Universal 原理到实践配置
Angular SSR 服务端渲染完全指南:从 Angular Universal 原理到实践配置 SSR(Server Side Rendering,服务端渲
文档教程知识库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考