diff --git a/.gitignore b/.gitignore index 5f95ef3..3c18b7f 100644 --- a/.gitignore +++ b/.gitignore @@ -27,5 +27,3 @@ tmp-cookie.txt Thumbs.db .DS_Store -# Lucide path ��ȡ��ʱ���� -frontend/tmp-lucide-paths.json diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7b28d39..73cfe88 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -21,7 +21,6 @@ "@svg-maps/china": "^2.0.0", "@svg-maps/world": "^2.0.0", "@tailwindcss/vite": "^4.3.3", - "@tanstack/react-virtual": "^3.11.2", "@tiptap/core": "^3.26.1", "@tiptap/extension-code-block": "^3.26.1", "@tiptap/extension-image": "^3.26.1", @@ -3119,33 +3118,6 @@ "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, - "node_modules/@tanstack/react-virtual": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.14.2.tgz", - "integrity": "sha512-IpWnmCLvuymRfeeLNVXIzNEYBFLpd3drVIS91sqV78VTZFyldlChkOocZRCPp1B+Wnk09bcLNme8WaMU/9/9bQ==", - "license": "MIT", - "dependencies": { - "@tanstack/virtual-core": "3.17.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@tanstack/virtual-core": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.17.0.tgz", - "integrity": "sha512-gOxY/hFkPh/XQYhnThBHzkbkX3Ed+z/iushyz+R+JAr213aXxUDgQoTgTdrDpBSRsjFM73P/KfUyWmaF9WHMkQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, "node_modules/@tiptap/core": { "version": "3.26.1", "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.26.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 00b80c9..5e5f750 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,7 +23,6 @@ "@svg-maps/china": "^2.0.0", "@svg-maps/world": "^2.0.0", "@tailwindcss/vite": "^4.3.3", - "@tanstack/react-virtual": "^3.11.2", "@tiptap/core": "^3.26.1", "@tiptap/extension-code-block": "^3.26.1", "@tiptap/extension-image": "^3.26.1", diff --git a/frontend/pages/_error/+data.ts b/frontend/pages/_error/+data.ts index 35c9a65..4ec02cc 100644 --- a/frontend/pages/_error/+data.ts +++ b/frontend/pages/_error/+data.ts @@ -1,5 +1,5 @@ import type { PageContextServer } from 'vike/types'; -import type { FeedData } from '../../src/ssr/feedData'; +import type { FeedData } from '../../src/ssr/feedSeed'; export { data }; diff --git a/frontend/renderer/+onRenderClient.tsx b/frontend/renderer/+onRenderClient.tsx index 3733ee2..f57c4d4 100644 --- a/frontend/renderer/+onRenderClient.tsx +++ b/frontend/renderer/+onRenderClient.tsx @@ -3,9 +3,10 @@ import type { ComponentType } from 'react'; import { hydrateRoot, createRoot } from 'react-dom/client'; import type { PageContextClient } from 'vike/types'; import { applyTheme, getStoredTheme } from '../src/utils/theme'; +import { installZoomGuard } from '../src/utils/zoomGuard'; import { beginHomeHydrate } from '../src/utils/homeHydrate'; import { reloadForStaleChunk } from '../src/utils/chunkLoad'; -import { seedFeedData, type FeedData } from '../src/ssr/feedData'; +import { seedFeedData, type FeedData } from '../src/ssr/feedSeed'; import { ensureColdBootReady, isMainLayoutPath } from '../src/utils/prefetchRoute'; export { onRenderClient }; @@ -40,6 +41,7 @@ function waitForStylesheets(): Promise { async function onRenderClient(pageContext: PageContextClient) { applyTheme(getStoredTheme()); + installZoomGuard(); // hydrate 前把 +data 结果 seed 进客户端缓存(与服务端渲染前 seed 同构,保证首帧一致) if (pageContext.isHydration) { seedFeedData((pageContext as { data?: FeedData }).data); @@ -54,6 +56,16 @@ async function onRenderClient(pageContext: PageContextClient) { const el = document.getElementById('root'); if (!el) return; + // 有 SSR 正文 → hydrate 接管同一 DOM(首帧数据已由 +data seed,无需冷启动预热); + // SPA 壳 / SSR 降级空壳 → createRoot 挂载,挂载前先冷启动预热,避免空白首帧 + const hydrating = pageContext.isHydration && el.innerHTML.trim() !== ''; + + // 仅非 hydrate 路径需要冷启动:SSR 页 seed 已灌入缓存,冷启动只会重复请求并阻塞 hydrate + if (!hydrating && isMainLayoutPath(window.location.pathname)) { + const path = `${window.location.pathname}${window.location.search}`; + await ensureColdBootReady(path); + } + await waitForStylesheets(); try { if (document.fonts?.ready) await document.fonts.ready; @@ -61,13 +73,6 @@ async function onRenderClient(pageContext: PageContextClient) { /* ignore */ } - const path = `${window.location.pathname}${window.location.search}`; - if (isMainLayoutPath(window.location.pathname)) { - await ensureColdBootReady(path); - } - - // 有 SSR 正文 → hydrate 接管同一 DOM(首帧 hydrate 同构变体);SPA 壳 → 整树挂载 - const hydrating = pageContext.isHydration && el.innerHTML.trim() !== ''; if (hydrating) beginHomeHydrate(); const Page = pageContext.Page as PageComponent; diff --git a/frontend/renderer/+onRenderHtml.tsx b/frontend/renderer/+onRenderHtml.tsx index 83497dc..a55f999 100644 --- a/frontend/renderer/+onRenderHtml.tsx +++ b/frontend/renderer/+onRenderHtml.tsx @@ -5,9 +5,11 @@ import type { PageContextServer } from 'vike/types'; import { createHomeStore } from '../src/store/homeStore'; import { createRequestScope } from '../src/ssr/scopeTypes'; import { runWithScope } from '../src/ssr/scope'; -import { seedFeedData, type FeedData } from '../src/ssr/feedData'; +import { seedFeedData, type FeedData } from '../src/ssr/feedSeed'; import { renderHeadTags } from '../src/ssr/head'; import { installServerDom } from '../src/ssr/serverDom'; +import { readThemeFromCookie } from '../src/utils/theme'; +import { createSsrApp } from '../src/App'; // 服务端 DOM polyfill:内容处理依赖 DOMParser/document,须在渲染前安装 installServerDom(); @@ -16,14 +18,6 @@ export { onRenderHtml }; type PageComponent = ComponentType<{ url?: string }>; -/** 首帧主题脚本(localStorage j13-theme),与旧 index.html 保持一致 */ -const THEME_INIT_SCRIPT - = "(function(){var t=localStorage.getItem('j13-theme')||'light';document.documentElement.classList.toggle('dark',t==='dark');document.documentElement.style.colorScheme=t;})();"; - -/** 锁死手机/平板双指与手势缩放;头像裁剪 / 图片灯箱放行 pinch */ -const ZOOM_BLOCK_SCRIPT - = "(function(){function inZoomAllowArea(target){return !!(target&&target.closest&&target.closest('.avatar-crop-stage, .image-lightbox'));}function blockGesture(e){if(inZoomAllowArea(e.target))return;if(e.cancelable)e.preventDefault();}function blockMultiTouch(e){if(e.touches&&e.touches.length>1){if(inZoomAllowArea(e.target))return;if(e.cancelable)e.preventDefault();}}document.addEventListener('gesturestart',blockGesture,{passive:false});document.addEventListener('gesturechange',blockGesture,{passive:false});document.addEventListener('gestureend',blockGesture,{passive:false});document.addEventListener('touchstart',blockMultiTouch,{passive:false});document.addEventListener('touchmove',blockMultiTouch,{passive:false});})();"; - /** 等 lazy 页面全部渲染完成(allReady)后缓冲为完整 HTML,首字节语义与旧 Go 文档级 SSR 一致 */ async function renderPageToString(element: ReactElement): Promise { const stream = await renderToReadableStream(element, { @@ -47,10 +41,13 @@ async function onRenderHtml(pageContext: PageContextServer) { const { pathname, searchOriginal } = pageContext.urlParsed; const url = pathname + (searchOriginal ?? ''); try { - pageHtml = await runWithScope(scope, () => { + pageHtml = await runWithScope(scope, async () => { // 渲染前把 +data 取回的数据 seed 进请求作用域(缓存层按 scope 隔离) seedFeedData(data); - return renderPageToString(); + // 官方 SSR 路径:await 首导航后 StaticRouterProvider 同步渲染(避免 RouterProvider suspend + // 导致内容落入