跳至正文
Starone

能力

面向快速、无障碍、可维护 Web 应用的前端架构

浏览器是整个系统中最不受控的运行环境:设备未知,网络不可靠,输入不可信。我们把前端当作架构问题来做,先决定代码在哪里渲染、状态放在哪里、界面如何降级,而不是把它当成一项样式工作。

概览

前端工程设计的是系统中运行在团队无法掌控的硬件和网络上的那一部分。在拥塞的移动网络下,一部中端 Android 手机解析 JavaScript 的速度比开发者的笔记本慢上数倍,因此每一个架构选择都会直接体现为用户可感知的延迟:下发多少代码,HTML 在哪里生成,页面何时可交互,一次点击到得到结果之间隔着多少次网络往返。

同样的选择也决定了代码库和团队能扩展到什么程度。没有模块边界、没有类型化 API 契约、没有共享组件库的前端,最终会变成一个单一的可部署单元,任何改动都可能在别处引发回归,人越多交付反而越慢。在功能之间划定明确边界,建立由带版本的设计令牌与组件构成的设计系统,并自动生成 API 类型,多个团队才能并行交付,而不必为每次发布相互协调。

我们使用 TypeScript,搭配 React 与 Next.js 或 Vue 与 Nuxt,把渲染策略、状态归属、性能预算、无障碍和浏览器安全视为一等设计决策,按路由、按功能逐一确定。每项决策都有记录,在流水线中度量,并在生产环境中以真实用户数据持续观测。

我们构建什么

技术领域

01

组件架构与设计系统

组件分为展示型基础组件、组合模式和自行负责数据访问的功能模块,依赖只允许单向流动。颜色、间距、字体排印和动效的设计令牌是唯一事实来源,并导出为 CSS 自定义属性,切换主题或品牌时无需改动组件代码。针对导入路径的 lint 规则强制执行这些边界,使其不随团队扩张而失效。

  • 设计令牌与 CSS 自定义属性
  • 无样式(headless)的无障碍基础组件
  • 以 Storybook 作为组件工作台与文档
  • 按功能切片的模块边界
  • TypeScript 严格模式
  • 视觉回归测试
02

状态管理与 API 集成

服务端状态是后端所拥有数据的一份缓存,需要处理获取、去重、失效和重试;客户端状态则是短暂的界面状态,例如面板开合、草稿和选中项。把两者塞进同一个全局 store,是数据过期和多余重渲染最常见的根源。我们把服务端状态放在查询缓存中,把可分享的状态放在 URL 中,剩下的才交给客户端 store。

  • 以 TanStack Query 与 SWR 管理服务端状态
  • 以 Zustand、Redux Toolkit 与 Pinia 管理客户端状态
  • 以 URL 查询参数承载状态
  • 基于 OpenAPI 与 GraphQL 生成类型
  • 带回滚的乐观更新
  • 请求取消与去重
03

渲染策略

服务端渲染、静态生成、客户端渲染和流式渲染,在首字节时间、可交互时间、可缓存性和基础设施成本之间各有不同的取舍。具体选择按路由进行,取决于内容需要多大程度的个性化和多高的新鲜度。水合(hydration)成本按预算管理,通过服务端组件、islands 架构和延迟水合来压缩。

  • 服务端渲染(SSR)
  • 静态生成与增量再生成
  • 基于 Suspense 边界的流式 SSR
  • React Server Components
  • 单页应用(SPA)外壳
  • 选择性水合与延迟水合
  • CDN 与边缘缓存
04

性能与 Core Web Vitals

Core Web Vitals 在真实用户的第 75 百分位上衡量加载(Largest Contentful Paint)、响应性(Interaction to Next Paint)和视觉稳定性(Cumulative Layout Shift),官方公布的“良好”阈值分别为 2.5 秒、200 毫秒和 0.1。我们为包体积和长任务设定预算,在 CI 中强制执行,并用实测数据(field data)加以验证,因为实验室分数反映不了低端设备上的表现。

  • 路由级与组件级代码分割
  • AVIF 与 WebP 格式的响应式图片
  • 字体子集化与 font-display 控制
  • 优先级提示与资源预加载
  • 长任务拆分与主线程调度
  • Lighthouse CI 预算
  • 真实用户监控(RUM)
05

无障碍与响应式架构

