news 2026/9/17 12:38:06

Vike react-full 示例深度解析:手动集成 React 实现客户端路由、数据获取与 HTML 流式渲染

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vike react-full 示例深度解析:手动集成 React 实现客户端路由、数据获取与 HTML 流式渲染

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/vike

Vike 官方仓库中的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 git@github.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)、vitereact/react-dom@vitejs/plugin-react-swc(React 编译插件)、@mdx-js/rollup(Markdown/MDX 支持)以及react-streaming(React 流式渲染辅助库)。

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.ts(clientRouting: true
数据获取(服务端 + 同构)pages/star-wars/index/+data.ts、renderer/+config.ts(dataIsomorph自定义设置)
预渲染 +onBeforePrerenderStart()pages/hello/+onBeforePrerenderStart.ts
Route Functionpages/hello/+route.ts
TypeScript全项目.ts/.tsx,另有 renderer/PageContext.ts 类型声明
Markdownpages/markdown/+Page.mdx
+client.tspages/markdown/+client.ts
Error Pagepages/_error/+Page.tsx
Active Linksrenderer/Link.tsx
任意组件访问pageContextrenderer/usePageContext.tsx
HTML 流式渲染renderer/+onRenderHtml.tsx
页面切换加载动画renderer/+onPageTransitionStart.ts、renderer/+onPageTransitionEnd.ts

页面组织上,pages/目录采用 Vike 的文件系统路由约定:index/hello/markdown/star-wars/@id/(动态段)、_error/(错误页)等子目录各包含若干+xxx约定文件。renderer/目录则存放与页面无关的全局渲染代码:Layout.tsxLink.tsx+onRenderHtml.tsx+onRenderClient.tsxPageContext.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

几个关键点:

  1. prerender: true+clientRouting: true组合。构建时所有页面被静态渲染为 HTML(SSG),运行时客户端导航不再请求服务器(SPA 式跳转)。README 特性列表中的“Pre-rendering”与“Client Routing”正是由这两个配置驱动的。
  2. 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 服务器。
  3. hooksTimeout.datadata()钩子设置超时:10 秒打印警告、30 秒抛出错误,用于暴露过慢的数据获取。
  4. 文件底部通过declare global { namespace Vike { interface Config { title?: string } } }为自定义设置title补充 TypeScript 类型,这是 Vike 自定义设置与 TS 联动的标准做法。

手动集成 React:onRenderHtml 与 onRenderClient

Vike 本身是渲染无关(renderer-agnostic)的框架,vike-react只是把下面的两个钩子封装成了开箱即用的实现。本示例手动编写它们,恰好展示了集成的本质。

服务端renderer/+onRenderHtml.tsx:

const 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> <head><title>${title}</title></head> <body><div id="root">${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,走createRoot+renderroot变量被闭包缓存,保证多次客户端导航复用同一个 ReactDOM root,这正是客户端路由不刷新整页的关键。

数据获取:服务端 data() 与同构 data()

服务端获取以 pages/star-wars/index/+data.ts 为代表:

export { data } export type Data = Awaited<ReturnType<typeof data>> async function data() { await sleep(700) // Simulate slow network const movies = await getStarWarsMovies() return { movies: filterMoviesData(movies), // 只传必要字段,减小网络传输量 title: getTitle(movies), // 页面 <title> } }

注释中明确了两点工程考量:data的返回值会被传给客户端,因此应当裁剪掉用不到的字段;Data类型通过Awaited<ReturnType<typeof data>>自动推导,使组件里读取data时获得完整类型。

同构获取则由 pages/star-wars/@id/+dataIsomorph.ts 配合前述meta配置实现:该页面把自定义设置dataIsomorph声明为true,于是+data.tsx在服务端和浏览器端都会执行。效果是:SSR 首屏时数据来自服务端;而此后用户在客户端导航到/star-wars/@id时,数据直接由浏览器发起请求,服务器完全不参与。

页面组件读取数据使用 renderer/useData.tsx,它只是从pageContext.data中取出值并做泛型断言,一行核心代码:

function useData<Data>() { const { data } = usePageContext() return data as Data }

Route Function 与路由保护(guard)

pages/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函数,适合做需要异步校验(如鉴权)的逻辑。

预渲染与 onBeforePrerenderStart

+config.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.createContext<PageContext>(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 Links:renderer/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读取is404abortReasonguard()抛出的render(401, 'This page is forbidden.')会把消息写进abortReason),并居中展示,未提供abortReason时按 404/500 给出默认文案。

Markdown:pages/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.svg);
  • renderer/+onPageTransitionEnd.ts:新页面渲染完成时移除该状态,结束动画。

另外 renderer/+onHydrationEnd.ts 在首次水合完成后打印“Hydration finished; page is now interactive.”,可用于在水合真正完成后再触发依赖完整 DOM 的客户端逻辑。

小结:从 react-full 示例能学到什么

examples/react-full用约 40 个源文件完整走了一遍“不依赖 vike-react、手动集成 React”的全部环节,其结构与实现可以直接作为参照清单:

  1. 插件最小集vike()+@mdx-js/rollup+@vitejs/plugin-react-swc(见 vite.config.ts);
  2. 渲染管线onRenderHtml拼 HTML(escapeInject防注入)+onRenderClientisHydration分支水合/客户端渲染;
  3. 数据流data()服务端获取、dataIsomorph同构获取、passToClient+ stream-end 异步数据三种通道(见 renderer/+config.ts);
  4. 路由层:文件系统路由 + Route Function +guard()保护 +onBeforePrerenderStart预渲染 URL 列表;
  5. 组件层:React Context 注入pageContextuseData/usePageContext取数、基于urlPathname的激活链接;
  6. 生命周期+client.tsonPageTransitionStart/EndonHydrationEnd各管一段客户端行为。

再次强调 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 12:37:32

MNN 模型可视化实战:看清 .mnn 结构、导出图片与调试一步到位

MNN 模型可视化实战&#xff1a;看清 .mnn 结构、导出图片与调试一步到位 【免费下载链接】MNN MNN: A blazing-fast, lightweight inference engine battle-tested by Alibaba, powering high-performance on-device LLMs and Edge AI. 项目地址: https://gitcode.com/GitHu…

作者头像 李华
网站建设 2026/9/17 12:37:26

Navicat连接SQL Server报错08001:从ODBC到TCP/IP的排查指南

1. 08001到底是谁在报错&#xff0c;先把这个搞明白很多人第一次看到[08001]这个错误码&#xff0c;下意识以为是 Navicat Premium 自身出了问题&#xff0c;于是卸载重装、换版本、找注册机折腾一圈&#xff0c;最后发现毫无用处。这里先给结论&#xff1a;08001不是 Navicat …

作者头像 李华
网站建设 2026/9/17 12:36:58

CSS字体样式全攻略:从基础避坑到渐变描边与实战速查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 12:36:58

Java Web开发入门:Servlet环境搭建与实战指南

1. Java Web开发入门&#xff1a;环境搭建与第一个Servlet程序刚接触Java Web开发时&#xff0c;很多新手会被各种概念和配置搞得晕头转向。作为一个从零开始摸爬滚打多年的开发者&#xff0c;我想分享一套经过实战验证的入门路径。第一天我们不需要急着学习框架&#xff0c;而…

作者头像 李华
网站建设 2026/9/17 12:33:47

DeepSeek 连上 TaoToken 后,一套 Key 接入全部软件

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华