模块二 · 工程化与网络
网络与缓存:一个请求要付出多少成本
一次请求的成本由 DNS、连接、TLS、传输与解析共同决定。这一节讲清 HTTP 版本差异、缓存策略、CDN、跨域与安全,以及可落地的优化手段。
读完这一节,你能做到
- 说清 HTTP/1.1、HTTP/2、HTTP/3 各自解决什么问题。
- 写出正确的缓存策略,而不是只会加
no-cache。 - 判断跨域问题出在预检、凭证还是响应头。
- 用 preload/preconnect 优化关键请求链。
一、HTTP 版本演进
| 版本 | 传输层 | 并发 | 主要问题 |
|---|---|---|---|
| HTTP/1.1 | TCP | 每域名 6 个左右连接 | 队头阻塞、头部冗余 |
| HTTP/2 | TCP | 单连接多路复用 | TCP 层队头阻塞 |
| HTTP/3 | QUIC(UDP) | 多路复用、0-RTT | 兼容性与中间设备 |
HTTP/2 的头部压缩(HPACK)与多路复用,解决的是应用层队头阻塞;但 TCP 丢包仍会阻塞所有流,HTTP/3 用 QUIC 把流独立开,才解决传输层问题。
理解这条演进,就能回答“为什么域名分片在 HTTP/2 下反而有害”——多连接会破坏多路复用与压缩收益。
二、缓存:先问三个问题
- 这个资源能不能缓存?(敏感数据不能)
- 缓存多久?(强缓存)
- 过期后怎么验证?(协商缓存)
| 指令 | 作用 |
|---|---|
Cache-Control: max-age=31536000, immutable | 一年强缓存,适合带哈希的静态资源 |
Cache-Control: no-cache | 可以缓存,但每次都要验证 |
Cache-Control: no-store | 完全不缓存,适合敏感数据 |
ETag + If-None-Match | 协商缓存,未变返回 304 |
Last-Modified + If-Modified-Since | 时间戳验证,精度较低 |
最常见的错误是把 no-cache 当成“不缓存”。它其实是“缓存但必须验证”,真正不缓存是 no-store。
内容哈希 + 长强缓存是静态资源的最佳实践:文件内容变,哈希变,URL 变,自然绕过缓存。
三、CDN 与边缘缓存
CDN 把资源放到离用户更近的边缘节点,减少 RTT 与回源。关键配置:
- 缓存键:默认按 URL,忽略无关查询参数,避免缓存碎片。
- 回源策略:边缘未命中时回源,回源带宽是成本大头。
- 刷新与预热:发布后刷新边缘缓存,大促前预热。
- 动态加速:API 走动态加速,减少跨地域延迟。
CDN 不是“加了就快”:如果缓存命中率低,收益会被回源抵消。
四、跨域与安全
跨域是浏览器同源策略的限制,不是服务器拒绝。解决方式:
- CORS:服务端返回
Access-Control-Allow-Origin等头;非简单请求会先发 OPTIONS 预检。 - 凭证:带 Cookie 时需要
credentials: 'include',且服务端不能返回*。 - 代理:开发环境用 devServer 代理,生产用同域反向代理。
安全相关:
| 头 | 作用 |
|---|---|
Content-Security-Policy | 限制脚本、样式、图片来源 |
X-Content-Type-Options: nosniff | 禁止 MIME 嗅探 |
Set-Cookie: SameSite=Lax | 限制跨站携带 Cookie |
Strict-Transport-Security | 强制 HTTPS |
CORS 是“读取限制”,不是“请求限制”:请求可能已经到达服务端,只是响应被浏览器拦截。
XSS、CSRF 与 CSP
先分清两类攻击:
- XSS:把恶意脚本注入你的页面,在用户浏览器里执行。
- CSRF:借用用户已登录的身份,在用户不知情时发起请求。
一个往里塞代码,一个借身份发请求,防御思路完全不同。
XSS 的三种类型
| 类型 | 注入点 | 例子 |
|---|---|---|
| 存储型 | 数据存进数据库后再渲染出来 | 评论区写入 <script> |
| 反射型 | 参数直接回显到页面 | 搜索词原样输出到 HTML |
| DOM 型 | 前端把不可信数据写进 DOM | innerHTML = location.hash |
防御三层
- 输出转义:渲染用户内容时转义
< > & " '。React 的 JSX 默认转义,Vue 的{{ }}也是;危险的是v-html和dangerouslySetInnerHTML。 - CSP:限制脚本只能从白名单加载,即使被注入也无法执行外链脚本。
- 输入校验:不能替代转义,但能减少攻击面。
CSRF 的三个条件:用户已登录、攻击者能构造请求、浏览器会自动带上 Cookie。防御就是打断其中任一条件:
| 手段 | 原理 |
|---|---|
SameSite=Lax/Strict | 跨站请求不携带 Cookie |
| CSRF Token | 请求需带上服务端下发的随机值 |
校验 Origin / Referer | 拒绝非本站来源 |
| 敏感操作改 POST + 二次确认 | 提高攻击成本 |
CSP 怎么配
最小的可用配置:
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'
三条纪律:不用 unsafe-inline(改用 nonce 或 hash)、不用 unsafe-eval、先用 report-only 观察一段时间再上线拦截。
面试一句话:「XSS 是注入,靠转义与 CSP 防;CSRF 是冒用身份,靠 SameSite 与 Token 防。」
五、关键请求链优化
<link rel="preconnect" href="https://api.example.com" crossorigin />
<link rel="dns-prefetch" href="//cdn.example.com" />
<link
rel="preload"
href="/fonts/main.woff2"
as="font"
type="font/woff2"
crossorigin
/>
三条原则:
- 只对“首屏必须且发现晚”的资源用 preload。
- preconnect 只给确定马上要用的域名。
- prefetch 用于下一页,优先级最低,不要抢首屏带宽。
六、常见坑
- 把
no-cache当成不缓存。 - 静态资源不带哈希却设置长缓存,发布后用户看到旧版本。
- 带 Cookie 的跨域请求返回
*,浏览器直接拒绝。 - 预检请求没缓存,每次请求都多一次 RTT。
- CDN 缓存键包含随机参数,命中率极低。
七、动手任务:自写 → AI 写 → 对比 → 取舍
四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。
① 自己写
- 用 DevTools 看一次请求的缓存命中情况,判断是强缓存还是 304。
- 给静态资源配置内容哈希 + 长缓存,验证发布后的更新。
- 用 OPTIONS 预检观察一次跨域请求,检查
Access-Control-Max-Age。
② 让 AI 写同一题
把第 2 题(缓存策略设计)交给 AI,补一句 说清强缓存与协商缓存分别用在什么资源上。
③ 对比要盯的点
Cache-Control与ETag分工它说对了吗?前者管强缓存有效期,后者管协商。- 它有没有区分“没有缓存”和“缓存但需验证”?
stale-while-revalidate它提了吗?这是静态资源常用的折中。
④ 一句话取舍
发版之后,它这套策略会让用户多久看到新资源?
八、自测
- HTTP/2 与 HTTP/3 分别解决什么队头阻塞?
- 强缓存与协商缓存的判断依据是什么?
no-cache与no-store的区别?- 带凭证的跨域为什么不能用
*? - preload 与 prefetch 的区别?
- XSS 与 CSRF 的防御思路分别是什么?
参考答案要点:应用层 vs 传输层;是否发请求;验证 vs 不存;凭证与通配冲突;关键资源 vs 空闲预取;XSS 靠输出转义与 CSP,CSRF 靠 SameSite、Token 与来源校验。
附录:高频追问
问:304 会带响应体吗? 不会,只有响应头,节省带宽。
问:ETag 和 Last-Modified 谁优先? 服务端可同时返回,浏览器优先用 ETag,因为更精确。
问:为什么静态资源要用内容哈希? 文件名随内容变化,可以放心设置长强缓存,发布即失效。
问:CORS 预检什么时候触发? 非简单请求(自定义头、PUT/DELETE、非表单 content-type)会先发 OPTIONS。
问:SameSite=Lax 影响什么? 限制跨站请求携带 Cookie,降低 CSRF 风险,但可能影响第三方登录回跳。
问:HTTP/2 的服务端推送为什么被弃用? 推送难以与缓存协作,容易推了不需要的资源,现多用 103 Early Hints 替代。
问:CDN 回源失败怎么办? 配置多源与降级,边缘可返回 stale 缓存,避免直接 5xx。
问:如何判断缓存配置是否正确? 看响应头的 Cache-Control、Age、X-Cache,以及 DevTools 的 “from disk/memory cache”。
三种缓存失效场景
- 发布后 HTML 命中旧缓存,引用了不存在的旧 chunk。解法:HTML 用
no-cache或短缓存。 - CDN 缓存键包含随机参数,命中率极低。解法:配置忽略无关 query。
- 灰度发布时同一 URL 返回不同内容,边缘缓存串味。解法:按版本或灰度标识区分缓存键。
一次跨域排查
现象:接口在 Postman 正常,浏览器报 CORS。排查顺序:
- 看请求是否发了 OPTIONS,预检返回什么。
- 看响应头是否包含
Access-Control-Allow-Origin,是否与 Origin 完全一致。 - 带 Cookie 时,检查
Allow-Credentials与Allow-Origin是否冲突。 - 检查网关/反代是否覆盖了服务端返回的头。
CORS 报错必须看响应头,不能只看控制台结论。
缓存策略速查
| 资源 | 策略 |
|---|---|
| 带 hash 的 JS/CSS | 一年强缓存 + immutable |
| HTML | no-cache(每次协商) |
| 图片/字体 | 长缓存,文件名带版本 |
| API 数据 | 按业务定,敏感数据 no-store |
这张表能覆盖 80% 的场景,剩下的是灰度与多租户的缓存键设计。
CDN 命中率怎么算
命中率 = 边缘命中次数 ÷ 总请求次数
命中率低通常有三个原因:缓存键太细(带随机参数)、TTL 太短、资源变体太多。先看命中率,再谈回源优化;命中率不上去,加带宽只是花钱。缓存是性能的第一杠杆,先命中,再优化;命中率决定收益。先命中,后回源。
配套题目
★ 越多越该先背;点题目可看完整答案与深入版。
提示:读完可以顺手做几道对应题目,检验是否真的理解