无障碍应当在组件库层面内建,那里成本低;等页面做完再靠审计去补,成本就高了。我们以 WCAG 2.2 AA 级为目标:语义化 HTML 优先,只有原生语义不够用时才使用 ARIA,全部功能可用键盘操作,并在对话框和路由切换时管理好焦点。布局依据容器尺寸和输入方式做出响应,而不是依据一份固定的设备宽度清单。

  • 语义化 HTML 与地标(landmark)结构
  • 键盘导航与焦点管理
  • 使用 NVDA 与 VoiceOver 进行屏幕阅读器测试
  • 使用 axe-core 进行自动化检查
  • 容器查询与流式字号体系
  • 减弱动效与颜色对比度支持
06

安全、PWA 与实时界面

浏览器中的一切对用户都是可见的,页面上执行的任何脚本也都能触及,因此前端在设计上应当持有尽可能少的权限。在产品确有需要时,Service Worker 可提供离线缓存、可安装能力和后台同步。实时数据在单向推送时使用 Server-Sent Events,在客户端也需要低延迟发送时使用 WebSocket。

  • 基于 nonce 的 Content Security Policy(CSP)
  • 以输出编码与 Trusted Types 防御 XSS
  • HttpOnly、Secure、SameSite 会话 Cookie
  • Subresource Integrity 与依赖审计
  • Service Worker 与 Workbox 缓存策略
  • WebSocket 与 Server-Sent Events

按路由选择渲染策略

渲染方式不是一个应用级的全局设置。同一个产品里的营销页、商品列表、登录后的仪表盘和文档编辑器,对新鲜度、个性化和交互性的要求各不相同,而 Next.js、Nuxt 这类框架允许每条路由各自选择。决策取决于两个问题:这份 HTML 能否在用户之间共享,以及它最多可以过期多久。

水合是服务端渲染的隐性成本。服务端先发送 HTML,客户端再下载并执行同一棵组件树来绑定事件处理器,在此完成之前,页面看起来已经就绪,实际上却没有响应。服务端组件把非交互组件彻底移出客户端包;流式渲染则先立即发送外壳,较慢的数据在 Suspense 边界之后陆续就绪,一条慢查询不再拖住整个响应。

  • 静态生成:内容对所有用户相同、仅在发布时变化,例如文档和营销页;由 CDN 直接提供,延迟和成本最低
  • 增量再生成:适用于全量重建耗时过长的大型目录,且可以接受数秒到数分钟的内容滞后
  • SSR:个性化或依赖请求上下文的页面,需要可被索引的 HTML 和较快的首次绘制,代价是每次请求都消耗服务端算力
  • 流式 SSR:外壳需要快速返回、数据源却较慢的页面
  • SPA:登录后使用、交互密集的工具,无需搜索引擎索引且会话较长,较重的首次加载可以被摊薄

架构

前端渲染策略

构建时(SSG)

  1. 01

    内容与数据

    构建时读 CMS、Markdown、API

  2. 02

    静态生成

    页面一次性渲染为 HTML

  3. 03

    CDN

    缓存后由边缘节点分发

  4. 04

    重新验证

    内容变更或定时触发再生成

请求时(SSR)

  1. 01

    请求

    需要用户专属或最新数据

  2. 02

    服务端渲染

    获取数据,渲染组件

  3. 03

    流式传输

    先发页面骨架,再发较慢部分

  4. 04

    水合

    在浏览器中绑定事件处理

客户端(SPA)

  1. 01

    客户端状态

    UI 状态与乐观更新

  2. 02

    API 调用

    向后端发起类型化请求

  3. 03

    缓存

    去重、重新验证、失效

  4. 04

    导航

    带预取的客户端路由

页面在哪里渲染,决定了它的速度、新鲜度与成本。多数产品三者并用:稳定内容用静态生成,个性化或实时数据用服务端渲染,交互用客户端状态。

服务端状态与客户端状态是两类不同的问题

应用展示的大部分内容并不是它自己的状态,而是后端数据的一份快照:随时可能过期,可能同时被多个组件请求,并且在数据变更之后必须重新获取。查询缓存正是对这一点的直接建模:每个资源都有键、过期窗口、重试策略和失效规则,组件订阅这个键,而不是把数据复制进 store。

