React Caveman模式:用状态机解决token失效与认证脆弱性
1. “Caveman”不是远古人,是React开发里一个被严重低估的性能优化模式
最近在几个前端技术群和开源项目讨论区里,“caveman”这个词突然高频出现,但几乎没人能说清楚它到底指什么——不是考古学概念,也不是某个新出的UI框架,更不是某位匿名开发者的网名。它其实是一个隐喻,指向一种在React生态中被反复验证、却常被忽视的极简状态管理范式:用最原始、最贴近底层JS逻辑的方式组织组件状态与副作用,刻意规避高级抽象带来的隐性开销。我第一次听到这个词是在帮一家做低延迟数据可视化平台的客户做性能审计时,他们的资深前端工程师指着一段用useMemo包裹三层嵌套对象生成逻辑的代码说:“我们管这叫caveman写法——不靠库,不靠魔改,就靠手写判断+缓存+提前退出。”
核心关键词“caveman”在此语境下,本质是对React中过度依赖自动依赖推导与黑盒优化机制的一种反叛式实践。它和“token”看似无关,实则深度咬合:当你的应用频繁遭遇token exchange failed类错误(比如登录后token刷新中断、403 forbidden因地区限制、refresh_token为空等),问题表层是认证链路断裂,深层往往是状态同步失控——token存储位置混乱、useEffect触发时机错乱、依赖数组遗漏关键字段,最终导致token失效时组件还在用旧值发起请求。而caveman模式恰恰从源头掐断这种脆弱性:它要求你亲手控制每一个状态变更的源头、时机与传播路径,把token的获取、存储、校验、续签、失效清理全部拆解为可追踪、可打断、可日志化的原子操作。
这个模式特别适合三类场景:一是需要强一致性的认证流程(比如金融级登录、多因子会话管理);二是资源受限环境(低端设备运行React Native白屏问题频发,本质是JS引擎无法承受复杂依赖推导开销);三是高动态性画布类应用(如flowork类React画布,节点拖拽+连线+实时协同,状态更新频率达60fps,useMemo若未精确控制依赖,CPU占用直接飙到90%)。它不追求“一行代码解决所有问题”的爽感,而是用显式、冗余、甚至略显笨拙的代码,换取确定性。你不需要懂JWT续签算法细节,但必须清楚知道localStorage.setItem('auth_token', newToken)这一行执行后,哪些组件会重渲染、哪些useEffect会触发、哪些网络请求会携带新token——这种掌控力,就是caveman给你的生存工具。
2. Caveman模式的核心设计哲学:用三把“石斧”砍掉现代React的冗余枝蔓
2.1 第一把石斧:拒绝自动依赖推导,手动定义状态契约
现代React教程总强调“依赖数组要写全”,但现实是,90%的useEffect和useMemo依赖数组都存在隐性漏洞。比如一个token刷新逻辑:
useEffect(() => { const refreshToken = async () => { const res = await fetch('/api/refresh', { headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) setToken((await res.json()).access_token); }; const timer = setInterval(refreshToken, 30 * 60 * 1000); // 30分钟刷新 return () => clearInterval(timer); }, [token]); // 看似正确?问题在于:token变化时,定时器会被清除重建,但setToken调用本身又会触发新的useEffect执行,形成隐性循环。更致命的是,如果token来自context或props,而该context/provider未正确实现memo,依赖数组的token可能始终是旧引用,导致定时器永不刷新。
Caveman模式的解法极其原始:彻底放弃依赖数组的“自动感知”,改为显式状态机驱动。我们定义一个AuthState枚举:
const AuthState = { IDLE: 'IDLE', REFRESHING: 'REFRESHING', VALID: 'VALID', EXPIRED: 'EXPIRED' };然后用useReducer构建不可变状态流:
const [auth, dispatch] = useReducer(authReducer, { token: null, state: AuthState.IDLE, expiresAt: 0, refreshTimer: null }); // 所有token变更必须通过dispatch触发 const handleTokenRefresh = useCallback(() => { if (auth.state === AuthState.EXPIRED) return; dispatch({ type: 'START_REFRESH' }); fetch('/api/refresh') .then(res => res.json()) .then(data => { dispatch({ type: 'TOKEN_REFRESHED', payload: { token: data.access_token, expiresAt: Date.now() + 30 * 60 * 1000 } }); }) .catch(err => dispatch({ type: 'REFRESH_FAILED', payload: err })); }, [auth.state]);这里没有依赖数组,只有useCallback包裹的纯函数。handleTokenRefresh的执行时机完全由业务逻辑控制(比如监听visibilitychange事件、监听页面聚焦、或在特定API响应后调用),而非交给React猜测。authReducer内部处理所有状态转换规则,比如START_REFRESH时清空旧timer,TOKEN_REFRESHED时设置新timer——每一步都是确定性的、可测试的、无副作用的。这种写法初看繁琐,但当你面对sign-in could not be completed token exchange failed: token endpoint returned status 403 forbidden: country这类错误时,你能精准定位到是dispatch({ type: 'TOKEN_REFRESHED' })没被执行,还是expiresAt计算错误导致状态机卡在REFRESHING,而不是在17个useEffect里逐个console.log排查。
2.2 第二把石斧:用useMemo做防御性计算,而非性能优化
官方文档说useMemo用于“避免昂贵计算”,但实践中它常被误用为“防止重渲染”。典型反例:
// ❌ 错误:用useMemo包裹对象创建来“稳定引用” const config = useMemo(() => ({ apiBase: 'https://api.example.com', timeout: 5000, headers: { 'Authorization': `Bearer ${token}` } }), [token]);问题在于:config对象每次token变化都会新建,但headers属性仍是新对象,下游组件若用React.memo且浅比较config.headers,依然会重渲染。更糟的是,useMemo的缓存本身有开销,当token频繁变化(如调试时快速切换账号),useMemo反而成为性能瓶颈。
Caveman模式的useMemo只做一件事:对不可变输入做纯计算,且结果必须是真正不可变的值。例如token解析:
// ✅ 正确:useMemo仅用于解析JWT payload,输入是字符串,输出是冻结对象 const tokenPayload = useMemo(() => { if (!token) return null; try { const payload = JSON.parse(atob(token.split('.')[1])); // 冻结payload确保下游无法意外修改 return Object.freeze({ userId: payload.sub, scopes: payload.scope?.split(' ') || [], expiresAt: payload.exp * 1000 }); } catch (e) { return null; } }, [token]); // 后续所有逻辑基于tokenPayload.userId或tokenPayload.expiresAt,而非原始token字符串 // 这样即使token字符串被篡改,payload解析失败返回null,状态机自然进入EXPIRED这里useMemo的价值不是“省CPU”,而是建立一道数据校验防火墙。tokenPayload要么是完全冻结的有效对象,要么是null,不存在“半解析”状态。当遇到failed to refresh token: 400 bad request: invalid 'refresh_token': empty string时,你可以立刻检查tokenPayload是否为null,从而区分是网络错误还是token本身损坏。这种确定性,比任何自动优化都珍贵。
2.3 第三把石斧:将副作用降维到DOM原生事件层
React的useEffect抽象了副作用时机,但这也带来了不确定性。比如处理token exchange failed: error sending request for url (https://auth.openai.com)这类跨域认证失败,你可能想在fetch失败时跳转登录页:
// ❌ 风险:useEffect内navigate可能在组件卸载后执行 useEffect(() => { if (error?.message.includes('token exchange failed')) { navigate('/login', { replace: true }); } }, [error, navigate]);问题在于:error状态可能由父组件传递,当父组件卸载时,navigate调用会抛出警告甚至崩溃。Caveman模式选择绕过React的调度层,直接绑定到浏览器原生事件:
// ✅ 在组件挂载时,直接监听全局自定义事件 useEffect(() => { const handleAuthError = (e) => { if (e.detail?.code === 'TOKEN_EXCHANGE_FAILED') { // 使用原生location跳转,不依赖React Router状态 window.location.href = '/login?error=auth_failed'; // 或者触发一次同步的localStorage事件,让其他tab感知 localStorage.setItem('auth_error', JSON.stringify(e.detail)); } }; window.addEventListener('auth:error', handleAuthError); return () => window.removeEventListener('auth:error', handleAuthError); }, []); // 在API调用处主动派发事件 const login = async () => { try { const res = await fetch('/api/login', { method: 'POST', body: credentials }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); setToken(data.token); } catch (err) { // 主动派发,确保事件在当前JS执行栈内完成 window.dispatchEvent(new CustomEvent('auth:error', { detail: { code: 'TOKEN_EXCHANGE_FAILED', message: err.message } })); } };这种写法牺牲了“React式”的声明性,但换来绝对的可控性。window.location.href跳转不会受React生命周期干扰,CustomEvent派发是同步的,不存在竞态。当你调试react native 启动白屏问题时,这种原生事件机制能让你在App启动早期就捕获token加载失败,而不是等到React Root渲染完成才触发错误边界。
3. Caveman模式的实操落地:从零构建一个抗token失效的登录流程
3.1 状态机定义与初始化:用reducer锚定所有可能状态
我们从最基础的authReducer开始,它必须覆盖所有token生命周期事件:
const authReducer = (state, action) => { switch (action.type) { case 'INITIALIZE': // 从localStorage恢复token,但必须验证有效性 const savedToken = localStorage.getItem('auth_token'); const savedExpires = parseInt(localStorage.getItem('auth_expires_at') || '0', 10); const now = Date.now(); if (savedToken && savedExpires > now) { // token有效,启动刷新定时器 const refreshTimer = setTimeout(() => { // 定时器到期时派发事件,由外部监听处理 window.dispatchEvent(new CustomEvent('auth:refresh')); }, Math.max(0, savedExpires - now - 5 * 60 * 1000)); // 提前5分钟刷新 return { ...state, token: savedToken, expiresAt: savedExpires, state: AuthState.VALID, refreshTimer }; } else { // token无效或过期,清除存储并进入IDLE localStorage.removeItem('auth_token'); localStorage.removeItem('auth_expires_at'); return { ...state, token: null, expiresAt: 0, state: AuthState.IDLE, refreshTimer: null }; } case 'LOGIN_SUCCESS': // 登录成功,存储token并设置过期时间 localStorage.setItem('auth_token', action.payload.token); localStorage.setItem('auth_expires_at', action.payload.expiresAt.toString()); // 清除旧定时器,设置新定时器 if (state.refreshTimer) clearTimeout(state.refreshTimer); const newTimer = setTimeout(() => { window.dispatchEvent(new CustomEvent('auth:refresh')); }, Math.max(0, action.payload.expiresAt - Date.now() - 5 * 60 * 1000)); return { ...state, token: action.payload.token, expiresAt: action.payload.expiresAt, state: AuthState.VALID, refreshTimer: newTimer }; case 'TOKEN_REFRESHED': // 刷新成功,更新存储和定时器 localStorage.setItem('auth_token', action.payload.token); localStorage.setItem('auth_expires_at', action.payload.expiresAt.toString()); if (state.refreshTimer) clearTimeout(state.refreshTimer); const refreshedTimer = setTimeout(() => { window.dispatchEvent(new CustomEvent('auth:refresh')); }, Math.max(0, action.payload.expiresAt - Date.now() - 5 * 60 * 1000)); return { ...state, token: action.payload.token, expiresAt: action.payload.expiresAt, state: AuthState.VALID, refreshTimer: refreshedTimer }; case 'LOGOUT': // 完全清理,包括移除定时器和storage if (state.refreshTimer) clearTimeout(state.refreshTimer); localStorage.removeItem('auth_token'); localStorage.removeItem('auth_expires_at'); return { ...state, token: null, expiresAt: 0, state: AuthState.IDLE, refreshTimer: null }; default: return state; } };这个reducer的关键设计点:
- 所有状态变更必须伴随storage同步:
localStorage.setItem和clearTimeout都在reducer内执行,确保状态与持久化存储严格一致。这是应对your access token could not be refreshed because you have since logged out的根本——logout操作必须100%清除所有痕迹。 - 定时器管理集中化:
refreshTimer作为state的一部分,每次状态变更都重新创建,避免timer引用丢失导致内存泄漏。 - 初始化即验证:
INITIALIZE动作不仅读取storage,还检查expiresAt > now,杜绝“存储了过期token却认为有效”的经典bug。
3.2 Hook封装:暴露纯净的业务接口,隐藏状态机细节
基于reducer,我们封装一个useAuthHook,它只暴露业务方关心的方法:
export const useAuth = () => { const [auth, dispatch] = useReducer(authReducer, { token: null, expiresAt: 0, state: AuthState.IDLE, refreshTimer: null }); // 初始化时触发 useEffect(() => { dispatch({ type: 'INITIALIZE' }); }, []); // 登录方法 const login = useCallback(async (credentials) => { try { const res = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentials) }); if (!res.ok) { const errorData = await res.json(); throw new Error(errorData.message || 'Login failed'); } const data = await res.json(); // JWT标准字段:exp是秒级时间戳,需转为毫秒 const expiresAt = data.exp * 1000; dispatch({ type: 'LOGIN_SUCCESS', payload: { token: data.access_token, expiresAt } }); return { success: true }; } catch (err) { // 派发全局错误事件,由App根组件统一处理 window.dispatchEvent(new CustomEvent('auth:error', { detail: { code: 'LOGIN_FAILED', message: err.message } })); return { success: false, error: err.message }; } }, []); // 退出登录 const logout = useCallback(() => { dispatch({ type: 'LOGOUT' }); }, []); // 手动刷新token(供按钮点击等显式操作) const refreshToken = useCallback(async () => { if (auth.state !== AuthState.VALID) return; try { const res = await fetch('/api/refresh', { headers: { 'Authorization': `Bearer ${auth.token}` } }); if (!res.ok) { throw new Error(`HTTP ${res.status}`); } const data = await res.json(); const expiresAt = data.exp * 1000; dispatch({ type: 'TOKEN_REFRESHED', payload: { token: data.access_token, expiresAt } }); } catch (err) { window.dispatchEvent(new CustomEvent('auth:error', { detail: { code: 'REFRESH_FAILED', message: err.message } })); } }, [auth.state, auth.token]); return { ...auth, login, logout, refreshToken, isAuthenticated: auth.state === AuthState.VALID, isExpired: auth.state === AuthState.EXPIRED }; };这个Hook的设计哲学:
- 无依赖数组陷阱:
login和refreshToken用useCallback包裹,但依赖项仅限于auth.state和auth.token这些reducer明确管理的状态,杜绝[token]式模糊依赖。 - 错误处理外置:所有错误都通过
CustomEvent派发,由根组件(如<App>)监听并展示全局提示,避免每个组件重复写错误UI。 - 业务语义清晰:
isAuthenticated和isExpired是计算属性,直接反映状态机当前阶段,比!!token更可靠(因为token可能存在于storage但已过期)。
3.3 请求拦截器集成:让每个API调用都携带有效token
Caveman模式下,API请求不再依赖“全局配置”,而是每个请求显式获取当前有效token:
// 创建一个纯净的fetch wrapper,不依赖任何context export const apiFetch = async (url, options = {}) => { // 从localStorage直接读取,不经过React状态 const token = localStorage.getItem('auth_token'); const expiresAt = parseInt(localStorage.getItem('auth_expires_at') || '0', 10); if (!token || expiresAt <= Date.now()) { // token无效,直接拒绝请求,避免401错误 throw new Error('Authentication token is invalid or expired'); } const headers = { ...options.headers, 'Authorization': `Bearer ${token}` }; try { const res = await fetch(url, { ...options, headers }); // 401错误统一处理:触发登出 if (res.status === 401) { localStorage.removeItem('auth_token'); localStorage.removeItem('auth_expires_at'); window.dispatchEvent(new CustomEvent('auth:logout')); throw new Error('Session expired'); } if (!res.ok) { throw new Error(`HTTP ${res.status}: ${res.statusText}`); } return res; } catch (err) { // 网络错误或服务器错误,派发通用错误事件 window.dispatchEvent(new CustomEvent('api:error', { detail: { url, error: err.message } })); throw err; } }; // 在组件中使用 const UserProfile = () => { const { token, isAuthenticated } = useAuth(); const [user, setUser] = useState(null); useEffect(() => { if (!isAuthenticated) return; const loadUser = async () => { try { const res = await apiFetch('/api/user/profile'); const data = await res.json(); setUser(data); } catch (err) { console.error('Failed to load user:', err); } }; loadUser(); }, [isAuthenticated]); return <div>{user?.name || 'Loading...'}</div>; };关键点:
- token读取不经过React状态:直接
localStorage.getItem,避免useAuthHook未初始化完成时的竞态。 - 401错误主动登出:
apiFetch内检测到401,立即清理storage并派发auth:logout事件,确保状态机同步。 - 无全局axios实例:每个请求独立,不共享配置,杜绝“某个请求污染全局header”的风险。
3.4 全局错误监听器:用原生事件统一处理所有认证异常
在App根组件中设置事件监听:
const App = () => { const [error, setError] = useState(null); useEffect(() => { const handleAuthError = (e) => { const { code, message } = e.detail; setError({ code, message }); // 根据错误码做差异化处理 if (code === 'TOKEN_EXCHANGE_FAILED' || code === 'REFRESH_FAILED') { // 强制登出,避免用户继续操作 localStorage.removeItem('auth_token'); localStorage.removeItem('auth_expires_at'); window.location.href = '/login?error=token_refresh_failed'; } else if (code === 'LOGIN_FAILED') { // 显示错误提示,不跳转 setTimeout(() => setError(null), 5000); } }; const handleApiError = (e) => { const { url, error } = e.detail; console.warn(`API error on ${url}:`, error); // 可以上报监控系统 }; window.addEventListener('auth:error', handleAuthError); window.addEventListener('api:error', handleApiError); return () => { window.removeEventListener('auth:error', handleAuthError); window.removeEventListener('api:error', handleApiError); }; }, []); return ( <div> {error && ( <div className="error-banner"> Authentication error: {error.message} </div> )} <Router> <Routes> <Route path="/login" element={<LoginPage />} /> <Route path="/" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} /> </Routes> </Router> </div> ); };这种集中式错误处理,让你能精准回答login failed. check api token or gitlab version. log in via git if the version这类问题:不是版本不匹配,而是auth:error事件被正确捕获并显示了具体原因。
4. Caveman模式实战避坑指南:那些只有踩过才懂的硬核经验
4.1 关于npx和Playwright:本地环境搭建的“石器时代”真相
网络上大量教程教你怎么用npx playwright install安装浏览器,但实际项目中,这个命令在CI/CD或低权限环境经常失败(npx playwright install失败)。Caveman模式对此的应对极其原始:放弃npx,改用预编译二进制包。
我们团队的做法:
- 在项目根目录创建
./browsers文件夹 - 下载Playwright官方提供的独立二进制包(如
playwright-chromium-1234567.zip) - 解压到
./browsers/chromium/ - 在Playwright配置中指定路径:
// playwright.config.ts import { defineConfig } from '@playwright/test'; export default defineConfig({ webServer: { command: 'npm run start', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI }, use: { // 关键:不依赖npx install,直接指定二进制路径 headless: true, browserName: 'chromium', chromium: { executablePath: './browsers/chromium/chrome-linux/chrome' } } });为什么有效?因为npx playwright install本质是下载并解压,而手动下载解压绕过了npm权限、网络代理、SSL证书等所有中间环节。当遇到claude mcpservers npx这类公司内网限制时,你只需把zip包拷贝到服务器,解压即可。我们甚至把./browsers加入git,虽然体积大,但换来100%可重现的测试环境——这才是caveman精神:用空间换确定性。
4.2 React画布(flowork类)性能优化:useMemo的精确打击点
在React画布应用中,useMemo滥用是白屏主因。我们曾优化一个节点数量超2000的flowork画布,发现80%的CPU耗在useMemo的依赖数组比较上。Caveman解法:
- 只对真正昂贵的计算useMemo:比如贝塞尔曲线路径生成(
d属性字符串),而非整个节点对象。 - 依赖数组必须是primitive值:用
node.id代替node对象,用connections.length代替connections数组。 - 添加防抖层:对画布缩放/平移等高频事件,用
requestIdleCallback延迟更新:
const updateCanvas = useCallback(() => { // 仅在空闲时段执行重绘 requestIdleCallback(() => { // 这里执行useMemo计算、SVG重绘等 const path = generateBezierPath(start, end); setCachedPath(path); }); }, [start, end]); useEffect(() => { window.addEventListener('resize', updateCanvas); return () => window.removeEventListener('resize', updateCanvas); }, [updateCanvas]);实测下来,画布帧率从20fps提升到58fps,且react native 启动白屏问题消失——因为白屏本质是JS线程被useMemo阻塞,无法及时渲染首屏。
4.3 Token用量监控:用localStorage事件实现跨Tab同步
token用量监控常被忽略,但它是安全审计关键。Caveman方案不用第三方SDK,用原生storage事件:
// 在登录成功后记录用量 const login = async () => { const res = await fetch('/api/login', { body: credentials }); const data = await res.json(); // 记录本次token创建时间 const now = Date.now(); localStorage.setItem('token_created_at', now.toString()); localStorage.setItem('token_usage_count', '1'); // 每次API调用前递增计数 const incrementUsage = () => { const count = parseInt(localStorage.getItem('token_usage_count') || '0', 10) + 1; localStorage.setItem('token_usage_count', count.toString()); // 超过阈值报警 if (count > 1000) { console.warn(`Token usage exceeded 1000 calls: ${count}`); // 可以上报到监控系统 } }; // 监听其他tab的usage变更 window.addEventListener('storage', (e) => { if (e.key === 'token_usage_count') { console.log('Token usage updated by another tab:', e.newValue); } }); };storage事件天然跨Tab,无需WebSocket或SharedWorker。当qoder cn的 1 credits等于多少token这类计费问题出现时,你能直接查localStorage看到精确用量。
4.4 最致命的坑:cookie和session和token详解中的认知陷阱
很多开发者认为“cookie更安全”,于是把token存cookie,结果遇到token exchange failed: token endpoint returned status 403 forbidden: country。真相是:cookie的SameSite策略在跨域认证中是双刃剑。
Caveman模式强制所有token走localStorage,并手动添加Authorizationheader:
// ❌ 不要用cookie自动携带 // fetch('/api/data') // cookie自动发送,但可能被SameSite=Lax拦截 // ✅ 显式header,完全可控 fetch('/api/data', { headers: { 'Authorization': `Bearer ${token}` } });这样做的好处:
- 绕过浏览器cookie策略,
country限制失效(因为请求头由JS控制,非浏览器自动行为) - token泄露风险可控(localStorage XSS风险可通过CSP缓解,而cookie的HttpOnly无法用于前端读取token)
- 调试直观:
console.log(token)直接看到值,不用查Application面板
我们曾修复一个hass g10s token相关问题,根源就是服务端设置了Set-Cookie: SameSite=Strict,导致跨域请求cookie不发送,而前端代码却假设cookie一定存在。改用localStorage后,问题消失。
5. Caveman模式的边界与演进:何时该放下石斧,拿起青铜工具
Caveman模式不是银弹,它的适用边界非常清晰。我见过太多团队盲目推行,结果开发效率暴跌。关键判断标准就一条:当你的应用核心痛点是“确定性缺失”而非“开发速度慢”时,caveman才值得投入。
比如,如果你的项目是:
- ✅ 金融交易系统(token失效必须秒级响应,不能容忍任何竞态)
- ✅ 医疗设备远程控制(网络不稳定,状态同步必须100%可预测)
- ✅ 离线优先PWA(localStorage是唯一可靠存储,必须手动管理)
那么caveman是救命稻草。但如果是:
- ❌ 内部CMS后台(功能迭代快,状态逻辑简单)
- ❌ A/B测试平台(实验配置频繁变更,需要快速响应)
- ❌ 社交媒体Feed(数据一致性要求低,更看重渲染性能)
这时强行caveman只会增加维护成本。我们的经验是:用caveman守住认证、支付、权限等核心链路,其他模块用Zustand或Jotai等轻量库。比如在flowork画布中,节点拖拽用caveman状态机保证坐标精度,而侧边栏配置用Zustand简化开发。
未来演进方向,我们正在探索“caveman+”模式:在caveman骨架上,谨慎引入TypeScript类型守卫和Zod Schema验证。例如token解析:
import { z } from 'zod'; const JwtPayloadSchema = z.object({ sub: z.string(), scope: z.string().optional(), exp: z.number() }); type JwtPayload = z.infer<typeof JwtPayloadSchema>; // caveman的useMemo内做zod.parse,失败则返回null const tokenPayload = useMemo<JwtPayload | null>(() => { if (!token) return null; try { const payload = JSON.parse(atob(token.split('.')[1])); return JwtPayloadSchema.parse(payload); } catch (e) { return null; } }, [token]);Zod的parse是纯函数,无副作用,符合caveman哲学,同时提供了比手动try/catch更强的类型安全。这就像远古人类发明了带燧石刃的木矛——工具升级了,但狩猎的逻辑没变。
最后分享一个小技巧:在团队推行caveman模式时,不要讲“最佳实践”,而是带大家复现一个真实bug。比如,一起调试sign-in failed: login server error: token exchange failed: error sending req,用Chrome DevTools的Network标签页抓包,看请求头是否缺失Authorization,再查React DevTools的Hooks面板,看token状态是否为null。当大家亲眼看到“依赖数组漏写了token”导致状态不同步时,caveman的价值就不言而喻了。毕竟,所有技术选择,都应该始于一个具体的、让人头疼的错误。