前端工程化实战:请求封装、环境变量、MSW 与 TanStack Query
从 http.ts 到数据缓存的完整工程实践——封装、Mock、缓存、错误处理一条链路
1. HTTP 请求封装(http.ts)
核心设计:洋葱模型(拦截器管道)
请求发出 → 拦截器1(加 token)→ 拦截器2(加 request-id)→ fetch → 拦截器3(检查状态码)→ 返回结果
设计思路(通用模板):
1. 类型先行:先定义 Input / Output / Config 的类型
2. 工具函数:拆成纯函数(buildUrl / parseResponse / createHttpError)
3. 核心逻辑:一个 request 函数串起来(唯一调用 fetch 的地方)
4. 语法糖:get / post / put / patch / delete 快捷方法
5. 拦截器:use 注册 / eject 注销(发布-订阅模式)
6. 导出门面:只暴露 http 对象
拦截器用法:
// 请求拦截:自动注入 token
http.interceptors.request.use((config) => {
const token = localStorage.getItem('token')
if (token) config.headers = { ...config.headers, Authorization: `Bearer ${token}` }
return config
})
// 响应拦截:401 自动跳登录
http.interceptors.response.use(
(res) => res,
(err) => {
if (err.status === 401) {
localStorage.removeItem('token')
window.location.href = '/login'
}
throw err
},
)
职责分工:
lib/http.ts → 请求管道(全局,一个就够)
features/*/api.ts → 模块 API(每个模块自己的接口)
features/*/hooks.ts → TanStack Query hooks
组件 → 调 hooks,不直接调 api
2. 环境变量(.env)
文件优先级:
.env.development.local > .env.development > .env
.env.production.local > .env.production > .env
三种模式:
VITE_API_BASE_URL= → 同源,MSW 拦截(默认)
VITE_API_BASE_URL=http://localhost:3000 → 跨域,Vite proxy 转发
VITE_API_BASE_URL=https://api.xxx.com → 生产环境
Vite proxy vs MSW vs 真实后端:
开发 + MSW: 请求走同源 → MSW Service Worker 拦截 → 返回假数据
开发 + proxy:请求走同源 → Vite dev server 转发 → 真实后端
生产: 请求走 .env.production 的 URL → 真实后端
.env 文件规则:
.env / .env.development / .env.production → 提交到 git(非敏感配置)
.env.local / .env.development.local → 不提交(个人覆盖)
http.ts 的 buildUrl 逻辑:
// BASE_URL 为空 → 同源(MSW / proxy 能拦截)
// BASE_URL 有值 → 跨域(直接请求)
function buildUrl(path, params) {
if (!BASE_URL) return path // /api/users
return new URL(path, BASE_URL).toString() // http://localhost:3000/api/users
}
3. MSW(Mock Service Worker)
在浏览器里拦截 fetch 请求,返回假数据。
// handlers.ts
export const handlers = [
http.get('/api/users', () => HttpResponse.json({ data: [...] })),
http.post('/api/auth/login', async ({ request }) => {
const body = await request.json()
if (body.email === 'admin@example.com') return HttpResponse.json({ token: 'xxx' })
return HttpResponse.json({ error: 'failed' }, { status: 401 })
}),
]
启动方式(main.tsx):
async function enableMocking() {
if (import.meta.env.PROD) return // 生产环境不启动
const { worker } = await import('./mocks/browser')
return worker.start({ onUnhandledRequest: 'bypass' })
}
enableMocking().then(() => {
createRoot(document.getElementById('root')!).render(...)
})
关键点:
-
MSW 只拦截同源请求(不同端口 = 不同源,拦截不到)
-
BASE_URL必须为空,请求走相对路径/api/users -
生产环境自动关闭,切换到真实后端零改动
4. TanStack Query
声明式数据管理,替代手动 useEffect \+ useState。
// hooks.ts
export function useUsers(params?: { page?: number }) {
return useQuery({
queryKey: ['users', params], // 缓存 key(变化时重新请求)
queryFn: () => userApi.list(params),
staleTime: 5 * 60 * 1000, // 5 分钟内不重新请求
})
}
// 组件里
const { data, isLoading, error } = useUsers()
对比手动写法:
// ❌ 手动:5 个 useState + 1 个 useEffect + 手动管理 loading/error
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => { ... }, [])
// ✅ TanStack Query:1 行搞定
const { data, isLoading, error } = useUsers()
核心配置:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 分钟新鲜
gcTime: 10 * 60 * 1000, // 10 分钟缓存保留
refetchOnWindowFocus: false, // 切回页面不自动刷新
retry: 1, // 失败重试 1 次
},
},
})
自动管理的能力:
-
loading / error / data 状态
-
缓存(同一个 queryKey 不重复请求)
-
重新聚焦时自动刷新
-
失败重试
-
窗口切换时的乐观更新
核心一句话
http.ts 管请求管道,.env 管环境切换,MSW 管 mock 数据,TanStack Query 管缓存和状态。四层各司其职,互不耦合。