ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于 TanStack Router 的 Solid SSR 流式渲染实战:从文件式路由到自定义 Express 服务器

基于 TanStack Router 的 Solid SSR 流式渲染实战:从文件式路由到自定义 Express 服务器 基于 TanStack Router 的 Solid SSR 流式渲染实战从文件式路由到自定义 Express 服务器【免费下载链接】router A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router导读本篇以仓库中examples/solid/basic-ssr-streaming-file-based示例为骨架完整讲解如何在 Solid 应用中基于 TanStack Router 搭建支持SSR服务端渲染与流式响应的全栈页面包括 Vite 双构建配置客户端/服务端、文件式路由File-Based Routing、renderRouterToStream流式渲染、客户端水合Hydration以及自定义 Express 服务器与请求管道对接。读完本文你将能独立复刻一个文件式路由 SSR 流式输出 客户端交互的最小可运行应用并理解其中每一层代码的作用。一、示例总览一个麻雀虽小、五脏俱全的 SSR 应用该示例的完整源码位于 examples/solid/basic-ssr-streaming-file-based其目录结构如下examples/solid/basic-ssr-streaming-file-based/ ├── package.json # 脚本与依赖定义 ├── vite.config.js # 客户端/服务端双构建配置 ├── server.js # 自定义 Express 开发/生产服务器 ├── tsconfig.json └── src/ ├── entry-client.tsx # 客户端入口水合hydrate ├── entry-server.tsx # 服务端入口流式渲染 ├── fetch-polyfill.js # 服务端 fetch 兼容 ├── posts.tsx # 数据获取函数JSONPlaceholder ├── router.tsx # 路由器工厂createRouter ├── routerContext.tsx # 路由上下文类型定义 ├── routeTree.gen.ts # 由插件自动生成的路由树 └── routes/ # 文件式路由目录 ├── __root.tsx # 根路由head/meta/布局 ├── index.tsx # 首页含 deferred 数据 ├── counter.tsx # 计数器页面 ├── posts.tsx # 文章列表页 ├── posts.$postId.tsx # 文章详情页动态参数 └── _pathlessLayout/ # 路径无关布局Pathless Layout项目依赖见 package.json的核心成员包括tanstack/solid-routerSolid 版 TanStack Router 运行时tanstack/router-plugin提供 Vite 插件负责文件式路由扫描与routeTree.gen.ts代码生成vite-plugin-solidSolid 的 Vite 集成开启ssr: trueexpress作为自定义 SSR 服务器solid-js、tailwindcss、zod等支撑库。运行方式与原文档一致直接可复现# 开发模式Vite 中间件 HMR SSR npm install npm start # 等价于 node server # 生产构建与启动 npm run build # 依次执行 build:client 与 build:server npm run serve # NODE_ENVproduction node server其中npm run build拆分为vite build客户端产物到dist/client与vite build --ssr服务端产物到dist/server两步这是 SSR 应用的标准构建流程。二、双构建配置一份 Vite 配置服务端与客户端两端vite.config.js 是理解整个示例的关键。它基于 Vite 的isSsrBuild标志在同一份配置中切换两套构建参数const ssrBuildConfig { ssr: true, outDir: dist/server, ssrEmitAssets: true, // 服务端构建也输出静态资源 copyPublicDir: false, emptyOutDir: true, rolldownOptions: { input: path.resolve(__dirname, src/entry-server.tsx), output: { entryFileNames: static/[name].js, chunkFileNames: static/assets/[name]-[hash].js, assetFileNames: static/assets/[name]-[hash][extname], }, }, } const clientBuildConfig { outDir: dist/client, emitAssets: true, copyPublicDir: true, emptyOutDir: true, rolldownOptions: { input: path.resolve(__dirname, src/entry-client.tsx), output: { /* 同上的 static/ 目录规范 */ }, }, } export default defineConfig((configEnv) { return { plugins: [ tailwindcss(), tanstackRouter({ target: solid, autoCodeSplitting: true }), solid({ ssr: true }), ], build: configEnv.isSsrBuild ? ssrBuildConfig : clientBuildConfig, } })几个值得注意的要点插件顺序与职责tanstackRouter({ target: solid, autoCodeSplitting: true })负责扫描src/routes生成routeTree.gen.tsautoCodeSplitting: true会让路由组件按需代码分割solid({ ssr: true })通知 Vite 插件 Solid 组件需要支持服务端渲染。入口分离客户端入口是entry-client.tsx负责水合服务端入口是entry-server.tsx负责渲染两者通过input分别指定。产物目录约定服务端构建产物默认输出到dist/server/static/entry-server.js客户端产物输出到dist/client/static/entry-client.js。这个路径约定被 server.js 生产模式分支import(./dist/server/static/entry-server.js)和根路由的head脚本声明/static/entry-client.js共同引用改动任意一端都要保持同步。三、自定义 Express 服务器开发与生产的统一入口server.js 实现了一个同时兼容开发与生产模式的 Express 服务器其核心逻辑如下开发模式下服务器以middlewareMode: true创建 Vite 实例并把 Vite 的 connect 中间件挂载到 Express 上从而获得即时编译与 HMRif (!isProd) { vite await (await import(vite)).createServer({ root, logLevel: isTest ? error : info, server: { middlewareMode: true, watch: { usePolling: true, interval: 100 }, // 测试环境保证文件变更可靠 hmr: { port: hmrPort }, }, appType: custom, // 完全自定义 HTML 输出 }) app.use(vite.middlewares) }生产模式下则挂载compression含 Brotli 支持压缩中间件与express.static(./dist/client)静态资源服务。两种模式下所有路由请求统一进入app.use(/{*splat}, ...)兜底处理器Express 5 通配语法过滤带文件扩展名的无效路径path.extname(url) ! 时返回 404开发模式通过vite.transformIndexHtml提取head片段用于注入 Vite 处理后的资源引用加载服务端入口开发vite.ssrLoadModule(/src/entry-server.tsx)生产import(./dist/server/static/entry-server.js)调用entry.render({ req, res, head: viteHead })进入 TanStack Router 的渲染流程。监听端口使用get-port在 3000–3100 范围内自动分配便于并行测试。四、服务端入口从 Express 请求到流式 HTML 响应entry-server.tsx 是整个 SSR 流程的枢纽演示了如何把 Express 的请求对象转换为 Fetch API 的Request再交给 TanStack Router 处理import { pipeline } from node:stream/promises import { RouterServer, createRequestHandler, renderRouterToStream, } from tanstack/solid-router/ssr/server import { createRouter } from ./router import ./fetch-polyfill export async function render({ req, res, head }) { // 1. Express req - fetch Request const url new URL(req.originalUrl || req.url, https://localhost:3000).href const request new Request(url, { method: req.method, headers: (() { const headers new Headers() for (const [key, value] of Object.entries(req.headers)) { headers.set(key, value as any) } return headers })(), }) // 2. 创建请求处理器每次请求都新建 router 实例 const handler createRequestHandler({ request, createRouter: () { const router createRouter() router.update({ context: { ...router.options.context, head }, }) return router }, }) // 3. 使用默认流式处理器产出响应 const response await handler(({ request, responseHeaders, router }) renderRouterToStream({ request, responseHeaders, router, children: () RouterServer router{router} /, }), ) // 4. fetch Response 转回 express Response并流式输出 res.statusMessage response.statusText res.status(response.status) response.headers.forEach((value, name) res.setHeader(name, value)) return pipeline(response.body as any, res) }这一层的设计要点每次请求独立创建 router 实例createRouter回调中调用createRouter()定义于 router.tsx并通过router.update({ context })注入来自 Vite 的head片段避免服务端请求间状态污染renderRouterToStream来自tanstack/solid-router/ssr/server是 Solid 版 SSR 流式渲染的核心 API。它将路由树渲染为可写流配合RouterServer组件输出流式 HTML——这也是流式Streaming的关键首屏可以先输出已就绪的内容Await/Suspense包裹的异步数据随后边解析边推送node:stream/promises的pipeline把响应体流安全地接到 Express 的res上处理背压与错误传播fetch-polyfillimport ./fetch-polyfill为 Node 环境补齐 Fetch API保证createRequestHandler内部依赖的 Request/Response 可用依赖node-fetch见 package.json。五、客户端入口与路由器工厂客户端入口entry-client.tsx 非常精简import { RouterClient } from tanstack/solid-router/ssr/client import { hydrate } from solid-js/web import { createRouter } from ./router const router createRouter() hydrate(() RouterClient router{router} /, document.body)hydrate来自solid-js/web把客户端渲染挂接到服务端已输出的 DOM 上而不是重新创建这是 SSR 应用水合的标准写法RouterClient负责接管路由器的客户端交互能力。路由器工厂router.tsximport { createRouter as createSolidRouter } from tanstack/solid-router import { routeTree } from ./routeTree.gen export function createRouter() { return createSolidRouter({ routeTree, defaultPreload: intent, // 悬停/聚焦链接时预加载 scrollRestoration: true, // 路由切换时恢复滚动位置 }) } declare module tanstack/solid-router { interface Register { router: ReturnTypetypeof createRouter } }两点值得说明routeTree.gen.ts由tanstack/router-plugin根据src/routes目录自动生成不要手工编辑新增/删除路由文件后插件会在开发时自动重新生成。类型注册declare module ... interface Register为useLoaderData、Link等 API 提供全链路类型安全这正是 TanStack Router fully type-safe 的体现。六、文件式路由从根路由到动态参数6.1 根路由与 head 管理__root.tsx 定义了应用的根布局。它通过head选项声明页面元信息——这是 SSR 场景下头部head注入的关键机制export const Route createRootRoute({ head: () ({ links: [ { rel: icon, href: /images/favicon.ico }, { rel: stylesheet, href: stylesCss }, ], meta: [ { title: TanStack Router SSR Basic File Based Streaming }, { charSet: UTF-8 }, { name: viewport, content: widthdevice-width, initial-scale1.0 }, ], scripts: [ { type: module, src: import.meta.env.PROD ? /static/entry-client.js : /src/entry-client.tsx, }, ], }), component: RootComponent, notFoundComponent: () (/* 全局 404 兜底 */), })notFoundComponent提供了全局未匹配路由的兜底 UI导航栏中This Route Does Not Exist链接正是用来演示这一能力。同时根路由也展示了 TanStack Router 的导航组件Link及其activeProps当前激活样式、activeOptions{{ exact: true }}精确匹配等用法。6.2 流式数据的标志性用法Await Suspenseindex.tsx 是流式渲染最直观的演示——loader同时返回同步数据与一个延迟 1 秒的 Promiseexport const Route createFileRoute(/)({ loader: () ({ date: new Date(), deferred: new Promise{ date: Date }((r) setTimeout(() r({ date: new Date() }), 1000), ), }), component: Home, }) function Home() { const data Route.useLoaderData() return ( div classp-2 h3Welcome Home!/h3 pData: {data().date.getDate()}/p Suspense fallback{divLoading.../div} Await promise{data().deferred} {(data) pDeferred: {new Date(data.date).getDate()}/p} /Await /Suspense /div ) }这里Awaittanstack/solid-router提供与 Solid 的Suspense协作已就绪的date立即渲染并随首屏流出deferred数据在 Promise resolve 后通过流式通道增量推送用户无需等待整页完成就能看到首屏内容——这正是流式 SSR相对传统 SSR 的核心优势。6.3 布局、动态参数与错误处理嵌套布局posts.tsx 定义/posts布局路由loader: fetchPosts拉取文章列表fetchPosts定义于 src/posts.tsx从 JSONPlaceholder 获取数据并setTimeout 500ms模拟延迟组件内通过Route.useLoaderData()消费数据并渲染Outlet /承载子路由。动态参数路由posts.$postId.tsx 演示$postId文件命名约定loader: async ({ params: { postId } }) fetchPost(postId)通过params读取 URL 参数fetchPost在请求 404 时抛出notFound()见 posts.tsx触发路由级notFoundComponent。错误边界errorComponent: PostErrorComponent位于 routes/-components/PostErrorComponent.tsx为详情页设置独立的加载失败 UI。路径无关布局_pathlessLayout.tsx 以下划线前缀命名createFileRoute(/_pathlessLayout)创建的布局不占用 URL 段内部嵌套的_nested-layout与route-a/route-b用于演示多层无路径布局组合。6.4 路由上下文routerContext.tsx 定义了路由上下文类型{ head: string }与entry-server.tsx中router.update({ context: { ...router.options.context, head } })的注入相呼应——服务端从 Vite 提取的 head 片段正是通过这一机制传递给渲染层。七、SSR 流式渲染的工作原理小结综合上述源码可以归纳出该示例的完整请求链路浏览器请求 → Express开发: Vite 中间件 / 生产: compression static → app.use(/{*splat}) 兜底处理器 → 加载 entry-server开发 ssrLoadModule / 生产 dist/server → render({ req, res, head }) → Express req 转 fetch Request → createRequestHandler新建 router 实例注入 head 上下文 → renderRouterToStream(RouterServer/) → pipeline 把响应流接入 res → 浏览器收到流式 HTML → 客户端 entry-client hydrate(RouterClient/) → 交互与后续导航由客户端接管defaultPreload、scrollRestoration 生效从源码结构看流式能力的三个支撑点缺一不可renderRouterToStream的流式输出 APItanstack/solid-router/ssr/server、Await/Suspense的异步分段渲染、以及pipeline的背压管理。若去掉Suspense或把 deferred 数据改为同步等待应用便退化为传统的一次性 SSR 输出。八、常见问题与调试提示产物路径不匹配导致生产 404entry-server.tsx中生产模式动态导入路径./dist/server/static/entry-server.js、根路由head.scripts中的/static/entry-client.js与vite.config.js的entryFileNames: static/[name].js必须保持一致。开发模式 head 提取server.js中通过vite.transformIndexHtml并截取head与/head之间的内容若自定义了index.html之外的 HTML 结构需同步调整这段提取逻辑。文件式路由未生效确认tanstack/router-plugin已在vite.config.js注册且target: solid并留意routeTree.gen.ts是否在新增路由后被自动重新生成。调试入口npm run debug以node --inspect-brk server启动可配合 Chrome DevTools 断点调试服务端渲染流程。延伸阅读服务端渲染核心 API 文档SSR 指南文件式路由概念与约定文件式路由本示例的 React 对应版本examples/react/basic-ssr-streaming-file-basedSolid Router 源码与测试packages/solid-router/src、packages/solid-router/tests【免费下载链接】router A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表