
Vike react-full 示例深度解析手动集成 React 实现客户端路由、数据获取与 HTML 流式渲染【免费下载链接】vike(Replaces Next.js/Nuxt) Build mission-critical applications with stability and development freedom.项目地址: https://gitcode.com/GitHub_Trending/vi/vikeVike 官方仓库中的examples/react-full是一个“全功能”示例它不依赖vike-react扩展包而是手动将 React 集成进 Vike 的页面渲染管线完整演示了客户端路由、同构数据获取、预渲染、Route Function、错误页、激活链接、HTML 流式渲染与页面切换加载动画等十余项能力。读完本文你将掌握如何在 Vike 中自行编写onRenderHtml/onRenderClient钩子完成 React 集成并通过该示例的源码理解每个特性背后的 Vike 配置与钩子调用机制。示例定位与运行方式examples/react-full/README.md对该示例的定位是手动集成 React 并展示尽可能多特性的示例Example of manually integrating React that showcases many features。README 同时给出两条重要提示创建新的 Vike 应用时官方推荐使用 Bati 脚手架而不是直接复制本示例。因为本示例使用自定义 React 集成而非通常更推荐的vike-react包如果只需要了解最小集成的样子可以改看更简单的examples/react-minimal/。因此本示例的价值在于“学习 Vike 的核心机制”而非“直接拿来当项目模板”。运行方式在 README 中已给出git clone gitgithub.com:vikejs/vike cd vike/examples/react-full/ npm install npm run dev从 package.json 可以看到三个脚本dev执行vike devbuild执行vike buildpreview执行vike build vike preview。核心依赖包括vike当前锁定 0.4.262、vite、react/react-dom、vitejs/plugin-react-swcReact 编译插件、mdx-js/rollupMarkdown/MDX 支持以及react-streamingReact 流式渲染辅助库。vite.config.ts 只有 8 行注册了三个 Vite 插件体现了“Vike 插件 MDX 插件 React 插件”的最小插件组合import react from vitejs/plugin-react-swc import mdx from mdx-js/rollup import vike from vike/plugin import type { UserConfig } from vite export default { plugins: [vike(), mdx(), react()], } satisfies UserConfig其中vike()插件负责把pages/目录下的文件约定Page、data、route等编译成路由与渲染配置这也是“文件系统路由 文件约定钩子”能够生效的基础。特性总览与文件结构README 中列出的特性清单与仓库中的文件一一对应特性对应源码文件Client Routing navigate()renderer/config.tsclientRouting: true数据获取服务端 同构pages/star-wars/index/data.ts、renderer/config.tsdataIsomorph自定义设置预渲染 onBeforePrerenderStart()pages/hello/onBeforePrerenderStart.tsRoute Functionpages/hello/route.tsTypeScript全项目.ts/.tsx另有 renderer/PageContext.ts 类型声明Markdownpages/markdown/Page.mdxclient.tspages/markdown/client.tsError Pagepages/_error/Page.tsxActive Linksrenderer/Link.tsx任意组件访问pageContextrenderer/usePageContext.tsxHTML 流式渲染renderer/onRenderHtml.tsx页面切换加载动画renderer/onPageTransitionStart.ts、renderer/onPageTransitionEnd.ts页面组织上pages/目录采用 Vike 的文件系统路由约定index/、hello/、markdown/、star-wars/id/动态段、_error/错误页等子目录各包含若干xxx约定文件。renderer/目录则存放与页面无关的全局渲染代码Layout.tsx、Link.tsx、onRenderHtml.tsx、onRenderClient.tsx、PageContext.ts等。全局配置prerender、clientRouting 与自定义设置renderer/config.ts 是整个示例的行为中枢其中每一项都值得展开export const config { prerender: true, // 构建时预渲染所有页面 passToClient: [someAsyncProps], // 白名单允许 pageContext 字段传给客户端 clientRouting: true, // 启用客户端路由SPA 式跳转 hydrationCanBeAborted: true, // 允许 hydration 中途被新导航打断 meta: { /* 定义 title 与 dataIsomorph 两个自定义设置 */ }, hooksTimeout: { data: { error: 30 * 1000, warning: 10 * 1000 } }, } satisfies Config几个关键点prerender: trueclientRouting: true组合。构建时所有页面被静态渲染为 HTMLSSG运行时客户端导航不再请求服务器SPA 式跳转。README 特性列表中的“Pre-rendering”与“Client Routing”正是由这两个配置驱动的。dataIsomorph是一个自定义 Vike 设置这是 Vikemeta机制的演示。源码中它声明env: { config: true }并通过effect校验dataIsomorph必须为布尔值当某页面把它设为true时effect返回{ meta: { data: { env: { server: true, client: true } } } }即覆盖 Vike 默认行为data()只在服务端执行让data()也在浏览器端执行。README 特性列表中的“isomorphic fetching”就靠它实现——客户端导航时数据直接从浏览器请求完全绕开 Node.js/Edge 服务器。hooksTimeout.data为data()钩子设置超时10 秒打印警告、30 秒抛出错误用于暴露过慢的数据获取。文件底部通过declare global { namespace Vike { interface Config { title?: string } } }为自定义设置title补充 TypeScript 类型这是 Vike 自定义设置与 TS 联动的标准做法。手动集成 ReactonRenderHtml 与 onRenderClientVike 本身是渲染无关renderer-agnostic的框架vike-react只是把下面的两个钩子封装成了开箱即用的实现。本示例手动编写它们恰好展示了集成的本质。服务端renderer/onRenderHtml.tsxconst onRenderHtml async (pageContext: PageContextServer) { const { Page } pageContext const stream await renderToStream( Layout pageContext{pageContext} Page / /Layout, { disable: true }, // 本示例实际关闭了流式仅为演示 Vike 可承载 react-streaming ) const title getPageTitle(pageContext) const documentHtml escapeInject!DOCTYPE html html headtitle${title}/title/head bodydiv idroot${stream}/div/body /html return { documentHtml, pageContext: async () ({ someAsyncProps: 42 }), } }两个细节值得注意HTML 骨架完全由自己拼接escapeInject是 Vike 提供的防注入模板函数Page /组件由 Vike 根据路由解析后注入pageContext。返回值中的pageContext: async () ({...})是在 HTML 流结束stream-end之后才发送到客户端的异步数据。配合config.ts中的passToClient: [someAsyncProps]这个字段才会被序列化并传给浏览器端的pageContext。源码注释说明引入react-streaming仅仅是为了演示 Vike 对“预渲染应用 react-streaming”组合的兼容性本示例实际通过{ disable: true }关闭了真正的流式输出。标题由 renderer/getPageTitle.ts 计算优先级为data()动态返回的title→ 静态自定义设置config.title→ 兜底Vike Demo。客户端renderer/onRenderClient.tsx 区分两种进入页面的路径const onRenderClient async (pageContext: PageContextClient) { const { Page } pageContext const page ( Layout pageContext{pageContext}Page //Layout ) const container document.getElementById(root)! if (pageContext.isHydration) { root ReactDOM.hydrateRoot(container, page) // 首次加载水合服务端 HTML } else { if (!root) root ReactDOM.createRoot(container) root.render(page) // 客户端导航直接客户端渲染 } document.title getPageTitle(pageContext) }pageContext.isHydration是判断依据初始页面加载时服务端已产出 HTML需要hydrateRoot水合而客户端路由跳转clientRouting: true后由 Vike 客户端运行时发起没有现成 HTML走createRootrender。root变量被闭包缓存保证多次客户端导航复用同一个 ReactDOM root这正是客户端路由不刷新整页的关键。数据获取服务端 data() 与同构 data()服务端获取以 pages/star-wars/index/data.ts 为代表export { data } export type Data AwaitedReturnTypetypeof data async function data() { await sleep(700) // Simulate slow network const movies await getStarWarsMovies() return { movies: filterMoviesData(movies), // 只传必要字段减小网络传输量 title: getTitle(movies), // 页面 title } }注释中明确了两点工程考量data的返回值会被传给客户端因此应当裁剪掉用不到的字段Data类型通过AwaitedReturnTypetypeof data自动推导使组件里读取data时获得完整类型。同构获取则由 pages/star-wars/id/dataIsomorph.ts 配合前述meta配置实现该页面把自定义设置dataIsomorph声明为true于是data.tsx在服务端和浏览器端都会执行。效果是SSR 首屏时数据来自服务端而此后用户在客户端导航到/star-wars/id时数据直接由浏览器发起请求服务器完全不参与。页面组件读取数据使用 renderer/useData.tsx它只是从pageContext.data中取出值并做泛型断言一行核心代码function useDataData() { const { data } usePageContext() return data as Data }Route Function 与路由保护guardpages/hello/route.ts 演示 Route Function——一种用函数完全接管路由解析的能力const route (pageContext: PageContextServer | PageContextClient) { if (pageContext.urlPathname /hello || pageContext.urlPathname /hello/) { const name anonymous return { routeParams: { name } } } return resolveRoute(/hello/name, pageContext.urlPathname) }它把/hello含尾斜杠映射到name anonymous其余路径交给resolveRoute按/hello/name模板解析。由于该函数同时接收PageContextServer | PageContextClient路由解析在服务端与客户端保持同构避免两端路由结果不一致。pages/hello/guard.ts 演示guard()钩子做页面保护const guard async (pageContext: PageContextServer) { if (pageContext.urlPathname /hello/forbidden) { await sleep(2 * 1000) // Unlike Route Functions, guard() can be async throw render(401, This page is forbidden.) } }访问/hello/forbidden时抛出render(401, This page is forbidden.)最终由错误页接管展示。值得注意的是源码注释guard()与 Route Function 的一个区别是它可以是async函数适合做需要异步校验如鉴权的逻辑。预渲染与 onBeforePrerenderStartconfig.ts开启prerender: true后默认只预渲染能被路由确定解析的页面对于/hello这类含动态默认值的页面pages/hello/onBeforePrerenderStart.ts 显式列出要生成的 URL 列表const onBeforePrerenderStart async () { return [/hello, ...names.map((name) /hello/${name})] }其中names来自 pages/hello/names.ts[evan, rom, alice, jon, eli]于是构建时会额外预渲染 5 个/hello/name静态页。pages/star-wars/index/onBeforePrerenderStart.ts 中有同样的用法。pageContext 的类型化与 React Context 注入Vike 的pageContext是一个可被项目类型系统增强的对象。renderer/PageContext.ts 通过全局声明扩展了它declare global { namespace Vike { interface PageContext { Page: Page // () React.ReactElement由渲染钩子消费 data?: { title?: string } config: { title?: string } abortReason?: string someAsyncProps?: number // 与 onRenderHtml 中 stream-end 数据对应 } } }“任意 React 组件都能访问pageContext”的能力则通过 renderer/usePageContext.tsx 实现——一个标准的 React Context 包装const Context React.createContextPageContext(undefined as any) function PageContextProvider({ pageContext, children }) { return Context.Provider value{pageContext}{children}/Context.Provider } function usePageContext() { return useContext(Context) }renderer/Layout.tsx 在渲染树最外层React.StrictMode内包裹PageContextProvider并传入pageContext因此任何子组件调用usePageContext()/useData()都能拿到当前页面的上下文。这是手动集成 React 时把“框架级数据”下沉到组件树的关键一步。激活链接、错误页与 Markdown 页面Active Linksrenderer/Link.tsx 用pageContext.urlPathname计算当前导航是否处于激活态const isActive href / ? urlPathname href : urlPathname.startsWith(href) const className [props.className, isActive is-active].filter(Boolean).join( ) return a {...props} className{className} /根路径要求精确匹配其余前缀匹配样式由 renderer/css/links.css 中的.is-active类控制。由于基于urlPathname激活状态在每次客户端导航后随新pageContext自动更新无需额外状态管理。Error Pagepages/_error/是 Vike 的错误页约定目录。pages/_error/Page.tsx 从pageContext读取is404与abortReasonguard()抛出的render(401, This page is forbidden.)会把消息写进abortReason并居中展示未提供abortReason时按 404/500 给出默认文案。Markdownpages/markdown/Page.mdx 是一个 MDX 文件作为页面组件的直接示例——vike()mdx-js/rollup两个插件配合后.mdx文件可以直接导出Page组件无需任何额外胶水代码。客户端钩子client.ts 与页面生命周期pages/markdown/client.ts 演示client.ts约定文件的用途——它只在浏览器端加载执行且仅当导航到该页面时触发console.log(Hello from client.ts with viewport height ${window.document.documentElement.clientHeight})这类钩子适合放埋点、仅客户端的初始化逻辑如读取window、设置定时器且不会进入服务端产物。页面切换动画由一对生命周期钩子驱动renderer/onPageTransitionStart.ts客户端导航开始时给body加上page-is-transitioning类名配合 renderer/css/page-transition-loading-animation.css 中的样式展示加载动画加载图标资源见 renderer/css/page-transition-loading-animation/loading.svgrenderer/onPageTransitionEnd.ts新页面渲染完成时移除该状态结束动画。另外 renderer/onHydrationEnd.ts 在首次水合完成后打印“Hydration finished; page is now interactive.”可用于在水合真正完成后再触发依赖完整 DOM 的客户端逻辑。小结从 react-full 示例能学到什么examples/react-full用约 40 个源文件完整走了一遍“不依赖 vike-react、手动集成 React”的全部环节其结构与实现可以直接作为参照清单插件最小集vike()mdx-js/rollupvitejs/plugin-react-swc见 vite.config.ts渲染管线onRenderHtml拼 HTMLescapeInject防注入onRenderClient按isHydration分支水合/客户端渲染数据流data()服务端获取、dataIsomorph同构获取、passToClient stream-end 异步数据三种通道见 renderer/config.ts路由层文件系统路由 Route Function guard()保护 onBeforePrerenderStart预渲染 URL 列表组件层React Context 注入pageContext、useData/usePageContext取数、基于urlPathname的激活链接生命周期client.ts、onPageTransitionStart/End、onHydrationEnd各管一段客户端行为。再次强调 README 的提醒生产项目初始化建议用官方脚手架而非复制本示例本示例的定位是让你看清vike-react这类扩展包在底层到底替你做了什么。想进一步缩小认知范围时可以对照更精简的 examples/react-minimal/README.md。【免费下载链接】vike(Replaces Next.js/Nuxt) Build mission-critical applications with stability and development freedom.项目地址: https://gitcode.com/GitHub_Trending/vi/vike创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考