外观
前端质量保障、安全、可访问性与可观测性系统学习文档
适合人群:希望从“能开发功能”进阶到“能保障生产质量”的前端学习者,尤其适合准备前端架构师、技术负责人、高级前端岗位。
学习目标:系统掌握测试体系、安全防护、可访问性、错误监控、性能监控、日志指标链路追踪、线上问题治理和质量门禁。
核对时间:2026-06-30。本文参考 Playwright、Vitest、Testing Library、Storybook、OWASP、W3C WCAG、MDN、OpenTelemetry、Sentry、web.dev Web Vitals 等官方资料整理。
目录
- 为什么架构师必须关注质量与稳定性
- 前端质量保障体系总览
- 测试分层
- 单元测试与组件测试
- 集成测试与 E2E 测试
- 视觉回归、可访问性测试与契约测试
- 前端安全基础
- XSS、CSRF、CSP 与鉴权安全
- 依赖与供应链安全
- 可访问性体系
- 可观测性体系
- 错误监控与 Source Map 管理
- 性能监控与 Web Vitals
- 日志、指标与链路追踪
- 线上问题排查与事故复盘
- 质量门禁与团队治理
- 学习路线与练习
- 官方参考资料
1. 为什么架构师必须关注质量与稳定性
前端架构师的职责不只是选框架、搭目录、配构建。真正上线后,用户遇到的是:
- 页面白屏。
- 点击无响应。
- 数据显示错误。
- 按钮没有权限却能点。
- 弱网加载失败。
- 屏幕阅读器无法使用。
- 新版本发布后大量报错。
- 线上问题没人能快速定位。
这些都属于前端质量和生产稳定性问题。
架构师要建立体系,让问题尽量在上线前被拦截,上线后能被发现、定位、降级和复盘。
2. 前端质量保障体系总览
完整质量体系包括:
| 层级 | 目标 | 典型工具 |
|---|---|---|
| 静态检查 | 提前发现代码问题 | ESLint、TypeScript、Stylelint |
| 格式化 | 保持风格一致 | Prettier |
| 单元测试 | 验证函数和小模块 | Vitest、Jest |
| 组件测试 | 验证组件行为 | Testing Library、Storybook |
| 集成测试 | 验证模块协作 | Vitest、MSW |
| E2E 测试 | 验证真实用户链路 | Playwright、Cypress |
| 视觉回归 | 验证 UI 未意外变化 | Storybook、Chromatic |
| 可访问性测试 | 验证基础 a11y | axe、Storybook a11y |
| 性能检查 | 防止性能退化 | Lighthouse CI、bundle size |
| 安全扫描 | 发现依赖和配置风险 | npm audit、Snyk、GitHub Dependabot |
| 监控告警 | 发现线上问题 | Sentry、OpenTelemetry、RUM |
2.1 测试不是越多越好
测试要服务于风险。
优先覆盖:
- 登录。
- 支付。
- 下单。
- 权限。
- 数据提交。
- 核心表单。
- 关键业务流程。
- 历史高频 bug。
不要把大量测试写在低价值细节上,否则维护成本会吞掉收益。
3. 测试分层
3.1 测试金字塔
常见结构:
- 大量单元测试。
- 适量组件和集成测试。
- 少量关键 E2E 测试。
原因:
- 单元测试快、稳定、定位准确。
- E2E 更接近真实用户,但慢、容易受环境影响。
- 组件测试在 UI 组件库和业务组件中非常有价值。
3.2 前端项目推荐比例
业务后台:
- 单元测试:工具函数、权限、数据处理。
- 组件测试:表单、表格、弹窗。
- E2E:登录、核心 CRUD、权限链路。
组件库:
- 组件测试。
- 视觉回归。
- 可访问性测试。
- 类型测试。
营销页:
- E2E smoke。
- 视觉回归。
- 性能检查。
- SEO 和可访问性检查。
3.3 测试命名
好的测试描述用户行为,而不是实现细节:
ts
it("shows validation message when email is invalid", async () => {
// ...
});不推荐:
ts
it("sets error state to true", () => {
// ...
});4. 单元测试与组件测试
4.1 Vitest
Vitest 适合现代 Vite / TypeScript 项目:
ts
import { describe, expect, it } from "vitest";
import { formatPrice } from "./formatPrice";
describe("formatPrice", () => {
it("formats cents to currency text", () => {
expect(formatPrice(1234)).toBe("$12.34");
});
});4.2 Testing Library
Testing Library 的核心思想是:测试越接近用户使用方式,越能给你信心。
React 示例:
tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { LoginForm } from "./LoginForm";
it("submits login form", async () => {
const user = userEvent.setup();
render(<LoginForm />);
await user.type(screen.getByLabelText("Email"), "ada@example.com");
await user.type(screen.getByLabelText("Password"), "secret");
await user.click(screen.getByRole("button", { name: "Login" }));
expect(await screen.findByText("Welcome")).toBeInTheDocument();
});4.3 Mock 策略
适合 mock:
- 网络请求。
- 时间。
- 随机数。
- 浏览器 API。
- 第三方 SDK。
不建议 mock:
- 被测模块本身的核心逻辑。
- 用户可见行为。
- 框架基础能力。
4.4 MSW
MSW 可以在测试和开发中模拟 API:
ts
import { http, HttpResponse } from "msw";
export const handlers = [
http.get("/api/users", () => {
return HttpResponse.json([{ id: 1, name: "Ada" }]);
}),
];优点:
- 更接近真实网络层。
- 测试不依赖后端。
- 可复用 mock 场景。
5. 集成测试与 E2E 测试
5.1 集成测试
集成测试关注多个模块协作:
- 页面 + 请求层。
- 表单 + 校验 + 提交。
- 状态管理 + 组件。
- 路由 + 权限。
它比单元测试更接近业务,但比 E2E 更轻。
5.2 Playwright
Playwright 用真实浏览器测试用户流程。
示例:
ts
import { expect, test } from "@playwright/test";
test("user can login", async ({ page }) => {
await page.goto("/login");
await page.getByLabel("Email").fill("ada@example.com");
await page.getByLabel("Password").fill("secret");
await page.getByRole("button", { name: "Login" }).click();
await expect(page.getByText("Dashboard")).toBeVisible();
});5.3 E2E 最佳实践
建议:
- 只覆盖关键链路。
- 使用稳定选择器和语义选择器。
- 每个测试独立。
- 准备确定性测试数据。
- 避免依赖测试执行顺序。
- 开启 trace、video、screenshot 便于排查。
- CI 中分片并行。
5.4 E2E 常见不稳定原因
- 测试数据共享。
- 网络不稳定。
- 动画未等待。
- 使用脆弱 CSS selector。
- 异步状态未等待。
- 第三方服务不可控。
- 环境资源不足。
Playwright 有自动等待能力,但测试仍要围绕用户可见状态写断言。
6. 视觉回归、可访问性测试与契约测试
6.1 视觉回归
适合:
- 组件库。
- 营销页。
- 设计系统。
- 高价值页面。
工具:
- Storybook。
- Chromatic。
- Playwright screenshot。
注意:
- 动态时间、随机数据要固定。
- 字体和截图环境要稳定。
- 不要对频繁变化区域做严格截图。
6.2 可访问性测试
自动化工具可以发现:
- 缺少 label。
- 颜色对比不足。
- ARIA 错误。
- heading 结构问题。
- 图片缺少 alt。
但自动化不能替代人工测试。键盘导航和屏幕阅读器体验仍需要人工验证。
自动化扫描的实际覆盖面:axe-core 的规则集映射到 WCAG 成功标准,但只能判定“机器可判定”的子集——业界统计自动化大约只能发现 30%~50% 的无障碍问题,对比度(需渲染后颜色)、键盘可达性、焦点顺序、屏幕阅读器朗读质量都必须人工。接入方式:@axe-core/playwright(E2E 注入页面扫描)、jest-axe(组件测试断言)、Storybook addon-a11y(每个 story 跑一遍)。规则可按 rule.id 关闭,但关闭必须留注释说明原因——否则扫描器很快被 disable all 掏空。
6.3 契约测试
前后端契约可以通过:
- OpenAPI。
- GraphQL schema。
- Pact。
- 类型生成。
目标:
- 后端接口变化提前发现。
- 前端 mock 与真实接口一致。
- 避免运行时才发现字段缺失。
7. 前端安全基础
7.1 前端安全责任
前端不能替代后端安全,但前端必须负责:
- 不把用户输入当可信 HTML。
- 不泄漏 token 和敏感数据。
- 正确处理鉴权状态。
- 防止第三方脚本扩大风险。
- 配合 CSP。
- 避免把 secret 打进产物。
- 做依赖安全治理。
7.2 常见风险
- XSS。
- CSRF。
- 点击劫持。
- 开放重定向。
- 敏感信息泄漏。
- 不安全 token 存储。
- 依赖漏洞。
- source map 泄露。
- 第三方脚本被污染。
8. XSS、CSRF、CSP 与鉴权安全
8.1 XSS
XSS:攻击者把恶意脚本注入页面。
危险示例:
tsx
<div dangerouslySetInnerHTML={{ __html: userInput }} />防护:
- 默认使用框架转义能力。
- 避免直接插入 HTML。
- 必须插入 HTML 时使用可信净化库。
- 配置 CSP。
- cookie 使用
HttpOnly、Secure、SameSite。
8.1.1 XSS 的三种类型与利用路径
| 类型 | 数据流向 | 典型场景 | 特点 |
|---|---|---|---|
| 存储型(Stored) | 用户输入 → 数据库 → 渲染给其他用户 | 评论、个人资料、富文本 | 影响所有看到数据的用户,危害最大 |
| 反射型(Reflected) | URL 参数 → 服务端拼进响应 | 搜索结果回显 | 需要诱导点击,仍要按漏洞处理 |
| DOM 型(DOM-based) | location.hash 等 → 危险 DOM API | 纯前端路由/渲染 | 服务器完全看不到攻击载荷,后端过滤无效 |
理解利用路径才能理解防御:浏览器有两个“解释器”——HTML 解析器和 JS 引擎。XSS 的本质是数据被提升为代码:一段本应是文本的输入,因为输出位置落在可执行上下文(<script> 内、事件属性、javascript: URL、innerHTML),被解析器当指令执行。DOM 型 XSS 的源(source)是 location/postMessage 等浏览器 API,汇(sink)是 innerHTML、document.write、eval、setTimeout(string)、srcdoc 等危险 API,审计前端代码就是审计 source→sink 链。
8.1.2 上下文感知输出编码(OWASP)
“转义”不是一个动作,而是按输出上下文选择编码器(OWASP XSS Prevention Cheat Sheet 的核心规则):
| 输出上下文 | 编码方式 | 要点 |
|---|---|---|
| HTML 文本节点 | HTML Entity 编码(& < > " ' /) | React/Vue 默认插值即此 |
| HTML 属性 | HTML Attribute 编码 + 必须加引号 | 引号决定上下文边界;onClick 等可执行属性不要用不可信值 |
| JavaScript 字符串 | \xHH 全量编码 | </script> 也逃不出字符串字面量 |
| JSON 响应 | 保持 Content-Type: application/json | 防止被当作 HTML 解析 |
| URL 参数 | URL 编码(只编码参数值) | 放进 href 时是“先 URL 编码再属性编码”双层 |
| CSS 值 | CSS hex 编码,且只放属性值 | 变量进选择器/表达式属于危险上下文 |
⚠️ 危险上下文(dangerous contexts):即使做了编码也不可靠——HTML 注释内、JS 字符串外、URL 的 scheme 部分(
javascript:通过任何 HTML 编码检查)、style表达式。这些地方不要输出数据,改为白名单校验。javascript:URL 的经典坑:对href做 entity 编码挡不住javascript:,必须做 scheme allowlist(只允许http/https/mailto/#)。
框架的默认防线来自“输出即编码”:React 的 JSX 插值、Vue 的 {{ }} 都自动做 HTML entity 编码;dangerouslySetInnerHTML / v-html / innerHTML 是显式关掉防线,用的就是净化器(DOMPurify)的位置。
8.1.3 DOMPurify 的机制与边界
DOMPurify 的做法是解析再序列化:把脏 HTML 交给浏览器 parser 建 DOM 树,按 allowlist 删除危险节点/属性(script、事件属性、javascript: URL),再重新序列化为 HTML 字符串。这比正则黑名单可靠得多(正则处理不了浏览器容错解析的畸形嵌套),也解释了为什么“黑名单过滤 <script>”必然被绕过——编码变体、<img src=x onerror=...>、MXSS(解析器差异:服务端净化视图与浏览器实际解析不一致)。
注意边界:DOMPurify 净化的是 HTML 结构,不校验 CSS 注入进 style、不防止把净化结果放进 <script> 内部——净化后的 HTML 仍然只能在 HTML 上下文使用。
8.1.4 Trusted Types:DOM XSS 的运行时强制
Trusted Types(W3C 规范,配合 CSP 指令启用)把“约定不用危险 sink”变成“浏览器强制拦截”:
ts
const policy = trustedTypes.createPolicy("app-html", {
createHTML: (input) => DOMPurify.sanitize(input),
});
el.innerHTML = policy.createHTML(dirty); // 通过:TrustedHTML
el.innerHTML = dirty; // TypeError:sink 只接受 TrustedHTML启用方式:Content-Security-Policy: require-trusted-types-for 'script'。任何字符串直接进入 XSS sink(innerHTML、eval、insertAdjacentHTML…)都会抛 TypeError 并产生违规报告——它不防“数据被当代码”的 HTML/反射型路径(那是 CSP + 编码的职责),专防 DOM 型 sink 滥用,两者互补。
8.2 CSRF
CSRF:攻击者诱导用户浏览器携带身份凭证发起请求。
防护:
- SameSite Cookie。
- CSRF token。
- 校验 Origin / Referer。
- 关键操作二次确认。
- 使用非简单请求配合服务端校验。
8.2.1 攻击原理:为什么浏览器“自愿”带上凭证
CSRF 利用的是 Cookie 的作用域模型:cookie 按 site(eTLD+1,可被 SameSite/Domain 调整)自动附加,而不是按 origin。只要请求最终打到目标站,浏览器就会把 HttpOnly 会话 cookie 附上——而 HttpOnly 恰恰让前端无法参与防御(读不到 token 就没法放进 header)。攻击者不需要读懂响应,只需要让写操作发生:
html
<!-- evil.com 上的页面,用户此刻正登录着 bank.com -->
<form action="https://bank.com/transfer" method="POST" id="f">
<input name="to" value="attacker" /><input name="amount" value="1000" />
</form>
<script>document.getElementById("f").submit();</script>
<!-- 或 <img src="https://bank/api/delete?id=1">、自动触发的 fetch -->关键判据:是否依赖 cookie 自动携带鉴权凭证。如果 API 只接受 Authorization: Bearer(token 存在内存/localStorage,由 JS 主动附加),跨站脚本无法让浏览器自动带上它,传统 CSRF 就不成立;如果同时接受 cookie,则 cookie 路径仍是攻击面。
8.2.2 SameSite 的机制与失效边界
| 取值 | 行为 | 注意点 |
|---|---|---|
Strict | 任何跨站请求都不带 cookie | 从外部链接/邮件跳回会被登出感,OAuth 回调会失败 |
Lax(浏览器默认) | 跨站顶层 GET 导航带 cookie,跨站 POST/XHR/fetch 不带 | 挡住绝大多数 CSRF,但 GET 改状态的接口仍然中招 |
None | 一律携带 | 必须同时 Secure,否则浏览器拒收 |
⚠️ SameSite 是纵深防御层,不是完整方案:同站跨子域(
a.corp.com→b.corp.com)被视为 same-site,SameSite完全不设防;Domain=.corp.com这类“域级 cookie”叠加一个可被 CNAME 接管子域,等于把会话交给整个子域。工程底线:状态变更只用 POST/PUT/PATCH/DELETE +SameSite=Lax+ token 校验,不要靠 SameSite 单打。
8.2.3 Token 方案的三种实现与其差异
| 方案 | 机制 | 服务端状态 | 弱点 |
|---|---|---|---|
| Synchronizer Token(有状态首选) | 会话内生成随机 token,表单隐藏域带回,服务端比对会话 | 需要存 session | 集群要共享会话;token 不能出现在 URL/日志(Referer 泄漏) |
| Double-Submit Cookie | 同名值同时放 cookie 和请求头/字段,服务端比较两者是否一致 | 无状态 | 裸实现不安全:若攻击者能在目标域写入 cookie(子域 cookie tossing、DNS 重绑定、明文 HTTP),可伪造一致 → OWASP 现推荐 Signed Double-Submit(HMAC 绑定会话 ID,且比较用常数时间防时序侧信道) |
| 自定义请求头 / 非简单请求 | 强制 X-CSRF-Token 或 Content-Type: application/json | 无状态 | 安全性来自同源策略:跨域表单发不了自定义头、application/json 会触发 CORS 预检;OWASP 因此建议 token 尽量放自定义头而非表单隐藏域 |
Fetch Metadata 是现代补充信号:服务端检查 Sec-Fetch-Site(cross-site 的状态变更请求直接拒),辅以 Origin/Referer 校验。Origin 在所有非 GET 的现代请求里都存在,比 Referer(会被 Referrer-Policy 剥掉、不带 scheme/path)可靠。
💡 前端职责清单:所有状态变更接口走封装好的 request client(自动附加 CSRF 头、统一 application/json),并在测试清单里加一条“跨站表单 POST 该接口应返回 403”。
8.3 CSP
CSP:Content Security Policy,用于限制页面可以加载和执行哪些资源。
示例:
http
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'CSP 可以降低 XSS 成功后的破坏范围,但不能替代输入处理。
8.3.1 工作机制:策略匹配与指令继承
响应头里的 policy 由“指令(directive)+ 源表达式(source expression)”组成。求值规则:
default-src是兜底;某类专用指令(script-src、img-src…)一旦声明就不再回退到default-src(少数例外:object-src、base-uri、form-action、frame-ancestors永不继承default-src,必须显式写)。- 多个 policy(如同时有 Header 与
<meta>、或增量策略)取交集,最严格者胜出。 - 违规默认是“拦截 + 控制台报错 + 上报”,不是页面报错——所以 CSP 配错表现为功能静默失效,必须靠 report 观察。
8.3.2 strict CSP:nonce、hash 与 strict-dynamic
白名单域名式 CSP(allowlist)防护弱:只要放行了某个会回显用户输入的第三方域(或存在开放重定向),XSS 依旧可执行。MDN 推荐 strict CSP,改用 nonce/hash:
http
Content-Security-Policy:
script-src 'nonce-R4nd0mValue' 'strict-dynamic';
object-src 'none';
base-uri 'none';nonce-:每次 HTTP 响应生成一个密码学随机、只使用一次的值,同时写进 header 和<script nonce=...>。注入的脚本猜不到 nonce,因此不执行。'strict-dynamic':把 nonce 赋予的信任传播给该脚本动态创建的脚本(createElement('script')加载的 chunk),同时忽略域名白名单和'unsafe-inline'——否则打包器注入的异步 chunk 会被拦。'unsafe-inline'与 nonce/hash 同时存在时,支持 CSP2+ 的浏览器忽略unsafe-inline,因此过渡期可以两者并写实现向后兼容(CSP1 只认 inline,CSP3 只认 nonce)。- 无 nonce 的纯静态站点用
'sha256'-<hash>逐个放行内联脚本。
配套指令:object-src 'none'(禁 Flash/插件)、base-uri 'none'(防 <base> 劫持相对路径)、form-action 'self'(防表单被外送到钓鱼域)、frame-ancestors 'none'(防点击劫持,比 X-Frame-Options 表达力强)、upgrade-insecure-requests。
8.3.3 灰度上线:Report-Only 与 Reporting API
直接上 enforce 会打断业务。标准路径:
http
Reporting-Endpoints: csp-endpoint="https://example.com/csp-reports"
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'nonce-...'; report-to csp-endpointContent-Security-Policy-Report-Only只上报不拦截;report-to需先在Reporting-Endpoints声明端点名(report-uri已废弃,过渡期两者并写以覆盖不支持report-to的浏览器)。- 页面内可用
new ReportingObserver(..., { types: ["csp-violation"] })自采违规做本地看板。 - 观察期重点清理:内联脚本、
eval(unsafe-eval是明确弱点,尽量消除)、第三方 SDK 的域、被 CSP 打断的 sourcemap 加载。
⚠️ CSP 不是万能:
meta标签只能配置部分指令(frame-ancestors、report-to等需响应头),SPA 里 CSP 由服务端/边缘统一注入;同时 CSP 挡不住 CSS 数据外带、点击劫持以外的 UI 攻击、以及同源下已执行的脚本继续作恶。
8.4 Token 存储
常见方案:
- HttpOnly Cookie。
- 内存。
- LocalStorage。
- SessionStorage。
LocalStorage 易受 XSS 影响。HttpOnly Cookie 可防止 JS 读取,但需要处理 CSRF。没有绝对完美方案,要结合业务风险选择。
8.4.1 各存储位置的威胁模型对比
| 位置 | XSS 后可被读走? | 有 CSRF 风险? | 关闭标签后 | 适用 |
|---|---|---|---|---|
HttpOnly; Secure; SameSite=Lax Cookie | 否(JS 读不到) | 是,必须配 CSRF 防护 | 按 Expires/Max-Age | 会话主凭证(推荐) |
| 内存变量 | 短暂(刷新即失) | 否(不自动携带) | 丢失 | 短时 access token,配 silent refresh |
| LocalStorage | 是 | 否 | 持久 | 低敏感状态;不建议放长期凭证 |
| SessionStorage | 是 | 否 | 标签页关闭即失 | 同上,作用域更小 |
判据不是“哪种更安全”,而是两种风险的组合你更能接受哪个:LocalStorage 把风险压在 XSS(token 被复制后离线可用),HttpOnly Cookie 把风险压在 CSRF(浏览器代为发送)。前者靠 CSP + 输出编码 + Trusted Types 收口,后者靠 token/头校验 + SameSite 收口,两侧都要做。
8.4.2 OAuth 2.0 浏览器应用的当前共识
- 公共客户端(SPA、PWA)不要保存 client secret——它无法保密,浏览器里任何字符串都能被扒出。改用 Authorization Code + PKCE(
code_verifier/code_challenge),拦截授权码替换攻击。 redirect_uri用精确匹配,不用前缀匹配。state参数用于绑定用户会话、防授权响应注入(不是防 CSRF 的全部,别只靠它)。- RFC 9700(OAuth 2.0 Security BCP)与
draft-ietf-oauth-browser-based-apps明确:把 access/refresh token 暴露给浏览器 JS 是不推荐的默认做法,优先 Backend For Frontend(BFF)——由后端持有 token,浏览器只拿一个HttpOnly会话 cookie;纯前端直连 IdP 时才退而求其次用“内存 access token + HttpOnly refresh cookie”。 - access token 要短寿命(分钟级)+ 轮换 refresh token,把被窃后的可用窗口压小。
💡 架构含义:一旦选 BFF,前端的“权限动态路由”数据(用户信息、权限码、菜单)也应从 BFF 的 /api/me 取,而不是让前端持有并解析 JWT——注意 JWT 一旦签发就无法撤销,前端拿它当权限真源会带来“权限已回收但 token 未过期”的窗口问题。
8.5 OAuth / OIDC 基础
前端架构师至少要理解:
- Authorization Code Flow。
- PKCE。
- access token。
- refresh token。
- redirect URI。
- token 过期和刷新。
不要在浏览器里保存 client secret。
9. 依赖与供应链安全
9.1 风险
- 恶意 npm 包。
- 依赖漏洞。
- postinstall 脚本。
- 维护者账号被盗。
- 依赖劫持。
- 私有 registry 泄漏。
机制层面最重要的两条:postinstall 脚本在代码运行之前就以本机权限执行(供应链 RCE 的主通道);依赖混淆攻击利用“私有包名在公共 registry 不存在”的解析缝隙。防御细节(pnpm 脚本白名单、scope registry 固定、provenance 溯源)见《前端工程化与构建发布系统学习文档》13.4/13.5。
9.2 治理
建议:
- 提交 lockfile。
- 定期运行依赖审计。
- 使用 Dependabot / Renovate。
- 对高危漏洞快速响应。
- 对大型依赖做许可证检查。
- 限制 CI 中不必要的脚本权限。
- 对私有包使用 scoped registry。
9.3 版本升级策略
不要一键升级全部依赖后直接上线。
应:
- 看 changelog。
- 分组升级。
- 跑测试。
- 看构建产物。
- 灰度发布。
- 保留回滚路径。
10. 可访问性体系
10.1 可访问性是什么
可访问性让不同能力、不同设备、不同场景下的用户都能使用产品。
包括:
- 视觉障碍。
- 听觉障碍。
- 运动障碍。
- 认知障碍。
- 临时受限场景。
- 键盘用户。
- 屏幕阅读器用户。
10.2 基础要求
必须掌握:
- 使用语义化 HTML。
- 表单控件有 label。
- 图片有 alt。
- 交互元素可键盘操作。
- 焦点可见。
- 弹窗管理焦点。
- 颜色对比度足够。
- 不只靠颜色表达状态。
- ARIA 不滥用。
10.2.1 WCAG 标准体系:POUR、13 条指南、一致性级别
WCAG(Web Content Accessibility Guidelines)由 W3C/WAI 维护,是当前国际通行的 Web 无障碍标准;现行稳定版 WCAG 2.2(2023-10 成为 W3C Recommendation,2025-10 被 ISO/IEC 采纳为 ISO/IEC 40500)。组织结构是三层:
text
4 条原则(POUR) -> 13 条指南(Guidelines) -> 可测试的成功标准(Success Criteria,2.2 共 86 条)| 原则 | 含义 | 前端典型落点 |
|---|---|---|
| Perceivable 可感知 | 信息能被用户至少一种感官接收 | alt 文本、字幕、对比度、200% 缩放、reflow |
| Operable 可操作 | 界面功能可被操作(不依赖单一输入方式) | 键盘可达、无焦点陷阱、足够操作时限、拖拽有替代 |
| Understandable 可理解 | 信息与操作可被理解 | 语言标注 lang、可预测导航、错误识别与提示、label |
| Robust 健壮 | 兼容当前与未来的辅助技术 | 有效 ARIA、无重复 id、状态可被 AT 读取 |
一致性级别:A(底线,2.2 有 32 条)、AA(A+AA 共约 56 条,法规与行业的事实基准)、AAA(增强)。⚠️ 注意欧盟《European Accessibility Act》、美国 ADA 判例、Section 508 引用的都是 WCAG 2.x Level AA——架构师定验收口径时直接写“WCAG 2.2 AA”,不要写“尽量无障碍”。
WCAG 2.2 新增的 9 条标准中与前端工程最相关的:2.4.11 Focus Not Obscured (Minimum)(sticky header/cookie 横幅盖住焦点元素即违规,中后台高频)、2.5.7 Dragging Movements(拖拽排序必须提供单指针替代操作)、2.5.8 Target Size (Minimum)(可点击目标 ≥ 24×24 CSS 像素,密集表格工具栏易违规)、3.3.7 Redundant Entry(同一流程内不要让用户重复输入已有信息)、3.2.6 Consistent Help。
10.2.2 对比度的量化规则
颜色对比不是主观题(WCAG 1.4.3 / 1.4.11):
| 标准 | 要求 | 说明 |
|---|---|---|
| 1.4.3 Contrast (Minimum, AA) | 正文 ≥ 4.5:1;大字 ≥ 3:1 | 大字 = ≥18pt(约 24px) 或 ≥14pt bold(约 18.66px bold) |
| 1.4.6 Contrast (Enhanced, AAA) | 正文 ≥ 7:1;大字 ≥ 4.5:1 | 一般不作为交付线 |
| 1.4.11 Non-text Contrast (AA) | UI 组件与图形 ≥ 3:1 | 最常被漏:输入框边框、focus 指示器、开关状态、图标含义、图表线条 |
比值为 (L1 + 0.05) / (L2 + 0.05)(相对亮度按 sRGB 线性化计算),纯黑白最大 21:1。4.5:1 的阈值来自约 20/40 视力的对比敏感度损失补偿,不是拍脑袋。设计 token 阶段就应把色板两两过一遍对比度检查(axe、Storybook a11y 可自动化),比逐页返工便宜得多。
💡 “不只靠颜色”(1.4.1 Use of Color):错误态红框要同时有文字/图标,图表不能只靠色相区分系列(辅以形状、图案、直接标注)。
10.3 ARIA 原则
优先使用原生元素。
例如按钮:
html
<button>Save</button>优于:
html
<div role="button" tabindex="0">Save</div>ARIA 可以补充语义,但不能让错误交互自动变正确。
10.4 键盘导航
检查:
- Tab 顺序是否合理。
- 焦点是否可见。
- 弹窗是否 trap focus。
- ESC 是否可关闭弹窗。
- 下拉菜单是否支持方向键。
- 表单错误是否能被读到。
10.5 团队落地
建议:
- 组件库内置可访问性。
- Storybook 加 a11y 检查。
- 关键流程人工键盘测试。
- 设计规范中包含颜色对比。
- code review 检查语义和 label。
10.6 内部机制:可访问性树与动态内容
浏览器并不把 DOM 直接给屏幕阅读器,而是维护一棵可访问性树(accessibility tree):每个节点带 role、name、state(expanded、checked、disabled),来自语义标签 + ARIA 属性的合并计算。SPA 的 a11y 难点都源于这棵树与 DOM 更新节奏的错位:
- 路由切换:焦点仍停在旧位置,读屏用户不知道“页面变了”。标准做法是切换后把焦点移到新页面主标题(
tabindex="-1"+focus()),配合<h1>变化;页面<title>更新是最低信号。 - 异步状态(toast、校验结果、局部加载完成):DOM 变了但读屏不播报。
aria-live="polite"(校验提示、toast)或assertive(严重错误)标注容器,且容器初始就要存在——实时区域被整体创建时部分读屏不监听;role="status"等价 polite。 - 弹窗:打开时焦点移入并 trap(Tab 循环在内部),关闭时焦点必须归还触发元素;背景内容用
aria-modal="true"或inert对 AT 隐藏。 - ARIA 的第一公理(W3C Using ARIA):能用原生语义就不用 ARIA——原生
<button>自动带来 role、可聚焦、Enter/Space 激活;role="button"只给了“名字”,键盘行为要自己实现。ARIA 属性写错比不写更糟(例如给<div onclick>加role="button"却不处理键盘)。 - 屏幕阅读器差异:NVDA/JAWS(Windows)、VoiceOver(macOS/iOS)、TalkBack(Android)朗读策略不同,动态内容测试至少要覆盖一种桌面 + 一种移动端组合。
11. 可观测性体系
11.1 可观测性不是埋点
埋点通常回答业务问题:
- 用户点击了什么。
- 转化率如何。
- 漏斗在哪里流失。
可观测性回答系统问题:
- 哪里报错。
- 为什么白屏。
- 哪个版本变慢。
- 哪个接口超时。
- 哪个地区用户受影响。
- 前后端链路在哪个环节失败。
11.2 前端可观测性内容
包括:
- JS 错误。
- Promise rejection。
- 资源加载错误。
- 接口错误和耗时。
- Web Vitals。
- 白屏。
- 长任务。
- 用户行为轨迹。
- release 版本。
- source map。
- trace id。
11.3 数据维度
建议记录:
- app version。
- release id。
- route。
- user agent。
- device。
- network。
- region。
- user id hash。
- tenant id。
- experiment group。
注意隐私合规,不要采集敏感明文。
11.4 三大支柱的关联机制(Correlation)
Logs / Metrics / Traces 不是三个孤立系统,价值在于互相跳转。OpenTelemetry 的答案是共享标识与上下文传播:
text
Metric 曲线异常(错误率突增)
-> Exemplar 携带 trace_id -> 找到一条具体 Trace
-> Trace 的 span 关联日志(log record 里注入 trace_id/span_id)
-> 前端 span 与后端 span 通过 W3C traceparent 串成一条链路关键机制拆解:
| 关联 | 载体 | 说明 |
|---|---|---|
| Trace ↔ Log | span 上下文注入日志字段 | SDK 可自动把 trace_id/span_id 写进日志,日志平台反查 |
| Metric ↔ Trace | Exemplar | 直方图样本点附带 trace id,从“P95 变慢”直接跳到慢请求 |
| 前端 ↔ 后端 | traceparent header | W3C Trace Context 格式:00-{32位 trace-id}-{16位 span-id}-{2位 flags} |
| 会话内各事件 | session.id / release | RUM SDK 把用户会话、版本作为公共 attribute 打到所有信号上 |
⚠️ traceparent 上线前先确认:CORS 跨域请求要把该头加进 Access-Control-Allow-Headers/请求白名单,网关要透传;同时评估暴露风险——trace id 本身不含敏感信息,但不要把内部拓扑当公共 API 承诺。采样在头部(head-based,前端 SDK 决定)与尾部(tail-based,collector 决定)之间选:前端常用头部采样控量,但错误链路应强制采样(sampled flag 例外策略),否则“低频高害”的白屏链路恰好被采掉。
💡 排障动线设计:告警来自 Metrics(错误率/延迟阈值),定位靠 Trace(哪个环节慢),定罪靠 Log/Event(具体参数与堆栈)。三者若没有共享 id,等于三个各看各的仪表盘——建可观测性时先定 id 规范(release、session、trace 三者贯穿),再选工具。
12. 错误监控与 Source Map 管理
12.1 全局错误捕获
ts
window.addEventListener("error", (event) => {
reportError({
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
});
});
window.addEventListener("unhandledrejection", (event) => {
reportError({
message: String(event.reason),
type: "unhandledrejection",
});
});实际项目建议使用成熟平台或 SDK。
12.1.1 捕获机制的底层细节
三个事件通道各有盲区,缺一不可:
| 通道 | 捕获内容 | 关键细节 |
|---|---|---|
window.addEventListener("error", fn, true) | JS 运行时错误 + 资源加载错误(script/img/css 404) | 资源加载错误不冒泡只捕获,必须 capture: true;运行时错误在冒泡/捕获阶段都能收到 |
unhandledrejection | 未处理的 Promise rejection | event.reason 可能是任意值,不是 Error 时没有堆栈,要包一层归一化 |
框架错误边界(React Error Boundary / Vue app.config.errorHandler) | 渲染生命周期内的错误 | 边界抓不到事件回调、async、SSR 错误;错误边界负责“恢复 UI”,上报仍走全局 |
两个高频坑:
Script error./ “No detailed error message available”:跨域脚本(CDN 上的 JS)出错时,浏览器出于隐私只给这一句话。解法是脚本资源返回Access-Control-Allow-Origin且<script>标签带crossorigin="anonymous"——缺任何一侧都拿不到堆栈。上 CDN 的团队必须把这两个条件写进发布检查。- 压缩后堆栈无意义:捕获到的行列号属于产物,平台侧靠 sourcemap 还原;这就是 12.3 的 release 匹配纪律存在的原因——采集端和符号化端以 release/debug id 对齐。
12.1.2 平台侧机制(以 Sentry 模型为例)
成熟的错误平台在你自己的 error 监听之上多做了几件事:面包屑(breadcrumbs,记录错误前的操作/请求/路由轨迹)、错误指纹聚合(fingerprinting,把同一根因的堆栈折叠成一个 issue,避免告警风暴)、Release Health(按 release 维度算 crash-free sessions/users 率,灰度时直接对比新旧版本曲线)、去重与采样(beforeSend 钩子控制数据量)。接入平台时至少配置三件事:release 与构建产物一致、environment 区分环境、source map 在错误发生之前上传(Sentry 官方排障文档明确要求 artifacts 先于错误上传,否则堆栈无法符号化)。
12.2 React Error Boundary
tsx
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
reportError({ error, info });
}
render() {
if (this.state.hasError) {
return <Fallback />;
}
return this.props.children;
}
}错误边界能防止局部组件错误导致整个页面崩溃。
12.3 Source Map
生产问题定位需要 sourcemap,但不建议公开暴露。
建议:
- 构建时生成 sourcemap。
- 发布时上传到监控平台。
- CDN 不公开 sourcemap。
- release id 与 sourcemap 一致。
- 回滚时保留历史 sourcemap。
12.4 错误分级
可以按影响分级:
- P0:大面积白屏、核心流程不可用。
- P1:关键功能异常。
- P2:局部页面错误。
- P3:非关键错误或兼容问题。
告警要关注影响面和趋势,不要每个零散错误都打扰团队。
13. 性能监控与 Web Vitals
13.1 Web Vitals
核心指标:
- LCP:最大内容绘制,良好阈值 <= 2.5s。
- INP:交互到下一次绘制,良好阈值 <= 200ms。
- CLS:累计布局偏移,良好阈值 <= 0.1。
13.1.1 指标的计算机制与判定时序
| 指标 | 计时起点/终点 | 机制要点 |
|---|---|---|
| LCP | 导航开始 → 视口内最大内容元素渲染完成 | 候选元素:<img>、<video> 海报帧、CSS url() 背景图、含文本节点的块级元素、SVG 内的 <image> 等;字体阻塞会推迟文本 LCP;用户首次交互或离开页面后停止记录候选,最终值由 web-vitals 在 visibilitychange 时上报 |
| INP | 用户交互事件 → 下一次呈现内容绘制 | 统计页面上所有 click/tap/键盘交互的延迟;一般报告最差交互延迟,交互极多的热门页面按约 p98 取值并忽略极端离群。延迟三段:输入处理(事件回调)→ 渲染处理 → 下一帧呈现 |
| CLS | 视口内元素非预期位移的累计分 | 单条记录 = impact fraction(受影响区域占视口比例)× distance fraction(最大位移 / 视口最大维度);用户输入后 500ms 内的偏移视为预期、不计入(每个交互只豁免前 20 次) |
三个必须理解的口径:
- 实验室数据 ≠ 现场数据:Lighthouse 是 lab(受控环境、一次性),CrUX/RUM 是 field(真实用户分布)。发布决策看 field P75,优化验证用 lab。
- P75 是判定线:Google 的“通过 Core Web Vitals”= 该指标 75% 的访问达到 good。这就是 13.3 强调看 P75 而不是平均值的依据——长尾设备/网络决定了第 75 百分位。
- 上报时机:
web-vitals库在页面生命终期监听visibilitychange,必须用sendBeacon或fetch(keepalive: true)传输;普通 XHR 在页面卸载时会被取消丢数据。INP 的 value 在会话中持续更新,采样上报时按“最后一次已知值”归档。
⚠️ LCP 常见误因:首屏大图未预加载/未设宽高、字体
font-display: swap造成的两次文本绘制、CDN 响应慢(TTFB 是 LCP 的前置成分)。优化 LCP 先拆 TTFB / 资源加载 / 渲染延迟三段,再决定动缓存还是动图片。
详细学习:前端性能优化系统学习文档.md。
13.2 上报示例
ts
import { onCLS, onINP, onLCP } from "web-vitals";
function report(metric) {
navigator.sendBeacon("/rum", JSON.stringify(metric));
}
onCLS(report);
onINP(report);
onLCP(report);13.3 统计方式
建议看:
- P75。
- 页面维度。
- 设备维度。
- 地区维度。
- 网络维度。
- release 维度。
不要只看平均值。
13.4 性能告警
适合告警:
- P75 LCP 连续超过阈值。
- 新版本 INP 明显恶化。
- 某页面 CLS 突增。
- 某地区 TTFB 变高。
- 第三方脚本耗时异常。
14. 日志、指标与链路追踪
14.1 Logs
前端日志包括:
- 用户操作日志。
- 错误日志。
- 接口错误。
- 状态变化。
- 关键业务事件。
日志不要记录敏感数据。
14.2 Metrics
指标包括:
- 错误率。
- 接口成功率。
- 接口耗时。
- 页面加载耗时。
- 资源错误率。
- JS heap。
- 长任务数量。
14.3 Traces
链路追踪用于串联前端和后端请求。
关键是 trace id:
text
frontend click -> API request -> gateway -> service A -> database如果前端请求带上 trace context,后端也接入 tracing,就能定位慢在哪一层。
14.4 OpenTelemetry
OpenTelemetry 是开放标准,用于采集 traces、metrics、logs。前端可以接入浏览器 instrumentation,把页面加载、XHR/fetch 等链路纳入统一追踪。
架构师要关注:
- 采样率。
- 数据量。
- 隐私。
- 性能开销。
- 与后端 trace 串联。
15. 线上问题排查与事故复盘
15.1 白屏排查
检查:
- JS 是否 404。
- CSS 是否 404。
- base 路径是否错误。
- 浏览器是否不兼容。
- 环境变量是否错误。
- 接口是否阻塞初始化。
- JS runtime error。
- CDN 缓存是否不一致。
15.2 点击无响应
检查:
- 是否有 JS 错误。
- 主线程是否长任务。
- 按钮是否被透明层遮挡。
- 权限状态是否异常。
- 请求是否 pending。
- 状态更新是否丢失。
15.3 接口异常
检查:
- 状态码。
- 错误码。
- trace id。
- 用户租户。
- 版本。
- 网络。
- 是否 token 过期。
- 是否 CORS。
15.4 事故复盘
复盘应包含:
- 时间线。
- 影响范围。
- 根因。
- 发现方式。
- 处理过程。
- 为什么没提前发现。
- 如何防止再次发生。
- 责任改进项。
复盘不是找人背锅,而是修系统。
16. 质量门禁与团队治理
16.1 PR 阶段
建议:
- lint。
- typecheck。
- unit test。
- affected build。
- 关键组件测试。
16.2 合并前
建议:
- E2E smoke。
- bundle size。
- 视觉回归。
- 安全扫描。
16.3 发布前
建议:
- 生产构建。
- sourcemap 上传。
- release id 注入。
- smoke test。
- 灰度开关确认。
- 回滚版本确认。
16.4 团队治理
建议:
- 质量指标可视化。
- 线上错误每周回顾。
- 高风险模块增加测试。
- 事故复盘进入技术债清单。
- 规范尽量自动化。
17. 学习路线与练习
17.1 基础阶段
目标:掌握测试和错误处理基础。
练习:
- 用 Vitest 测试工具函数。
- 用 Testing Library 测试表单组件。
- 添加全局 error 和 unhandledrejection 捕获。
- 手动制造一个错误并上报。
17.2 进阶阶段
目标:覆盖关键业务链路。
练习:
- 用 Playwright 测试登录流程。
- 用 MSW mock 接口。
- 为组件库加 Storybook。
- 为关键组件加 a11y 检查。
- 在 CI 中运行测试。
17.3 高级阶段
目标:建立生产稳定性能力。
练习:
- 接入 Sentry 或同类监控。
- 上传 sourcemap。
- 上报 LCP、INP、CLS。
- 建立错误率和性能看板。
- 设置一个合理告警。
17.4 架构师阶段
目标:设计质量治理体系。
练习:
- 设计测试分层策略。
- 设计安全检查清单。
- 设计可访问性落地规范。
- 设计线上事故排查流程。
- 把质量门禁写进 CI/CD。
18. 官方参考资料
- Playwright:https://playwright.dev/docs/intro
- Vitest:https://vitest.dev/guide/
- Testing Library:https://testing-library.com/docs/guiding-principles
- Storybook:https://storybook.js.org/docs
- MSW:https://mswjs.io/docs/
- OWASP Cheat Sheet Series:https://cheatsheetseries.owasp.org/
- OWASP XSS Prevention:https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
- OWASP CSRF Prevention:https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
- MDN Content Security Policy:https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
- MDN CSP 实施指南:https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/CSP
- W3C Trusted Types:https://www.w3.org/TR/trusted-types/
- web.dev Trusted Types 实践:https://web.dev/articles/trusted-types
- W3C WCAG:https://www.w3.org/WAI/standards-guidelines/wcag/
- MDN Accessibility:https://developer.mozilla.org/en-US/docs/Web/Accessibility
- web.dev Web Vitals:https://web.dev/articles/vitals
- OpenTelemetry JavaScript:https://opentelemetry.io/docs/languages/js/
- Sentry JavaScript:https://docs.sentry.io/platforms/javascript/
- Chrome DevTools Performance:https://developer.chrome.com/docs/devtools/performance