本册涵盖:B1 前端高级专题 · B2 事故 10 例 · B3 浏览器性能诊断 · B4 软实力
B1 - 前端高级专题
B1.1 Web Worker:把昂贵计算移出主线程
线索:用户点了"导出 1 万行 CSV",前端卡 5 秒,INP 红到爆。
主线程占着 = UI 全卡。任何 > 50ms 的 JS 计算都该考虑放 Worker。
// workers/csv.worker.ts
self.addEventListener("message", (e: MessageEvent<{ rows: Row[] }>) => {
const csv = e.data.rows.map(r => Object.values(r).join(",")).join("\n");
self.postMessage({ csv });
});
// 使用
"use client";
import { useState } from "react";
export function ExportButton({ rows }: { rows: Row[] }) {
const [busy, setBusy] = useState(false);
async function exportCsv() {
setBusy(true);
const worker = new Worker(new URL("../workers/csv.worker.ts", import.meta.url));
worker.postMessage({ rows });
worker.onmessage = (e) => {
const blob = new Blob([e.data.csv], { type: "text/csv" });
saveAs(blob, "export.csv");
worker.terminate();
setBusy(false);
};
}
return <button onClick={exportCsv} disabled={busy}>{busy ? "..." : "Export"}</button>;
}
💡
new Worker(new URL(..., import.meta.url))让 webpack/Next 自动打包 worker。比手写 worker 路径稳。
⚠️ Worker 不能用 DOM,只能传可序列化数据(用
Transferable可以零拷贝传 ArrayBuffer)。
Comlink 让 Worker 像普通对象用:
import * as Comlink from "comlink";
const api = Comlink.wrap<typeof workerApi>(new Worker(...));
const result = await api.heavyCompute(input); // 像调本地 async 一样
B1.2 复杂状态机:XState
简单状态用 useState / Zustand;5 个以上状态 + 复杂转移 → 用 XState。
import { createMachine, assign } from "xstate";
import { useMachine } from "@xstate/react";
const checkoutMachine = createMachine({
id: "checkout",
initial: "cart",
context: { items: [], paymentMethod: null, error: null },
states: {
cart: {
on: { CHECKOUT: { target: "shipping", guard: "hasItems" } },
},
shipping: {
on: {
SUBMIT: { target: "payment" },
BACK: { target: "cart" },
},
},
payment: {
on: {
PAY: { target: "processing" },
BACK: { target: "shipping" },
},
},
processing: {
invoke: {
src: "chargeService",
onDone: { target: "success" },
onError: { target: "failed", actions: assign({ error: ({ event }) => event.data }) },
},
},
success: { type: "final" },
failed: {
on: { RETRY: { target: "payment" } },
},
},
}, {
guards: { hasItems: ({ context }) => context.items.length > 0 },
});
export function Checkout() {
const [state, send] = useMachine(checkoutMachine);
// state.matches("payment") -> render payment UI
}
收益:
- 状态 + 转移 + 守卫 + 副作用集中,不再散落 useEffect
- XState Inspector 可视化整张图,PM / QA 看图都能讨论
- 不可能进入不合法状态(类型保证)
💡 画出来 = 一半的 bug 暴露 用 XState Studio 把图画出来,80% 的"忘了处理的情况"会暴露给你。先画图,再写代码。
B1.3 表格虚拟化:百万行也丝滑
pnpm add @tanstack/react-virtual
"use client";
import { useVirtualizer } from "@tanstack/react-virtual";
import { useRef } from "react";
export function VirtualList({ items }: { items: Row[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const v = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 40,
overscan: 5,
});
return (
<div ref={parentRef} className="h-[600px] overflow-auto">
<div style={{ height: v.getTotalSize(), position: "relative" }}>
{v.getVirtualItems().map((row) => (
<div
key={row.key}
style={{ position: "absolute", top: 0, left: 0, width: "100%", height: row.size, transform: `translateY(${row.start}px)` }}
>
{items[row.index].name}
</div>
))}
</div>
</div>
);
}
💡 何时需要虚拟化? DOM 节点 > 500 就开始卡。> 5000 必虚拟化。表格、长 feed、聊天记录、搜索结果。
B1.4 离线优先(PWA)
pnpm add next-pwa @serwist/next
next.config.mjs:
import withSerwist from "@serwist/next";
export default withSerwist({ swSrc: "src/sw.ts", swDest: "public/sw.js" })(nextConfig);
src/sw.ts(基本):
import { defaultCache } from "@serwist/next/worker";
import { installSerwist } from "@serwist/sw";
installSerwist({
precacheEntries: self.__SW_MANIFEST,
skipWaiting: true,
clientsClaim: true,
navigationPreload: true,
runtimeCaching: defaultCache,
});
要考虑:
- 何时降级到离线 UI?网络慢/无网时
- **离线时用户能干什么?**只读 / 暂存写操作待联网后同步
- 缓存策略:静态资源 CacheFirst,API 数据 NetworkFirst + 短 TTL
- 离线提交:Background Sync API
⚠️ PWA 是高承诺 Service Worker 一旦注册就长期驻留,写错了用户更新不到新版。开 PWA 前必须有 SW 版本管理 + skip-waiting + 通知用户刷新的方案。
B1.5 复杂动画与性能
60 FPS = 16ms 每帧预算。
性能友好的动画:
- 只动
transform和opacity(GPU 加速) - 不动
width / height / top / left(触发 layout) - 用
will-change提示浏览器(节制使用,会占显存) - 避免动画期间引起 layout shift
Framer Motion 进阶:
import { motion, useScroll, useTransform } from "framer-motion";
export function HeroParallax() {
const { scrollY } = useScroll();
const y = useTransform(scrollY, [0, 500], [0, 150]);
return <motion.div style={{ y }}>{/* ... */}</motion.div>;
}
💡 测量帧率:Chrome DevTools → Performance,看 FPS 曲线。掉到 30 以下要警惕。
B1.6 微前端(Module Federation)
何时需要?
- 多团队独立发布同一个 Web 应用
- 部分模块用不同框架(老 Vue + 新 Next)共存
- 巨型应用(几百屏)
- 不是"组件复用",那是 npm 包的事
Next 现状:官方对 Module Federation 支持不完善。可选方案:
- @module-federation/nextjs-mf
- 简单方案:iframe(隔离强但通信麻烦)
- Web Components 跨框架嵌入
⚠️ 微前端是终极武器,不是默认选择 它解决的是组织问题,不是技术问题。如果你能用 npm workspaces 解决,别上微前端。
B1.7 i18n 进阶:右到左、复数、命名空间
// next-intl 高级用法
const t = useTranslations("invoice");
t("items", { count }); // count=0/1/2+ 自动选 message
// messages/zh.json
{
"invoice": {
"items": "{count, plural, =0 {无商品} one {# 件商品} other {# 件商品}}"
}
}
RTL(阿拉伯语等):
// app/[locale]/layout.tsx
const dir = locale === "ar" ? "rtl" : "ltr";
<html lang={locale} dir={dir}>
Tailwind RTL:pe-4 而非 pr-4(logical properties)。
💡 命名空间分模块:每个 feature 一个 namespace(
auth.signIn/invoice.create),按 locale 分文件,懒加载只装该 locale 的命名空间 → bundle 小。
B1.8 表单"恶魔级"复杂度
复杂表单的痛点:
- 数百字段、嵌套层级深
- 字段之间联动(A 改 → B 重新加载选项)
- 部分字段权限不同(只读 / 隐藏)
- 大型选择器(异步加载、虚拟化)
- 草稿保存、断点续填
策略:
- 拆分 Step Form(向导)→ 状态机控制
- 每 step 一个 sub-schema,Zod
.pick()出来 - 草稿持久化:
useEffectdebounce 写 localStorage / API - 字段权限用 schema 上的
meta,渲染时按 meta 决定显示/锁定 - 联动用
useWatch局部订阅,不要form.watch()全订阅
// 每 step 校验子 schema
const stepSchema = FullFormSchema.pick({ name: true, email: true });
const step1 = stepSchema.safeParse(form.getValues());
B1.9 表格状态:复杂查询参数
URL 状态(nuqs)+ TanStack Table:
import { useReactTable, getCoreRowModel, getSortedRowModel, getFilteredRowModel } from "@tanstack/react-table";
import { useQueryStates, parseAsString, parseAsInteger, parseAsArrayOf } from "nuqs";
export function DataTable() {
const [params, setParams] = useQueryStates({
q: parseAsString.withDefault(""),
page: parseAsInteger.withDefault(1),
sort: parseAsString.withDefault(""),
cols: parseAsArrayOf(parseAsString).withDefault(["name","email"]),
});
const { data } = useQuery({
queryKey: ["users", params],
queryFn: () => api.list(params),
});
// ... table 用 data,sortingChange 同步到 params
}
💡 所有筛选 / 排序 / 列显示都进 URL → 用户可以分享、可以收藏、可以后退。
B1.10 SSE 与 WebSocket
实时通知 / 协作:
| 选型 | 适用 |
|---|---|
| SSE(Server-Sent Events) | 单向推送(通知、live feed),简单,自动重连 |
| WebSocket | 双向高频(聊天、协作编辑) |
| TanStack Query 轮询 | 频率低(每分钟一次的状态) |
SSE 在 Next 中:
// app/api/events/route.ts
export const runtime = "nodejs";
export async function GET(req: NextRequest) {
const stream = new ReadableStream({
start(controller) {
const send = (data: unknown) =>
controller.enqueue(`data: ${JSON.stringify(data)}\n\n`);
const interval = setInterval(() => send({ ts: Date.now() }), 5000);
req.signal.addEventListener("abort", () => clearInterval(interval));
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
},
});
}
客户端:
useEffect(() => {
const es = new EventSource("/api/events");
es.onmessage = (e) => setData(JSON.parse(e.data));
return () => es.close();
}, []);
⚠️ K8s Ingress 必须关
proxy-buffering(09 章已强调)否则 SSE 没用。
B2 - 前端事故 10 例
案例 1:useEffect 死循环 → 浏览器卡死
现象:用户访问列表页,Chrome 标签卡到崩溃。
根因:
useEffect(() => {
fetchData().then(setData);
}, [data]); // ❌ data 变 → effect 重跑 → setData → data 变...
修复:依赖数组改 [] 或正确的依赖([userId])。
预防:eslint-plugin-react-hooks 的 exhaustive-deps 必开。
案例 2:hydration mismatch → React 警告满屏
现象:Server 渲染 OK,client hydrate 时 React 报警:Text content does not match。
根因:
function Time() {
return <span>{new Date().toLocaleString()}</span>;
// 服务端是 UTC,客户端是本地时区 → 字符串不一致
}
修复:
- 用
<time dateTime={iso}>{visibleStr}</time>+useEffect客户端再格式化 - 或
suppressHydrationWarning(仅特定节点) - 或 server 端就用用户时区(从 cookie / header 读)
预防:CR 时看到任何"和当前时间 / 随机数 / 浏览器 only API" 渲染要警惕。
案例 3:"use client" 顶上 page.tsx → 首屏空白
现象:Lighthouse LCP 5 秒+,首屏巨大空白。
根因:某工程师把 "use client" 放 page.tsx 顶部,整页变 client,RSC 优势全没,数据获取改成 useEffect → 首屏没数据。
修复:page 保持 server,只把需要互动的子组件抽 client。
预防:Code Review 模板加一条"page.tsx 顶部不能有 "use client""。
案例 4:Image 没 width/height → CLS 飙红
现象:打开页面后内容跳来跳去,用户点击错位。
根因:用了 <img> 而非 <Image>,或 <Image> 没设 width/height。
修复:全部 next/image + 必须 width/height(或 fill + 父级 relative)。
预防:eslint-plugin-jsx-a11y / @next/next 的 no-img-element 必开。
案例 5:token 进了 localStorage → 被供应链攻击读
现象:某 npm 包(event-stream 类事件)被注入恶意代码,所有用户 token 被偷。
根因:为了"方便"把 access token 放 localStorage,JS 可读 → 恶意包可读。
修复:Cookie + HttpOnly + Secure + SameSite,client JS 拿不到。
预防:ESLint 自定义规则禁止 localStorage / sessionStorage 写关键字(token、auth、secret)。
案例 6:Sentry source map 上传失败 → 错误堆栈无法读
现象:Sentry 收到大量 minified 堆栈,看不到原始函数名。
根因:CI 上传 source map 步骤失败,但没 fail 整个 build → 部署照常进行。
修复:CI 上传 source map 改为强制成功;失败即整个发布失败。
预防:任何"辅助步骤但实际重要"的 CI step 都必须 continue-on-error: false。
案例 7:CSP 加严 → 老 dashboard 全挂
现象:上线新 CSP 后,某个嵌入式 BI dashboard 不显示数据。
根因:dashboard 用了 inline script,新 CSP 禁了 unsafe-inline。
修复:
- 灰度发 CSP(用
Content-Security-Policy-Report-Only先收集 30 天) - 看 violation 报告,把合法的 inline 改成 nonce / 移到外部文件
- 再切到 enforcing
预防:任何安全头变更先 Report-Only,等 violation 归零再 enforce。
案例 8:RSC 缓存把私有数据透传给所有人
现象:用户 A 看到了用户 B 的私人订单。
根因:
async function getOrders() {
return fetch(`${API}/orders`, { next: { revalidate: 60 } }).then(r => r.json());
// ❌ revalidate=60 + 没区分用户 → 全局缓存了 A 的数据
}
修复:
- 个性化 fetch 必须
cache: "no-store" - 或缓存 key 带 userId(
next: { tags: [orders:${userId}] })+ revalidateTag - DataCache 默认不区分用户,忘了 = 全员共享
预防:
- 封装
apiFetch,默认cache: "no-store",显式开缓存才生效 - Code Review 看到
next: { revalidate: ... }必问"是否个性化"
教训:这是最危险的 RSC 缓存事故。涉及隐私 / 合规可能 SEV-0。
案例 9:Server Action 越权 → 任何登录用户可删任何资源
现象:Penetration test 发现,登录用户 A 调 deleteOrderAction({ id: B_order_id }) 成功删了 B 的订单。
根因:
"use server";
export async function deleteOrderAction(input: { id: string }) {
await prisma.order.delete({ where: { id: input.id } }); // ❌ 没鉴权 + 没归属检查
}
修复:
export async function deleteOrderAction(input: { id: string }) {
const session = await getSession();
if (!session) throw new UnauthorizedError();
const order = await prisma.order.findUnique({ where: { id: input.id } });
if (!order || order.userId !== session.userId) throw new ForbiddenError();
await prisma.order.delete({ where: { id: input.id } });
}
预防:CR 模板必勾"Server Action 是否鉴权 + 授权 + 输入校验";Sentry / 安全扫描自动检测无鉴权的 action。
案例 10:CDN 缓存了 logged-in 页面 → 显示别人的姓名
现象:用户 A 打开首页,看到欢迎"你好,Bob"(Bob 是另一个用户)。
根因:CDN 配置缓存了所有 HTML,RSC payload 里有用户姓名,缓存命中后所有人看到同一个用户名。
修复:
- 个人化页面必须
Cache-Control: private, no-store - CDN allowlist:只缓存营销静态页 /
_next/static/*,其他透传 - 在 Next 里给个人化 page 设
dynamic = "force-dynamic"或cache: "no-store"
预防:CDN 配置 review 必查"是否区分公开 / 个性化路径"。
教训:CDN + 登录页 = 高危组合。每加一层缓存都要问:"谁会被错误命中?"
B3 - 浏览器侧性能诊断
B3.1 排查心法(前端版)
1. 量化:是 LCP / INP / TTFB 哪个差?p75 多少?
2. 分层:网络/JS 解析/Render/Layout/Paint 哪一段慢?
3. 定位:Performance panel + Lighthouse + 真实用户数据
4. 验证:改一处压一处
5. 灰度上线,监控 Vitals 变化
B3.2 Chrome DevTools Performance 实战
录一段操作 → 看时间线:
- Main 线程:每个长任务(> 50ms,红色三角)都是 INP 杀手
- Network:大文件 / 慢请求
- Paint:layout shift 在哪一帧发生
- Memory:有无意外飙升
常见模式:
- 黄色 Scripting 段巨长 → JS 卡 → 看是哪个函数(双击 stack)
- 紫色 Rendering 段巨长 → DOM 太大或样式昂贵 → 看 layout / style 重算
- 绿色 Painting 段巨长 → 渲染层过多 → 减少 will-change / 滤镜
B3.3 Lighthouse 关键指标
每个 PR 跑一次 Lighthouse CI:
- uses: treosh/lighthouse-ci-action@v11
with:
urls: |
https://preview-${{ github.event.number }}.app.example.com/
https://preview-${{ github.event.number }}.app.example.com/dashboard
budgetPath: ./lighthouse-budget.json
uploadArtifacts: true
lighthouse-budget.json:
[{
"path": "/*",
"resourceSizes": [
{ "resourceType": "script", "budget": 250 },
{ "resourceType": "total", "budget": 1500 }
],
"timings": [
{ "metric": "interactive", "budget": 3500 },
{ "metric": "largest-contentful-paint", "budget": 2500 }
]
}]
预算超出 → CI fail。
B3.4 INP 调优
INP 衡量"最慢的交互"。最常见原因:
- 点击后大量 setState 触发重渲染
- 同步计算阻塞(序列化、加密、排序大数组)
- 第三方脚本占主线程
// 修法 1:把昂贵更新放低优先级
import { startTransition } from "react";
function handleClick() {
setQuickResponse(value); // 立即响应
startTransition(() => {
setExpensiveFilter(value); // 让 React 在空闲时更新
});
}
// 修法 2:debounce
const debouncedSet = useMemo(() => debounce(setFilter, 200), []);
// 修法 3:计算移到 Worker
B3.5 React DevTools Profiler
录一段操作 → 看哪些组件重渲染过多。
红黄色 = 渲染慢的组件。点进去看:
- 重渲染原因(props 变化、parent 重渲、context 变化)
- 渲染耗时
常见根因:
- Context 频繁变化让所有消费者重渲
- 父组件每次 new 一个对象作 prop → 子的 memo 失效
- 没 key 或 key 不稳定的列表
💡
react-scan实时高亮重渲染组件,比 Profiler 更直观,开发期推荐。
B3.6 内存:浏览器侧泄漏
线索:用户长时间使用后变卡,刷新就好。
工具:Chrome DevTools → Memory → Heap snapshot,对比两个时间点。
常见根因:
- listener 未 cleanup(
useEffectreturn 没 off) - timer 未 clear
- WebSocket 未 close
- 大对象进了全局 store 不清
useEffect(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id); // 必有
}, []);
useEffect(() => {
const handler = () => {};
window.addEventListener("resize", handler);
return () => window.removeEventListener("resize", handler);
}, []);
B3.7 Bundle 优化进阶
Tree shaking 实战:
// ❌ 全量导入 lodash → bundle 70KB+
import _ from "lodash";
_.debounce(...)
// ✅ 按需(配合 lodash-es)→ 仅 5KB
import debounce from "lodash-es/debounce";
// ✅ 用 modern alternative
import { debounce } from "@reactuses/core";
代码分割:
// 重组件懒加载
const RichEditor = dynamic(() => import("./RichEditor"), {
loading: () => <EditorSkeleton />,
ssr: false, // 仅互动型组件
});
// 按路由自动分割(App Router 默认)
barrel file 陷阱:
// components/index.ts —— ❌
export * from "./Button";
export * from "./ChartHeavy";
// 一个 import 拉整个 barrel
// ✅ 直接 import
import { Button } from "@/components/Button";
Next 14+ optimizePackageImports 能自动优化某些 barrel,但仍以直接 import 为安全。
B3.8 字体优化
import { Inter } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
display: "swap",
preload: true,
weight: ["400", "600"], // 只装必要字重
variable: "--font-inter",
});
中文字体:
- 全量字体 5MB+,不要全包
- 子集化(
subset-font工具)按页面预编子集 - 或用 woff2 + unicode-range 分片
B3.9 网络优化
- HTTP/2 / HTTP/3(CDN 默认开)
<link rel="preconnect">给跨域第三方<link rel="preload">给关键资源- Service Worker 缓存策略
- 图片用 AVIF / WebP(
next/image自动)
// app/layout.tsx
<link rel="preconnect" href="https://api.example.com" />
<link rel="dns-prefetch" href="https://cdn.example.com" />
B3.10 压测前端:WebPageTest
不只是 Lighthouse。WebPageTest 能:
- 在真实网络(3G / 4G)模拟
- 多地域测试(瑞士 / 印度 / 巴西)
- 同时跑 9 次取中位数(去抖动)
- film strip 看渲染过程逐帧
月度 Vitals review 必跑 WebPageTest 关键页面。
B4 - 工程师软实力(前端版)
与后端 A5 章重叠,这里只补 前端特有 部分。
B4.1 与设计师协作
- Figma → Code 直接 1:1 像素还原是常见错觉。设计稿是规格,不是实现细节,留出 token / 系统层差异
- 设计 token(spacing / color / typography)与 Tailwind 配置同步,改一处全局生效
- 复杂交互让设计师在 Figma 标"hover / focus / disabled / loading / error" 五态,别全靠口头沟通
// tailwind.config.ts 与设计 token 对齐
import { figmaTokens } from "./design-tokens.json";
export default {
theme: {
colors: figmaTokens.colors,
spacing: figmaTokens.spacing,
},
};
B4.2 与后端协作
- OpenAPI / Zod schema 共享:前后端用同一份 schema(本教程的
@my-app/shared) - 错误模型一致:RFC 7807,字段错误统一映射(04 章)
- breaking change 提前 2 周公告,加
Sunsetheader - 生产 bug 联调:前端报
x-request-id,后端查日志,5 分钟定位
B4.3 PR 描述模板(前端)
## 改动
- 加 / 改 / 删了什么
## 截图 / 录屏
- (必须)桌面 + 移动端 各一张
- 暗色 / 亮色 各一张(若涉及)
## 影响范围
- 视觉:有变化 / 无变化
- API:无 / 有(列出 endpoint)
- a11y:已 / 未验证
- 性能:bundle 增减,Lighthouse 跑过
## 验证
- [ ] 桌面 Chrome / Safari / Firefox
- [ ] 移动 iOS Safari / Android Chrome
- [ ] 暗色 / 亮色
- [ ] 键盘可达
- [ ] 屏幕阅读器朗读合理
- [ ] 网络慢速 3G 加载体验
## 风险
- 灰度策略 / 回滚方案
B4.4 评审清单(前端)
| 项 | 关注 |
|---|---|
| 正确性 | 边界(空、null、长文本、超长 URL) |
| RSC 心智 | "use client" 是不是放对位置?最浅边界? |
| 缓存 | 个性化数据是否误缓存(B2 案例 8)? |
| 安全 | dangerouslySetInnerHTML / token 存储 / Server Action 鉴权 |
| 性能 | bundle 增量?重渲染?N+1 fetch? |
| a11y | label / aria / 键盘 / 对比度 |
| i18n | 写死中文?换行?复数? |
| 移动端 | 触摸目标 ≥ 44px?safe-area?横竖屏? |
B4.5 设计前先做"5 个核心问题"
任何复杂 UI 开干前自问:
- 加载状态什么样?(骨架屏 / spinner / 占位)
- 空数据什么样?(引导操作 / 解释)
- 错误什么样?(可重试 / 联系客服 / 错误 ID)
- 数据量极端时什么样?(0 / 1 / 100 / 10000 条)
- 慢网络什么样?(3G / 离线)
💡 70% 的前端 bug 来自这 5 种状态没考虑全。设计师常常只画"happy path",剩下的全靠你想到。
B4.6 学习路径(前端)
- 基础:HTML 语义 / CSS layout(flex / grid)/ JS 异步 / TS 进阶
- 框架:React 文档(尤其新的)、Next.js 源码 examples
- 性能:web.dev 课程、Chrome DevTools 文档
- a11y:WCAG 2.2、A11y Project、屏幕阅读器实操
- 进阶:Server Components RFC、React Compiler、Dan Abramov 博客
- 设计:Refactoring UI 这本书
B4.7 高级前端 vs 中级前端
| 维度 | 中级 | 高级 |
|---|---|---|
| 用 React | 写组件 | 知道何时 RSC / Client / Worker |
| 状态管理 | 用 Redux / Zustand | 知道何时 URL / Server / Local / Shared |
| 性能 | 跑 Lighthouse | 真实用户 Vitals + 定位优化 |
| a11y | 听过 | 键盘 + 屏幕阅读器都测过 |
| 设计 | 还原 Figma | 与设计师协商 token 体系 |
| 安全 | 知道 XSS | 知道 RSC 缓存可能泄露、CSP nonce、SSRF |
| 上线 | 推 main | 灰度 / 监控 / 回滚自动化 |
结语
到这里整个教程(11 章 + 4 章附录)结束。
如果你只能记住一件事,记这个:
代码是工具,判断力才是壁垒。 框架会换、库会过时、最佳实践会演进。但"知道决策的代价、知道失败的形态、知道何时该说不"这套判断力,在哪门技术下都通用。
高级工程师不是"会更多框架的中级工程师",是"在同样问题上能给出更深、更全、更长期的答案的工程师"。
祝你写出值得自己 5 年后回看不脸红的代码。