服务端状态移出全局 store 之后,剩下的通常不多:表单草稿、向导进度、界面偏好。筛选条件、排序、分页和选中的标签页应当放在 URL 中,这样视图可以通过链接分享,刷新后能够恢复,后退按钮的行为也保持正确。只有被相距较远的组件共享、又高频变化的状态,才值得使用专门的客户端 store,例如画布编辑器或交易下单面板。

前端架构如何限制或支撑团队规模

只有改动能够保持局部化,前端才能随团队一起扩展。这要求功能模块具备公开接口,功能之间不得互相导入内部实现,并且有一个带版本、有明确负责人的共享层,承载设计系统组件和 API 客户端。在 monorepo 中,Turborepo 或 Nx 等工具利用依赖图只构建和测试受改动影响的部分,使流水线耗时在代码库增长时基本保持平稳。

微前端更进一步,让每个团队拥有可独立部署的一块,再通过模块联邦或在边缘层进行组合。它解决的是组织问题,即多个团队需要各自独立的发布节奏;代价是依赖重复、一致性更难保证、运行时更复杂。对大多数产品而言,边界得到强制执行的模块化单体前端可以提供同样的隔离,却只需要一次构建和一次部署。

API 契约是另一个制约扩展的因素。由 OpenAPI 文档或 GraphQL schema 生成的类型,能把后端的破坏性变更变成流水线中的编译错误,而不是生产环境里的运行时故障,同时也让每个组件省去一整类防御性代码。

我们如何构建

我们的方法

我们从产品使用者和维护团队所面对的约束出发,在动手构建任何页面之前,先把那些事后更改代价高昂的决策确定下来。

  1. 01

    明确约束与预算

    我们梳理目标设备、网络条件、搜索索引需求、无障碍义务和浏览器支持范围,再把它们转化为可量化的预算:每条路由的 JavaScript 体积、Core Web Vitals 以及交互延迟。

  2. 02

    按路由确定渲染方式与数据归属

    为每条路由指定渲染策略和缓存策略,为每一份状态指定归属:服务端缓存、URL、组件或客户端 store。会话模型和令牌处理方式也在这一阶段与后端一并敲定。

  3. 03

    先建设计系统

    在功能开发之前,先实现设计令牌和无障碍基础组件,并在 Storybook 中形成文档。键盘行为、焦点处理和对比度在组件层面验证一次,所有页面即可继承。

  4. 04

    基于类型化契约实现功能

    功能模块使用生成的 API 类型和 mock 服务器,前后端因此可以并行推进。单元测试覆盖逻辑,组件测试覆盖行为,Playwright 端到端覆盖关键用户路径。

  5. 05

    在流水线中把住质量关

    每个 pull request 都会针对预览部署运行类型检查、lint、无障碍检查、包体积对比和 Lighthouse 预算。超出预算的改动会直接导致构建失败,而不是等到发布之后才被发现。

  6. 06

    在生产环境度量并迭代

    真实用户监控按路由、设备等级和发布版本上报 Core Web Vitals、JavaScript 错误和交互追踪。回归可以追溯到具体某次部署,并依据实测数据进行修复。

取舍

决定设计走向的决策

在确定架构之前,我们会与您的团队逐一厘清的问题。

