组件架构与设计系统
组件分为展示型基础组件、组合模式和自行负责数据访问的功能模块,依赖只允许单向流动。颜色、间距、字体排印和动效的设计令牌是唯一事实来源,并导出为 CSS 自定义属性,切换主题或品牌时无需改动组件代码。针对导入路径的 lint 规则强制执行这些边界,使其不随团队扩张而失效。
- 设计令牌与 CSS 自定义属性
- 无样式(headless)的无障碍基础组件
- 以 Storybook 作为组件工作台与文档
- 按功能切片的模块边界
- TypeScript 严格模式
- 视觉回归测试
能力
浏览器是整个系统中最不受控的运行环境:设备未知,网络不可靠,输入不可信。我们把前端当作架构问题来做,先决定代码在哪里渲染、状态放在哪里、界面如何降级,而不是把它当成一项样式工作。
前端工程设计的是系统中运行在团队无法掌控的硬件和网络上的那一部分。在拥塞的移动网络下,一部中端 Android 手机解析 JavaScript 的速度比开发者的笔记本慢上数倍,因此每一个架构选择都会直接体现为用户可感知的延迟:下发多少代码,HTML 在哪里生成,页面何时可交互,一次点击到得到结果之间隔着多少次网络往返。
同样的选择也决定了代码库和团队能扩展到什么程度。没有模块边界、没有类型化 API 契约、没有共享组件库的前端,最终会变成一个单一的可部署单元,任何改动都可能在别处引发回归,人越多交付反而越慢。在功能之间划定明确边界,建立由带版本的设计令牌与组件构成的设计系统,并自动生成 API 类型,多个团队才能并行交付,而不必为每次发布相互协调。
我们使用 TypeScript,搭配 React 与 Next.js 或 Vue 与 Nuxt,把渲染策略、状态归属、性能预算、无障碍和浏览器安全视为一等设计决策,按路由、按功能逐一确定。每项决策都有记录,在流水线中度量,并在生产环境中以真实用户数据持续观测。
我们构建什么
组件分为展示型基础组件、组合模式和自行负责数据访问的功能模块,依赖只允许单向流动。颜色、间距、字体排印和动效的设计令牌是唯一事实来源,并导出为 CSS 自定义属性,切换主题或品牌时无需改动组件代码。针对导入路径的 lint 规则强制执行这些边界,使其不随团队扩张而失效。
服务端状态是后端所拥有数据的一份缓存,需要处理获取、去重、失效和重试;客户端状态则是短暂的界面状态,例如面板开合、草稿和选中项。把两者塞进同一个全局 store,是数据过期和多余重渲染最常见的根源。我们把服务端状态放在查询缓存中,把可分享的状态放在 URL 中,剩下的才交给客户端 store。
服务端渲染、静态生成、客户端渲染和流式渲染,在首字节时间、可交互时间、可缓存性和基础设施成本之间各有不同的取舍。具体选择按路由进行,取决于内容需要多大程度的个性化和多高的新鲜度。水合(hydration)成本按预算管理,通过服务端组件、islands 架构和延迟水合来压缩。
Core Web Vitals 在真实用户的第 75 百分位上衡量加载(Largest Contentful Paint)、响应性(Interaction to Next Paint)和视觉稳定性(Cumulative Layout Shift),官方公布的“良好”阈值分别为 2.5 秒、200 毫秒和 0.1。我们为包体积和长任务设定预算,在 CI 中强制执行,并用实测数据(field data)加以验证,因为实验室分数反映不了低端设备上的表现。
无障碍应当在组件库层面内建,那里成本低;等页面做完再靠审计去补,成本就高了。我们以 WCAG 2.2 AA 级为目标:语义化 HTML 优先,只有原生语义不够用时才使用 ARIA,全部功能可用键盘操作,并在对话框和路由切换时管理好焦点。布局依据容器尺寸和输入方式做出响应,而不是依据一份固定的设备宽度清单。
浏览器中的一切对用户都是可见的,页面上执行的任何脚本也都能触及,因此前端在设计上应当持有尽可能少的权限。在产品确有需要时,Service Worker 可提供离线缓存、可安装能力和后台同步。实时数据在单向推送时使用 Server-Sent Events,在客户端也需要低延迟发送时使用 WebSocket。
渲染方式不是一个应用级的全局设置。同一个产品里的营销页、商品列表、登录后的仪表盘和文档编辑器,对新鲜度、个性化和交互性的要求各不相同,而 Next.js、Nuxt 这类框架允许每条路由各自选择。决策取决于两个问题:这份 HTML 能否在用户之间共享,以及它最多可以过期多久。
水合是服务端渲染的隐性成本。服务端先发送 HTML,客户端再下载并执行同一棵组件树来绑定事件处理器,在此完成之前,页面看起来已经就绪,实际上却没有响应。服务端组件把非交互组件彻底移出客户端包;流式渲染则先立即发送外壳,较慢的数据在 Suspense 边界之后陆续就绪,一条慢查询不再拖住整个响应。
架构
构建时(SSG)
内容与数据
构建时读 CMS、Markdown、API
静态生成
页面一次性渲染为 HTML
CDN
缓存后由边缘节点分发
重新验证
内容变更或定时触发再生成
请求时(SSR)
请求
需要用户专属或最新数据
服务端渲染
获取数据,渲染组件
流式传输
先发页面骨架,再发较慢部分
水合
在浏览器中绑定事件处理
客户端(SPA)
客户端状态
UI 状态与乐观更新
API 调用
向后端发起类型化请求
缓存
去重、重新验证、失效
导航
带预取的客户端路由
应用展示的大部分内容并不是它自己的状态,而是后端数据的一份快照:随时可能过期,可能同时被多个组件请求,并且在数据变更之后必须重新获取。查询缓存正是对这一点的直接建模:每个资源都有键、过期窗口、重试策略和失效规则,组件订阅这个键,而不是把数据复制进 store。
服务端状态移出全局 store 之后,剩下的通常不多:表单草稿、向导进度、界面偏好。筛选条件、排序、分页和选中的标签页应当放在 URL 中,这样视图可以通过链接分享,刷新后能够恢复,后退按钮的行为也保持正确。只有被相距较远的组件共享、又高频变化的状态,才值得使用专门的客户端 store,例如画布编辑器或交易下单面板。
只有改动能够保持局部化,前端才能随团队一起扩展。这要求功能模块具备公开接口,功能之间不得互相导入内部实现,并且有一个带版本、有明确负责人的共享层,承载设计系统组件和 API 客户端。在 monorepo 中,Turborepo 或 Nx 等工具利用依赖图只构建和测试受改动影响的部分,使流水线耗时在代码库增长时基本保持平稳。
微前端更进一步,让每个团队拥有可独立部署的一块,再通过模块联邦或在边缘层进行组合。它解决的是组织问题,即多个团队需要各自独立的发布节奏;代价是依赖重复、一致性更难保证、运行时更复杂。对大多数产品而言,边界得到强制执行的模块化单体前端可以提供同样的隔离,却只需要一次构建和一次部署。
API 契约是另一个制约扩展的因素。由 OpenAPI 文档或 GraphQL schema 生成的类型,能把后端的破坏性变更变成流水线中的编译错误,而不是生产环境里的运行时故障,同时也让每个组件省去一整类防御性代码。
我们如何构建
我们从产品使用者和维护团队所面对的约束出发,在动手构建任何页面之前,先把那些事后更改代价高昂的决策确定下来。
我们梳理目标设备、网络条件、搜索索引需求、无障碍义务和浏览器支持范围,再把它们转化为可量化的预算:每条路由的 JavaScript 体积、Core Web Vitals 以及交互延迟。
为每条路由指定渲染策略和缓存策略,为每一份状态指定归属:服务端缓存、URL、组件或客户端 store。会话模型和令牌处理方式也在这一阶段与后端一并敲定。
在功能开发之前,先实现设计令牌和无障碍基础组件,并在 Storybook 中形成文档。键盘行为、焦点处理和对比度在组件层面验证一次,所有页面即可继承。
功能模块使用生成的 API 类型和 mock 服务器,前后端因此可以并行推进。单元测试覆盖逻辑,组件测试覆盖行为,Playwright 端到端覆盖关键用户路径。
每个 pull request 都会针对预览部署运行类型检查、lint、无障碍检查、包体积对比和 Lighthouse 预算。超出预算的改动会直接导致构建失败,而不是等到发布之后才被发现。
真实用户监控按路由、设备等级和发布版本上报 Core Web Vitals、JavaScript 错误和交互追踪。回归可以追溯到具体某次部署,并依据实测数据进行修复。
取舍
在确定架构之前,我们会与您的团队逐一厘清的问题。
生产就绪
严格的基于 nonce 的 Content Security Policy、框架自带的输出编码和 Trusted Types 共同限制跨站脚本的影响范围,会话凭证保存在 HttpOnly Cookie 中,脚本无法触及。依赖通过 lockfile 锁定版本,在 CI 中接受审计,从第三方源加载时启用 Subresource Integrity。授权始终由 API 强制执行;在界面上隐藏某个控件只是可用性手段,不是安全控制。
静态资源以内容哈希命名、按不可变方式缓存并由 CDN 提供,因此流量增长大多落在边缘而不是源站。服务端渲染的路由以无状态进程的形式水平扩展,cache-control 与 stale-while-revalidate 按路由调优。代码库规模的增长则通过模块边界、仅构建受影响部分和远程构建缓存来应对。
真实用户监控记录 LCP、INP 和 CLS,并归因到具体的元素或交互,按路由、设备和发布版本分段。错误捕获配合私有上传的 source map;浏览器端的 span 通过 OpenTelemetry 传播到后端链路追踪,一次缓慢的交互可以一路追到导致它的那个 API。
每个 pull request 都会生成一个隔离的预览部署,自动化检查和人工检查都在其上进行。发布是原子且不可变的,回滚只需把指针切回上一个构建,功能开关则把部署与发布解耦。在发布过程中,上一版本的资源仍然保持可用,已打开的会话不会因为缺失 chunk 而出错。
前端通过生成的客户端调用 REST 或 GraphQL API,通过 OpenID Connect 完成认证,并经由 SSE 或 WebSocket 接收实时数据。当通用 API 调用过于零碎时,由 BFF 聚合调用并裁剪响应数据。分析、用户同意管理、功能开关和内容管理等服务都通过轻量适配层接入,更换供应商时无需改动功能代码。
根据工作负载和您现有的技术资产来选择。
从零到生产
想法
探索
系统架构
UX / 产品设计
应用工程
AI / 数据层
云基础设施
安全
质量保证与性能
部署
可观测性
持续演进
常见问题
影响是直接的。渲染策略决定内容多快出现,JavaScript 的体积决定页面多快可以响应,状态与数据获取的组织方式决定交互是即时完成还是要等待网络。布局稳定性、焦点行为和离线处理同样是架构的结果,而不是视觉设计的结果。
它是 Google 定义的三项实测指标:衡量加载的 Largest Contentful Paint、衡量响应性的 Interaction to Next Paint 和衡量视觉稳定性的 Cumulative Layout Shift,均以真实页面访问的第 75 百分位来评估。它们之所以重要,是因为描述的是用户在真实设备上的实际体验,同时也是搜索排名所采用的信号之一。
大多数情况下可以。渐进式迁移把新旧应用放在同一个路由器或反向代理之后,一次迁移一条路由,两者之间共享会话和设计令牌。这样产品在整个过程中始终可以发布,每条迁移完成的路由也可以与它所替换的旧路由进行对比度量。
当产品能从可安装、离线访问或后台同步中受益,但不需要深度的设备集成时。PWA 与网站共用一套代码库,也无需经过应用商店审核,但部分设备能力的访问、后台执行和推送行为在不同平台上存在差异,在 iOS 上尤其明显。如果这些限制对产品很关键,原生或跨平台移动应用会更合适。
axe-core 等自动化工具只能发现少部分问题,主要是缺失的名称、角色和对比度不达标,因此它们作为基线在 CI 中运行。其余问题则对照 WCAG 2.2 AA 级成功标准,在关键用户路径上通过纯键盘走查以及使用 NVDA 和 VoiceOver 的屏幕阅读器测试来验证。