- 帖子快照携带作者主页资料与统计(authorProfile/authorStats), SSR 端与客户端预热均取 /api/users/:id;PostAuthorCard 首帧直接渲染, 不再挂载后请求,统计数字不再显示 — 占位 - MainLayout 首帧从已 seed 的帖子快照派生右栏 outline(作者卡+文章目录), 修复 outline 仅靠 effect 上报导致 SSR 不渲染右栏的问题 - 壳层首屏请求门控:useAuth 有种子跳过 /api/me、branding 缓存命中不重拉、 侧栏三个区块首屏用 seed 缓存、未读数首屏跳过即时请求(轮询/事件刷新保留) - serverDom(linkedom) 修复:DOMParser 片段包裹、属性顺序、SVG 自闭合展开 - 验证:首页与帖子页 hydration 后仅剩 pageview 上报,0 hydration 错误
75 lines
2.2 KiB
TypeScript
75 lines
2.2 KiB
TypeScript
import { createContext, useContext, useEffect, useState, useCallback, useRef, ReactNode } from 'react';
|
||
import { api } from '../api/client';
|
||
import type { User } from '../api/types';
|
||
import { clearAllFeedCache } from '../utils/feedCache';
|
||
import { clearSessionSnapshots } from '../utils/sessionPageCache';
|
||
import { peekAuthSeed } from '../utils/authBoot';
|
||
|
||
interface AuthCtx {
|
||
user: User | null;
|
||
loading: boolean;
|
||
refresh: () => Promise<void>;
|
||
logout: () => Promise<void>;
|
||
}
|
||
|
||
const AuthContext = createContext<AuthCtx>({
|
||
user: null, loading: true,
|
||
refresh: async () => {}, logout: async () => {},
|
||
});
|
||
|
||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||
// peek 不清种子,StrictMode 重挂 / initializer 双调仍与 SSR 同构
|
||
const [user, setUser] = useState<User | null>(() => {
|
||
const s = peekAuthSeed();
|
||
return s.hasSeed ? s.user : null;
|
||
});
|
||
const [loading, setLoading] = useState(() => !peekAuthSeed().hasSeed);
|
||
|
||
const refresh = useCallback(async () => {
|
||
try {
|
||
const data = await api.me();
|
||
setUser(data.user ?? null);
|
||
} catch {
|
||
setUser(null);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
// SSR +data / 冷启动预热已注入登录态种子(含已确认访客 null)时首屏不重复请求;
|
||
// 无种子(纯 SPA 且未预热)才首屏拉取。登录/退出均显式更新状态,不依赖 mount 校验。
|
||
// 不清空 auth 种子:StrictMode 重挂会再次跑 useState initializer,破坏性 clear 会导致 user 空窗闪动
|
||
useEffect(() => {
|
||
if (peekAuthSeed().hasSeed) return;
|
||
void refresh();
|
||
}, [refresh]);
|
||
|
||
const prevUserId = useRef<number | null | 'init'>('init');
|
||
useEffect(() => {
|
||
if (loading) return;
|
||
const id = user?.id ?? null;
|
||
if (prevUserId.current === 'init') {
|
||
prevUserId.current = id;
|
||
return;
|
||
}
|
||
if (prevUserId.current !== id) {
|
||
prevUserId.current = id;
|
||
clearSessionSnapshots();
|
||
clearAllFeedCache();
|
||
}
|
||
}, [user, loading]);
|
||
|
||
const logout = async () => {
|
||
await api.logout();
|
||
setUser(null);
|
||
};
|
||
|
||
return (
|
||
<AuthContext.Provider value={{ user, loading, refresh, logout }}>
|
||
{children}
|
||
</AuthContext.Provider>
|
||
);
|
||
}
|
||
|
||
export const useAuth = () => useContext(AuthContext);
|