这个应用应该采用服务端渲染,还是做成单页应用?
如果页面需要被索引、需要以带预览的链接分享,或需要在低端设备上尽快完成绘制,就选择服务端渲染,并接受服务端算力和水合带来的成本。对于会话长、交互重的登录后工具,选择 SPA:首次加载的开销只付一次,部署在 CDN 上的静态外壳运维起来也更简单。很多产品两者都需要,按路由拆分即可。
选 React 配 Next.js,还是 Vue 配 Nuxt?
两个生态都覆盖 SSR、静态生成、流式渲染和类型化数据获取,因此这主要是一个关于团队的决策。React 的人才储备和类库生态更大,服务端组件模型也更成熟;Vue 的 API 面更小,细粒度响应式也减少了手动 memoization 的需要。团队现有的技能和现有的代码库,比两者之间的任何技术差异都更重要。
我们真的需要全局状态库吗?
通常比预想的要少。先把服务端数据放进查询缓存,把视图状态放进 URL,把局部状态留在组件里,再看还剩下什么。当状态被广泛共享、频繁更新,并且需要 devtools 或时间旅行调试时,再引入 Zustand、Redux Toolkit 或 Pinia;Redux Toolkit 更严格的结构主要在大型团队中才能体现价值。
浏览器应该把认证令牌存放在哪里?
优先采用 BFF(backend-for-frontend):令牌保存在服务端,浏览器只拿到一个 HttpOnly、Secure、SameSite 的 Cookie,并对会改变状态的请求加上 CSRF 防护。存放在 localStorage 中的令牌可以被页面上运行的任何脚本读取,一个 XSS 漏洞或一个被攻陷的依赖就足以让它泄露。如果令牌必须留在浏览器中,就只保存在内存里,保持短有效期,并轮换刷新令牌。
单体前端、monorepo 还是微前端?
从一个强制执行模块边界的单一应用起步。当多个应用共享设计系统和 API 客户端时,再迁移到带共享包的 monorepo。只有当相互独立的团队被同一条发布列车卡住时,才采用微前端,因为运行时组合、版本偏差和重复的加载体积都是长期存在的成本。
实时数据用 WebSocket、Server-Sent Events 还是轮询?
通知、进度、AI 逐 token 输出这类由服务端流向客户端的数据流,使用 Server-Sent Events:它运行在普通 HTTP 之上,会自动重连,也容易穿过代理。客户端同样需要频繁发送消息时,例如协作或交易场景,使用 WebSocket。如果更新不频繁,且可以接受数秒的延迟,带条件请求的轮询依然是正确的选择。

生产就绪

怎样才算生产就绪

如何保障安全

严格的基于 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 聚合调用并裁剪响应数据。分析、用户同意管理、功能开关和内容管理等服务都通过轻量适配层接入,更换供应商时无需改动功能代码。

技术

根据工作负载和您现有的技术资产来选择。

  • TypeScript
  • React
  • Next.js
  • Vue
  • Nuxt
  • Vite
  • TanStack Query
  • Tailwind CSS
  • Storybook
  • Playwright
  • Vitest
  • axe-core
  • Workbox
  • OpenTelemetry

从零到生产

工程生命周期

  1. 01

    想法

  2. 02

    探索

  3. 03

    系统架构

  4. 04

    UX / 产品设计

  5. 05

    应用工程

  6. 06

    AI / 数据层

  7. 07

    云基础设施

  8. 08

    安全

  9. 09

    质量保证与性能

  10. 10

    部署

  11. 11

    可观测性

  12. 12

    持续演进

常见问题

常见问题

前端架构如何影响用户体验?

影响是直接的。渲染策略决定内容多快出现,JavaScript 的体积决定页面多快可以响应,状态与数据获取的组织方式决定交互是即时完成还是要等待网络。布局稳定性、焦点行为和离线处理同样是架构的结果,而不是视觉设计的结果。

什么是 Core Web Vitals,为什么重要?

它是 Google 定义的三项实测指标:衡量加载的 Largest Contentful Paint、衡量响应性的 Interaction to Next Paint 和衡量视觉稳定性的 Cumulative Layout Shift,均以真实页面访问的第 75 百分位来评估。它们之所以重要,是因为描述的是用户在真实设备上的实际体验,同时也是搜索排名所采用的信号之一。

现有前端能否在不重写的前提下完成现代化改造?

大多数情况下可以。渐进式迁移把新旧应用放在同一个路由器或反向代理之后,一次迁移一条路由,两者之间共享会话和设计令牌。这样产品在整个过程中始终可以发布,每条迁移完成的路由也可以与它所替换的旧路由进行对比度量。

什么情况下适合选择渐进式 Web 应用(PWA)?

当产品能从可安装、离线访问或后台同步中受益,但不需要深度的设备集成时。PWA 与网站共用一套代码库,也无需经过应用商店审核,但部分设备能力的访问、后台执行和推送行为在不同平台上存在差异,在 iOS 上尤其明显。如果这些限制对产品很关键,原生或跨平台移动应用会更合适。

无障碍是如何验证的?

axe-core 等自动化工具只能发现少部分问题,主要是缺失的名称、角色和对比度不达标,因此它们作为基线在 CI 中运行。其余问题则对照 WCAG 2.2 AA 级成功标准,在关键用户路径上通过纯键盘走查以及使用 NVDA 和 VoiceOver 的屏幕阅读器测试来验证。

有一套必须在生产环境中正常运行的系统?

告诉我们您正在构建什么。工程师会阅读您的留言,并在两个工作日内回复。