Skip to content

前端质量保障、安全、可访问性与可观测性系统学习文档 ​

适合人群:希望从“能开发功能”进阶到“能保障生产质量”的前端学习者,尤其适合准备前端架构师、技术负责人、高级前端岗位。

学习目标:系统掌握测试体系、安全防护、可访问性、错误监控、性能监控、日志指标链路追踪、线上问题治理和质量门禁。

核对时间:2026-06-30。本文参考 Playwright、Vitest、Testing Library、Storybook、OWASP、W3C WCAG、MDN、OpenTelemetry、Sentry、web.dev Web Vitals 等官方资料整理。


目录 ​

  1. 为什么架构师必须关注质量与稳定性
  2. 前端质量保障体系总览
  3. 测试分层
  4. 单元测试与组件测试
  5. 集成测试与 E2E 测试
  6. 视觉回归、可访问性测试与契约测试
  7. 前端安全基础
  8. XSS、CSRF、CSP 与鉴权安全
  9. 依赖与供应链安全
  10. 可访问性体系
  11. 可观测性体系
  12. 错误监控与 Source Map 管理
  13. 性能监控与 Web Vitals
  14. 日志、指标与链路追踪
  15. 线上问题排查与事故复盘
  16. 质量门禁与团队治理
  17. 学习路线与练习
  18. 官方参考资料

1. 为什么架构师必须关注质量与稳定性 ​

前端架构师的职责不只是选框架、搭目录、配构建。真正上线后,用户遇到的是:

  • 页面白屏。
  • 点击无响应。
  • 数据显示错误。
  • 按钮没有权限却能点。
  • 弱网加载失败。
  • 屏幕阅读器无法使用。
  • 新版本发布后大量报错。
  • 线上问题没人能快速定位。

这些都属于前端质量和生产稳定性问题。

架构师要建立体系,让问题尽量在上线前被拦截,上线后能被发现、定位、降级和复盘。


2. 前端质量保障体系总览 ​

完整质量体系包括:

层级目标典型工具
静态检查提前发现代码问题ESLint、TypeScript、Stylelint
格式化保持风格一致Prettier
单元测试验证函数和小模块Vitest、Jest
组件测试验证组件行为Testing Library、Storybook
集成测试验证模块协作Vitest、MSW
E2E 测试验证真实用户链路Playwright、Cypress
视觉回归验证 UI 未意外变化Storybook、Chromatic
可访问性测试验证基础 a11yaxe、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 编码挡不住 jav&#x61;script:,必须做 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-endpoint
  • Content-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 版本升级策略 ​

不要一键升级全部依赖后直接上线。

应:

  1. 看 changelog。
  2. 分组升级。
  3. 跑测试。
  4. 看构建产物。
  5. 灰度发布。
  6. 保留回滚路径。

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 ↔ Logspan 上下文注入日志字段SDK 可自动把 trace_id/span_id 写进日志,日志平台反查
Metric ↔ TraceExemplar直方图样本点附带 trace id,从“P95 变慢”直接跳到慢请求
前端 ↔ 后端traceparent headerW3C Trace Context 格式:00-{32位 trace-id}-{16位 span-id}-{2位 flags}
会话内各事件session.id / releaseRUM 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 rejectionevent.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 基础阶段 ​

目标:掌握测试和错误处理基础。

练习:

  1. 用 Vitest 测试工具函数。
  2. 用 Testing Library 测试表单组件。
  3. 添加全局 error 和 unhandledrejection 捕获。
  4. 手动制造一个错误并上报。

17.2 进阶阶段 ​

目标:覆盖关键业务链路。

练习:

  1. 用 Playwright 测试登录流程。
  2. 用 MSW mock 接口。
  3. 为组件库加 Storybook。
  4. 为关键组件加 a11y 检查。
  5. 在 CI 中运行测试。

17.3 高级阶段 ​

目标:建立生产稳定性能力。

练习:

  1. 接入 Sentry 或同类监控。
  2. 上传 sourcemap。
  3. 上报 LCP、INP、CLS。
  4. 建立错误率和性能看板。
  5. 设置一个合理告警。

17.4 架构师阶段 ​

目标:设计质量治理体系。

练习:

  1. 设计测试分层策略。
  2. 设计安全检查清单。
  3. 设计可访问性落地规范。
  4. 设计线上事故排查流程。
  5. 把质量门禁写进 CI/CD。

18. 官方参考资料 ​

基于 VitePress 构建的个人学习文档知识库