feat: Vike 骨架(pages feed+spa / renderer 双端入口 / AppShell 拆分 / serverClient)
vite.config 接入 vike 插件并改产出 dist/client+dist/server;feed 白名单 route 匹配 /、/board/:id、/post/:id(.html);spa catch-all ssr:false;onRenderHtml renderToReadableStream 等 allReady 输出完整 HTML 并带主题/缩放脚本;onRenderClient 沿用样式表等待+冷启动预热+hydrate 回退;getHomeStoreState 作用域 + hydrating 变体入 RequestScope;删除 index.html 与 main.tsx
This commit is contained in:
@@ -1,114 +1,29 @@
|
||||
import { Suspense } from 'react';
|
||||
import {
|
||||
createBrowserRouter,
|
||||
createRoutesFromElements,
|
||||
RouterProvider,
|
||||
Route,
|
||||
Navigate,
|
||||
} from 'react-router';
|
||||
import './styles/global.css';
|
||||
import { AuthProvider } from './hooks/useAuth';
|
||||
import { ThemeProvider } from './hooks/useTheme';
|
||||
import MainLayout from './layouts/MainLayout';
|
||||
import AdminLayout from './layouts/AdminLayout';
|
||||
import ErrorBoundary from './components/ErrorBoundary';
|
||||
import AppRouteError from './components/AppRouteError';
|
||||
import PageLoader from './components/PageLoader';
|
||||
import AuthPageFallback from './components/AuthPageFallback';
|
||||
import { Toaster } from './components/ui/sonner';
|
||||
import PullToRefresh from './components/PullToRefresh';
|
||||
import TopProgressBar from './components/TopProgressBar';
|
||||
import { lazyWithRetry } from './utils/lazyWithRetry';
|
||||
import { RouterProvider, createBrowserRouter, createMemoryRouter } from 'react-router';
|
||||
import AppShell from './AppShell';
|
||||
import { appRoutes } from './AppRoutes';
|
||||
|
||||
const HomePage = lazyWithRetry(() => import('./pages/HomePage'));
|
||||
const PostDetailPage = lazyWithRetry(() => import('./pages/PostDetailPage'));
|
||||
const LoginPage = lazyWithRetry(() => import('./pages/LoginPage'));
|
||||
const RegisterPage = lazyWithRetry(() => import('./pages/RegisterPage'));
|
||||
const ForgotPasswordPage = lazyWithRetry(() => import('./pages/ForgotPasswordPage'));
|
||||
const ComposePage = lazyWithRetry(() => import('./pages/ComposePage'));
|
||||
const BoardsManagePage = lazyWithRetry(() => import('./pages/BoardsManagePage'));
|
||||
const ProfilePage = lazyWithRetry(() => import('./pages/ProfilePage'));
|
||||
const UserProfilePage = lazyWithRetry(() => import('./pages/UserProfilePage'));
|
||||
const FavoritesPage = lazyWithRetry(() => import('./pages/FavoritesPage'));
|
||||
const MessagesPage = lazyWithRetry(() => import('./pages/MessagesPage'));
|
||||
const ProjectsPage = lazyWithRetry(() => import('./pages/ProjectsPage'));
|
||||
const LinksPage = lazyWithRetry(() => import('./pages/LinksPage'));
|
||||
const ShowcasePage = lazyWithRetry(() => import('./pages/ShowcasePage'));
|
||||
const AdminDashboardPage = lazyWithRetry(() => import('./pages/admin/AdminDashboardPage'));
|
||||
const AdminPostsPage = lazyWithRetry(() => import('./pages/admin/AdminPostsPage'));
|
||||
const AdminCommentsPage = lazyWithRetry(() => import('./pages/admin/AdminCommentsPage'));
|
||||
const AdminReportsPage = lazyWithRetry(() => import('./pages/admin/AdminReportsPage'));
|
||||
const AdminUsersPage = lazyWithRetry(() => import('./pages/admin/AdminUsersPage'));
|
||||
const AdminBadgesPage = lazyWithRetry(() => import('./pages/admin/AdminBadgesPage'));
|
||||
const AdminMediaPage = lazyWithRetry(() => import('./pages/admin/AdminMediaPage'));
|
||||
const AdminMonitorPage = lazyWithRetry(() => import('./pages/admin/AdminMonitorPage'));
|
||||
const AdminPagesPage = lazyWithRetry(() => import('./pages/admin/AdminPagesPage'));
|
||||
const AdminSitePageEditPage = lazyWithRetry(() => import('./pages/admin/AdminSitePageEditPage'));
|
||||
const AdminLinksPage = lazyWithRetry(() => import('./pages/admin/AdminLinksPage'));
|
||||
const SitePageView = lazyWithRetry(() => import('./pages/SitePageView'));
|
||||
const AdminSettingsPage = lazyWithRetry(() => import('./pages/admin/AdminSettingsPage'));
|
||||
const AdminCommunityPage = lazyWithRetry(() => import('./pages/admin/AdminCommunityPage'));
|
||||
const NotFoundPage = lazyWithRetry(() => import('./pages/NotFoundPage'));
|
||||
type AppRouter = ReturnType<typeof createMemoryRouter>;
|
||||
|
||||
const router = createBrowserRouter(
|
||||
createRoutesFromElements(
|
||||
<Route errorElement={<AppRouteError />}>
|
||||
<Route path="/login" element={<Suspense fallback={<AuthPageFallback />}><LoginPage /></Suspense>} />
|
||||
<Route path="/register" element={<Suspense fallback={<AuthPageFallback />}><RegisterPage /></Suspense>} />
|
||||
<Route path="/forgot-password" element={<Suspense fallback={<AuthPageFallback />}><ForgotPasswordPage /></Suspense>} />
|
||||
<Route path="/boards" element={<Navigate to="/admin/boards" replace />} />
|
||||
<Route path="/admin" element={<AdminLayout />}>
|
||||
<Route index element={<Navigate to="/admin/dashboard" replace />} />
|
||||
<Route path="dashboard" element={<Suspense fallback={<PageLoader />}><AdminDashboardPage /></Suspense>} />
|
||||
<Route path="boards" element={<Suspense fallback={<PageLoader />}><BoardsManagePage /></Suspense>} />
|
||||
<Route path="pages/new" element={<Suspense fallback={<PageLoader />}><AdminSitePageEditPage /></Suspense>} />
|
||||
<Route path="pages/:id/edit" element={<Suspense fallback={<PageLoader />}><AdminSitePageEditPage /></Suspense>} />
|
||||
<Route path="pages" element={<Suspense fallback={<PageLoader />}><AdminPagesPage /></Suspense>} />
|
||||
<Route path="links" element={<Suspense fallback={<PageLoader />}><AdminLinksPage /></Suspense>} />
|
||||
<Route path="community" element={<Suspense fallback={<PageLoader />}><AdminCommunityPage /></Suspense>} />
|
||||
<Route path="posts" element={<Suspense fallback={<PageLoader />}><AdminPostsPage /></Suspense>} />
|
||||
<Route path="comments" element={<Suspense fallback={<PageLoader />}><AdminCommentsPage /></Suspense>} />
|
||||
<Route path="reports" element={<Suspense fallback={<PageLoader />}><AdminReportsPage /></Suspense>} />
|
||||
<Route path="users" element={<Suspense fallback={<PageLoader />}><AdminUsersPage /></Suspense>} />
|
||||
<Route path="badges" element={<Suspense fallback={<PageLoader />}><AdminBadgesPage /></Suspense>} />
|
||||
<Route path="media" element={<Suspense fallback={<PageLoader />}><AdminMediaPage /></Suspense>} />
|
||||
<Route path="monitor" element={<Suspense fallback={<PageLoader />}><AdminMonitorPage /></Suspense>} />
|
||||
<Route path="settings" element={<Suspense fallback={<PageLoader />}><AdminSettingsPage /></Suspense>} />
|
||||
<Route path="*" element={<Suspense fallback={<PageLoader />}><NotFoundPage title="后台页面不存在" /></Suspense>} />
|
||||
</Route>
|
||||
<Route element={<MainLayout />}>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/board/:id" element={<HomePage />} />
|
||||
{/* :id 可为 123 或 123.html(伪静态后缀由后台配置) */}
|
||||
<Route path="/post/:id" element={<PostDetailPage />} />
|
||||
<Route path="/compose" element={<ComposePage />} />
|
||||
<Route path="/post/:id/edit" element={<ComposePage />} />
|
||||
<Route path="/profile" element={<ProfilePage />} />
|
||||
<Route path="/user/:id" element={<UserProfilePage />} />
|
||||
<Route path="/favorites" element={<FavoritesPage />} />
|
||||
<Route path="/projects" element={<ProjectsPage />} />
|
||||
<Route path="/links" element={<LinksPage />} />
|
||||
<Route path="/showcase" element={<Suspense fallback={null}><ShowcasePage /></Suspense>} />
|
||||
<Route path="/messages" element={<MessagesPage />} />
|
||||
<Route path="/page/:slug" element={<Suspense fallback={null}><SitePageView /></Suspense>} />
|
||||
<Route path="*" element={<Suspense fallback={null}><NotFoundPage /></Suspense>} />
|
||||
</Route>
|
||||
<Route path="*" element={<Suspense fallback={<PageLoader fullScreen />}><NotFoundPage standalone /></Suspense>} />
|
||||
</Route>,
|
||||
),
|
||||
);
|
||||
/** 浏览器路由单例:整页加载创建一次,此后站内导航全部由 react-router 接管 */
|
||||
let browserRouter: AppRouter | null = null;
|
||||
|
||||
export default function App() {
|
||||
function getAppRouter(url?: string): AppRouter {
|
||||
if (import.meta.env.SSR) {
|
||||
// 服务端:每请求新建 memory router(与浏览器同一棵路由树,保证 DOM 同构)
|
||||
return createMemoryRouter(appRoutes, { initialEntries: [url ?? '/'] });
|
||||
}
|
||||
browserRouter ??= createBrowserRouter(appRoutes);
|
||||
return browserRouter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用根组件:Vike +Page 与客户端入口共用。
|
||||
* SSR 时传入 `url`(pathname + search),客户端忽略。
|
||||
*/
|
||||
export default function App({ url }: { url?: string }) {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<ErrorBoundary>
|
||||
<RouterProvider router={router} />
|
||||
<TopProgressBar />
|
||||
<PullToRefresh />
|
||||
<Toaster />
|
||||
</ErrorBoundary>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
<AppShell>
|
||||
<RouterProvider router={getAppRouter(url)} />
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
|
||||
85
frontend/src/AppRoutes.tsx
Normal file
85
frontend/src/AppRoutes.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { Suspense } from 'react';
|
||||
import { createRoutesFromElements, Route, Navigate } from 'react-router';
|
||||
import MainLayout from './layouts/MainLayout';
|
||||
import AdminLayout from './layouts/AdminLayout';
|
||||
import AppRouteError from './components/AppRouteError';
|
||||
import PageLoader from './components/PageLoader';
|
||||
import AuthPageFallback from './components/AuthPageFallback';
|
||||
import { lazyWithRetry } from './utils/lazyWithRetry';
|
||||
|
||||
const HomePage = lazyWithRetry(() => import('./pages/HomePage'));
|
||||
const PostDetailPage = lazyWithRetry(() => import('./pages/PostDetailPage'));
|
||||
const LoginPage = lazyWithRetry(() => import('./pages/LoginPage'));
|
||||
const RegisterPage = lazyWithRetry(() => import('./pages/RegisterPage'));
|
||||
const ForgotPasswordPage = lazyWithRetry(() => import('./pages/ForgotPasswordPage'));
|
||||
const ComposePage = lazyWithRetry(() => import('./pages/ComposePage'));
|
||||
const BoardsManagePage = lazyWithRetry(() => import('./pages/BoardsManagePage'));
|
||||
const ProfilePage = lazyWithRetry(() => import('./pages/ProfilePage'));
|
||||
const UserProfilePage = lazyWithRetry(() => import('./pages/UserProfilePage'));
|
||||
const FavoritesPage = lazyWithRetry(() => import('./pages/FavoritesPage'));
|
||||
const MessagesPage = lazyWithRetry(() => import('./pages/MessagesPage'));
|
||||
const ProjectsPage = lazyWithRetry(() => import('./pages/ProjectsPage'));
|
||||
const LinksPage = lazyWithRetry(() => import('./pages/LinksPage'));
|
||||
const ShowcasePage = lazyWithRetry(() => import('./pages/ShowcasePage'));
|
||||
const AdminDashboardPage = lazyWithRetry(() => import('./pages/admin/AdminDashboardPage'));
|
||||
const AdminPostsPage = lazyWithRetry(() => import('./pages/admin/AdminPostsPage'));
|
||||
const AdminCommentsPage = lazyWithRetry(() => import('./pages/admin/AdminCommentsPage'));
|
||||
const AdminReportsPage = lazyWithRetry(() => import('./pages/admin/AdminReportsPage'));
|
||||
const AdminUsersPage = lazyWithRetry(() => import('./pages/admin/AdminUsersPage'));
|
||||
const AdminBadgesPage = lazyWithRetry(() => import('./pages/admin/AdminBadgesPage'));
|
||||
const AdminMediaPage = lazyWithRetry(() => import('./pages/admin/AdminMediaPage'));
|
||||
const AdminMonitorPage = lazyWithRetry(() => import('./pages/admin/AdminMonitorPage'));
|
||||
const AdminPagesPage = lazyWithRetry(() => import('./pages/admin/AdminPagesPage'));
|
||||
const AdminSitePageEditPage = lazyWithRetry(() => import('./pages/admin/AdminSitePageEditPage'));
|
||||
const AdminLinksPage = lazyWithRetry(() => import('./pages/admin/AdminLinksPage'));
|
||||
const SitePageView = lazyWithRetry(() => import('./pages/SitePageView'));
|
||||
const AdminSettingsPage = lazyWithRetry(() => import('./pages/admin/AdminSettingsPage'));
|
||||
const AdminCommunityPage = lazyWithRetry(() => import('./pages/admin/AdminCommunityPage'));
|
||||
const NotFoundPage = lazyWithRetry(() => import('./pages/NotFoundPage'));
|
||||
|
||||
/** 全量 react-router 路由树(Vike 页面壳与浏览器共用同一棵树) */
|
||||
export const appRoutes = createRoutesFromElements(
|
||||
<Route errorElement={<AppRouteError />}>
|
||||
<Route path="/login" element={<Suspense fallback={<AuthPageFallback />}><LoginPage /></Suspense>} />
|
||||
<Route path="/register" element={<Suspense fallback={<AuthPageFallback />}><RegisterPage /></Suspense>} />
|
||||
<Route path="/forgot-password" element={<Suspense fallback={<AuthPageFallback />}><ForgotPasswordPage /></Suspense>} />
|
||||
<Route path="/boards" element={<Navigate to="/admin/boards" replace />} />
|
||||
<Route path="/admin" element={<AdminLayout />}>
|
||||
<Route index element={<Navigate to="/admin/dashboard" replace />} />
|
||||
<Route path="dashboard" element={<Suspense fallback={<PageLoader />}><AdminDashboardPage /></Suspense>} />
|
||||
<Route path="boards" element={<Suspense fallback={<PageLoader />}><BoardsManagePage /></Suspense>} />
|
||||
<Route path="pages/new" element={<Suspense fallback={<PageLoader />}><AdminSitePageEditPage /></Suspense>} />
|
||||
<Route path="pages/:id/edit" element={<Suspense fallback={<PageLoader />}><AdminSitePageEditPage /></Suspense>} />
|
||||
<Route path="pages" element={<Suspense fallback={<PageLoader />}><AdminPagesPage /></Suspense>} />
|
||||
<Route path="links" element={<Suspense fallback={<PageLoader />}><AdminLinksPage /></Suspense>} />
|
||||
<Route path="community" element={<Suspense fallback={<PageLoader />}><AdminCommunityPage /></Suspense>} />
|
||||
<Route path="posts" element={<Suspense fallback={<PageLoader />}><AdminPostsPage /></Suspense>} />
|
||||
<Route path="comments" element={<Suspense fallback={<PageLoader />}><AdminCommentsPage /></Suspense>} />
|
||||
<Route path="reports" element={<Suspense fallback={<PageLoader />}><AdminReportsPage /></Suspense>} />
|
||||
<Route path="users" element={<Suspense fallback={<PageLoader />}><AdminUsersPage /></Suspense>} />
|
||||
<Route path="badges" element={<Suspense fallback={<PageLoader />}><AdminBadgesPage /></Suspense>} />
|
||||
<Route path="media" element={<Suspense fallback={<PageLoader />}><AdminMediaPage /></Suspense>} />
|
||||
<Route path="monitor" element={<Suspense fallback={<PageLoader />}><AdminMonitorPage /></Suspense>} />
|
||||
<Route path="settings" element={<Suspense fallback={<PageLoader />}><AdminSettingsPage /></Suspense>} />
|
||||
<Route path="*" element={<Suspense fallback={<PageLoader />}><NotFoundPage title="后台页面不存在" /></Suspense>} />
|
||||
</Route>
|
||||
<Route element={<MainLayout />}>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/board/:id" element={<HomePage />} />
|
||||
{/* :id 可为 123 或 123.html(伪静态后缀由后台配置) */}
|
||||
<Route path="/post/:id" element={<PostDetailPage />} />
|
||||
<Route path="/compose" element={<ComposePage />} />
|
||||
<Route path="/post/:id/edit" element={<ComposePage />} />
|
||||
<Route path="/profile" element={<ProfilePage />} />
|
||||
<Route path="/user/:id" element={<UserProfilePage />} />
|
||||
<Route path="/favorites" element={<FavoritesPage />} />
|
||||
<Route path="/projects" element={<ProjectsPage />} />
|
||||
<Route path="/links" element={<LinksPage />} />
|
||||
<Route path="/showcase" element={<Suspense fallback={null}><ShowcasePage /></Suspense>} />
|
||||
<Route path="/messages" element={<MessagesPage />} />
|
||||
<Route path="/page/:slug" element={<Suspense fallback={null}><SitePageView /></Suspense>} />
|
||||
<Route path="*" element={<Suspense fallback={null}><NotFoundPage /></Suspense>} />
|
||||
</Route>
|
||||
<Route path="*" element={<Suspense fallback={<PageLoader fullScreen />}><NotFoundPage standalone /></Suspense>} />
|
||||
</Route>,
|
||||
);
|
||||
24
frontend/src/AppShell.tsx
Normal file
24
frontend/src/AppShell.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import './styles/global.css';
|
||||
import { AuthProvider } from './hooks/useAuth';
|
||||
import { ThemeProvider } from './hooks/useTheme';
|
||||
import ErrorBoundary from './components/ErrorBoundary';
|
||||
import { Toaster } from './components/ui/sonner';
|
||||
import PullToRefresh from './components/PullToRefresh';
|
||||
import TopProgressBar from './components/TopProgressBar';
|
||||
|
||||
/** 与路由无关的应用壳:Provider + 全局挂件(SSR 服务端 / 客户端共用) */
|
||||
export default function AppShell({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<ErrorBoundary>
|
||||
{children}
|
||||
<TopProgressBar />
|
||||
<PullToRefresh />
|
||||
<Toaster />
|
||||
</ErrorBoundary>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
28
frontend/src/api/serverClient.ts
Normal file
28
frontend/src/api/serverClient.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 服务端 API 客户端:Vike +data 在 Node 侧调用 Go API 用(仅 SSR 打包引用)。
|
||||
*
|
||||
* - GO_API_URL 指向 Go 服务(dev 默认与 Vite proxy 一致;生产由环境变量给 :3001)
|
||||
* - 转发浏览器 cookie 以透传登录态
|
||||
* - 5s 超时;任何失败返回 null,调用方降级为 SPA 壳,不阻断渲染
|
||||
*/
|
||||
|
||||
const GO_API_URL = process.env.GO_API_URL || `http://127.0.0.1:${process.env.VITE_API_PORT || '3000'}`;
|
||||
|
||||
export async function serverApi<T>(
|
||||
path: string,
|
||||
opts?: { cookie?: string; timeoutMs?: number },
|
||||
): Promise<T | null> {
|
||||
try {
|
||||
const res = await fetch(`${GO_API_URL}${path}`, {
|
||||
headers: {
|
||||
...(opts?.cookie ? { cookie: opts.cookie } : {}),
|
||||
},
|
||||
signal: AbortSignal.timeout(opts?.timeoutMs ?? 5000),
|
||||
cache: 'no-store',
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
return (await res.json()) as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import React from 'react';
|
||||
import { hydrateRoot, createRoot } from 'react-dom/client';
|
||||
import { applyTheme, getStoredTheme } from './utils/theme';
|
||||
import App from './App';
|
||||
import { consumeHomeBoot } from './utils/homeBoot';
|
||||
import { beginHomeHydrate } from './utils/homeHydrate';
|
||||
import { reloadForStaleChunk } from './utils/chunkLoad';
|
||||
import { ensureColdBootReady, isMainLayoutPath } from './utils/prefetchRoute';
|
||||
|
||||
applyTheme(getStoredTheme());
|
||||
consumeHomeBoot();
|
||||
|
||||
// Vite modulepreload / 动态 import 失败(发版后旧 hashed URL 404)
|
||||
window.addEventListener('vite:preloadError', (event) => {
|
||||
event.preventDefault();
|
||||
reloadForStaleChunk();
|
||||
});
|
||||
|
||||
function hasSSRHome(): boolean {
|
||||
return !!document.querySelector('#root .ssr-home');
|
||||
}
|
||||
|
||||
function waitForStylesheets(): Promise<void> {
|
||||
const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]'));
|
||||
if (links.length === 0) return Promise.resolve();
|
||||
return Promise.all(
|
||||
links.map(
|
||||
(node) =>
|
||||
new Promise<void>((resolve) => {
|
||||
const link = node as HTMLLinkElement;
|
||||
try {
|
||||
if (link.sheet) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
/* cross-origin */
|
||||
}
|
||||
const finish = () => resolve();
|
||||
link.addEventListener('load', finish, { once: true });
|
||||
link.addEventListener('error', finish, { once: true });
|
||||
}),
|
||||
),
|
||||
).then(() => undefined);
|
||||
}
|
||||
|
||||
function renderApp() {
|
||||
return (
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
function mountCreateRoot(el: HTMLElement) {
|
||||
createRoot(el).render(renderApp());
|
||||
}
|
||||
|
||||
/** 有 SSR 正文:hydrate 接管同一 DOM;失败则回退 createRoot */
|
||||
function mountHydrateHome(el: HTMLElement) {
|
||||
beginHomeHydrate();
|
||||
try {
|
||||
hydrateRoot(el, renderApp());
|
||||
} catch (err) {
|
||||
console.warn('[j13] hydrateRoot 失败,回退 createRoot', err);
|
||||
el.innerHTML = '';
|
||||
mountCreateRoot(el);
|
||||
}
|
||||
}
|
||||
|
||||
async function boot() {
|
||||
const path = `${window.location.pathname}${window.location.search}`;
|
||||
const ssr = hasSSRHome();
|
||||
|
||||
await waitForStylesheets();
|
||||
try {
|
||||
if (document.fonts?.ready) await document.fonts.ready;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
if (isMainLayoutPath(window.location.pathname)) {
|
||||
await ensureColdBootReady(path);
|
||||
}
|
||||
|
||||
const root = document.getElementById('root');
|
||||
if (!root) return;
|
||||
|
||||
if (ssr) {
|
||||
mountHydrateHome(root);
|
||||
} else {
|
||||
mountCreateRoot(root);
|
||||
}
|
||||
}
|
||||
|
||||
void boot();
|
||||
26
frontend/src/ssr/feedPaths.ts
Normal file
26
frontend/src/ssr/feedPaths.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* SSR feed 页路由匹配(Vike +route.ts 用)。
|
||||
*
|
||||
* 真渲染白名单:首页 `/`、板块页 `/board/:id`、帖子详情 `/post/:id`
|
||||
* (兼容 `123.html` 伪静态)。其余 URL 一律走 SPA 壳,后续扩展 SEO 页只改这里。
|
||||
*/
|
||||
|
||||
export type FeedSsrMatch =
|
||||
| { kind: 'home'; id: '' }
|
||||
| { kind: 'board'; id: string }
|
||||
| { kind: 'post'; id: string };
|
||||
|
||||
const FEED_PATTERNS: Array<{ re: RegExp; pick: (m: RegExpMatchArray) => FeedSsrMatch }> = [
|
||||
{ re: /^\/$/, pick: () => ({ kind: 'home', id: '' }) },
|
||||
{ re: /^\/board\/([^/]+)$/, pick: (m) => ({ kind: 'board', id: m[1] }) },
|
||||
// 123 与 123.html(伪静态后缀由后台配置,两种形态都接住)
|
||||
{ re: /^\/post\/([^/]+?)(?:\.html?)?$/, pick: (m) => ({ kind: 'post', id: m[1] }) },
|
||||
];
|
||||
|
||||
export function matchFeedSsrPath(urlPathname: string): FeedSsrMatch | null {
|
||||
for (const { re, pick } of FEED_PATTERNS) {
|
||||
const m = urlPathname.match(re);
|
||||
if (m) return pick(m);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -25,6 +25,8 @@ export type RequestScope = {
|
||||
checkIns: Map<number, CheckInStatus>;
|
||||
/** 主题(由 +data 从 cookie 解析) */
|
||||
theme: 'light' | 'dark';
|
||||
/** 本请求渲染是否采用「hydrate 首帧同构变体」(feed SSR 页恒为 true) */
|
||||
hydrating: boolean;
|
||||
};
|
||||
|
||||
export function createRequestScope(homeStore: HomeStoreState, theme: 'light' | 'dark' = 'light'): RequestScope {
|
||||
@@ -37,5 +39,6 @@ export function createRequestScope(homeStore: HomeStoreState, theme: 'light' | '
|
||||
pages: null,
|
||||
checkIns: new Map(),
|
||||
theme,
|
||||
hydrating: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/** 首页 SSR hydrate 首帧同构标志(仅 / 与板块首页) */
|
||||
|
||||
import { getScope } from '../ssr/scope';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__J13_HYDRATING_HOME__?: boolean;
|
||||
@@ -30,8 +32,10 @@ export function endHomeHydrate() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 首帧是否必须与 Go SSR DOM 同构 */
|
||||
/** 首帧是否必须与 SSR DOM 同构(SSR 渲染期读请求作用域,客户端读模块/窗口标志) */
|
||||
export function isHomeHydrating(): boolean {
|
||||
const scope = getScope();
|
||||
if (scope) return scope.hydrating === true;
|
||||
if (hydrating) return true;
|
||||
try {
|
||||
return !!window.__J13_HYDRATING_HOME__;
|
||||
|
||||
Reference in New Issue
Block a user