主题
09 · 网络与缓存:HTTP、鉴权刷新、上传
目标:说清 强缓存 / 协商缓存;实现「并发 401 只刷新一次 Token」的心智模型;了解大文件分片上传思路;能对照本仓库
request.js讲给同事听。
1. 背景:网络不只是 fetch 包一层
Journal C 端走 Koa API,Admin 走 React;静态资源要 CDN,接口要带 JWT。面试和线上都高频的三块:浏览器缓存、Token 过期竞态、大文件上传。分开理解,再落到 client/src/utils/request.js。
2. 核心概念:HTTP 缓存
| 阶段 | 关键响应头 | 行为 |
|---|---|---|
| 强缓存 | Cache-Control: max-age=… / Expires | 未过期直接用本地,不发请求 |
| 协商缓存 | ETag / Last-Modified | 发条件请求 If-None-Match / If-Modified-Since,304 则用缓存 |
直觉:
- 带 hash 的 JS/CSS:
max-age很长,文件名变即换版本 - HTML / 接口 JSON:通常
no-store或短max-age,避免旧数据 - API 鉴权响应不应被公共 CDN 缓存
Journal:图片走 OSS/CDN 可强缓存;/notes 列表必须带 Token,缓存键含用户与会话。
3. 核心概念:并发 401 与单次刷新
场景:首页同时发 5 个请求,Token 同时过期。若每个 401 各刷一次 refresh,后端可能吊销会话或产生 race。
模式:单飞(single-flight)+ 订阅队列
text
请求 A 401 → isRefreshing=true → 调 /auth/refresh
请求 B 401 → 发现正在刷新 → subscribe,挂起
请求 C 401 → 同上,进 refreshSubscribers 队列
刷新成功 → onRefreshed(newToken) → A/B/C 用新 Token 重试
刷新失败 → onRefreshFailed → 全体 reject / 强制重新登录1
2
3
4
5
2
3
4
5
对照 Journal request.js(节选逻辑):
javascript
let isRefreshing = false
let refreshSubscribers = []
function subscribeTokenRefresh(resolve, reject) {
refreshSubscribers.push({ resolve, reject })
}
function onRefreshed(token) {
refreshSubscribers.forEach(({ resolve }) => resolve(token))
refreshSubscribers = []
}
async function refreshToken() {
if (isRefreshing) {
return new Promise((resolve, reject) => {
subscribeTokenRefresh(resolve, reject)
})
}
isRefreshing = true
try {
// POST /auth/refresh …
const newToken = /* … */
onRefreshed(newToken)
return newToken
} catch (e) {
onRefreshFailed(e)
throw e
} finally {
isRefreshing = false
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
业务请求在 shouldTryTokenRefresh(如 code 1002 +「Token 过期」)时走 refresh,再用新 Token 重放原请求——而不是让用户手动再点一次。
还要区分:refresh 失败 vs「用户不存在」→ Journal 用 shouldForceRelogin + handleForceRelogin 清 Token 并触发重新登录,避免死循环刷新。
4. 核心概念:大文件上传(分片思路)
| 步骤 | 说明 |
|---|---|
| 切片 | File.slice(start, end),每片 2~5MB |
| 上传 | 每片带 uploadId、partNumber、checksum |
| 合并 | 全片成功后 POST /complete |
| 断点续传 | 服务端返回已收片列表,客户端跳过 |
取舍:小图直传 OSS 预签名即可;视频、导出包再上分片。uni-app 注意小程序包大小与 uni.uploadFile 并发限制。
5. 最小实践:自写刷新队列(练习版)
在不改 Journal 的前提下,用 30 行练手:
javascript
let refreshing = null
const waiters = []
async function getToken() {
if (!refreshing) {
refreshing = fetch('/auth/refresh', { method: 'POST' })
.then(r => r.json())
.then(d => d.token)
.finally(() => { refreshing = null })
}
return refreshing
}
async function api(url, opts) {
let res = await fetch(url, opts)
if (res.status !== 401) return res
const token = await new Promise((ok, no) => waiters.push({ ok, no }))
?? await getToken().then(t => (waiters.splice(0).forEach(w => w.ok(t)), t))
return fetch(url, { ...opts, headers: { ...opts.headers, Authorization: token } })
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
对照仓库真实现:注意 X-Request-Id、uni.request 封装、以及业务 code 而非纯 HTTP 401。
6. 踩坑与取舍
| 坑 | 后果 | 做法 |
|---|---|---|
| 刷新接口也用同一拦截器 | 无限递归 | refresh 请求 bypass 或使用独立 client |
| 重试无上限 | 风暴 | 每请求最多重试 1 次 |
| 缓存 Token 在 localStorage + XSS | 令牌被盗 | 见 10;敏感场景 HttpOnly Cookie + CSRF 防护 |
| 分片无幂等 | 重复片污染 | partNumber + etag 校验 |
7. 验收清单
- [ ] 画强缓存 vs 协商缓存决策树(静态资源 vs API)
- [ ] 口述 single-flight + subscribers 流程,并打开
request.js指认变量 - [ ] 说清 Journal 业务码 1002 与 HTTP 401 的差异
- [ ] 列出大文件上传 4 步(切、传、并、续)
8. 下一步
参考链接
- MDN — HTTP caching
- RFC 9110 — Conditional Requests
- Journal:
client/src/utils/request.js、backend鉴权路由
