news 2026/9/15 20:25:09

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于 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-router:Solid 版 TanStack Router 运行时;
  • @tanstack/router-plugin:提供 Vite 插件,负责文件式路由扫描与routeTree.gen.ts代码生成;
  • vite-plugin-solid:Solid 的 Vite 集成(开启ssr: true);
  • express:作为自定义 SSR 服务器;
  • solid-jstailwindcsszod等支撑库。

运行方式(与原文档一致,直接可复现):

# 开发模式(Vite 中间件 + HMR + SSR) npm install npm start # 等价于 node server # 生产构建与启动 npm run build # 依次执行 build:client 与 build:server npm run serve # NODE_ENV=production 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 上,从而获得即时编译与 HMR:

if (!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 通配语法):

  1. 过滤带文件扩展名的无效路径(path.extname(url) !== ''时返回 404);
  2. 开发模式通过vite.transformIndexHtml提取<head>片段,用于注入 Vite 处理后的资源引用;
  3. 加载服务端入口(开发vite.ssrLoadModule('/src/entry-server.tsx'),生产import('./dist/server/static/entry-server.js'));
  4. 调用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/promisespipeline:把响应体流安全地接到 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.tsx:

import { 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: ReturnType<typeof createRouter> } }

两点值得说明:

  • routeTree.gen.ts@tanstack/router-plugin根据src/routes目录自动生成,不要手工编辑;新增/删除路由文件后插件会在开发时自动重新生成。
  • 类型注册(declare module ... interface Register)为useLoaderDataLink等 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: 'width=device-width, initial-scale=1.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 + Suspense

index.tsx 是"流式渲染"最直观的演示——loader同时返回同步数据与一个延迟 1 秒的 Promise:

export 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 class="p-2"> <h3>Welcome Home!</h3> <p>Data: {data().date.getDate()}</p> <Suspense fallback={<div>Loading...</div>}> <Await promise={data().deferred}> {(data) => <p>Deferred: {new Date(data.date).getDate()}</p>} </Await> </Suspense> </div> ) }

这里Await@tanstack/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-layoutroute-a/route-b用于演示多层无路径布局组合。

6.4 路由上下文

routerContext.tsx 定义了路由上下文类型{ head: string },与entry-server.tsxrouter.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的流式输出 API(@tanstack/solid-router/ssr/server)、Await/Suspense的异步分段渲染、以及pipeline的背压管理。若去掉Suspense或把 deferred 数据改为同步等待,应用便退化为传统的一次性 SSR 输出。

八、常见问题与调试提示

  • 产物路径不匹配导致生产 404entry-server.tsx中生产模式动态导入路径./dist/server/static/entry-server.js、根路由head.scripts中的/static/entry-client.jsvite.config.jsentryFileNames: '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 debugnode --inspect-brk server启动,可配合 Chrome DevTools 断点调试服务端渲染流程。

延伸阅读

  • 服务端渲染核心 API 文档:SSR 指南
  • 文件式路由概念与约定:文件式路由
  • 本示例的 React 对应版本:examples/react/basic-ssr-streaming-file-based
  • Solid 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),仅供参考

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

SSM框架作业提交批改系统:权限控制与核心流程详解

简介&#xff1a;基于SSM框架的作业提交与批改程序&#xff0c;是一套面向高校计算机专业学生的Java毕业设计项目&#xff0c;主要解决传统作业收发效率低、批改反馈不及时的痛点。系统按角色划分为管理员、学生和老师&#xff1a;学生可维护个人资料、查看成绩并提交作业&…

作者头像 李华
网站建设 2026/9/15 20:21:06

超标量处理器全面解析:从IPC到乱序执行与寄存器重命名

这篇是体系结构学习笔记系列的第六篇&#xff0c;聊超标量处理器&#xff08;Superscalar Processor&#xff09;。学到这里先要有心理准备&#xff1a;它和前面几篇的流水线、冒险这些基础章节不一样&#xff0c;超标量把流水线“时间重叠”的思路升级成了“空间并行”的思路&…

作者头像 李华
网站建设 2026/9/15 20:20:12

如何在 QEMU 中运行 Zephyr 示例应用并检查结果输出

如何在 QEMU 中运行 Zephyr 示例应用并检查结果输出 【免费下载链接】zephyr Primary Git Repository for the Zephyr Project. Zephyr is a new generation, scalable, optimized, secure RTOS for multiple hardware architectures. 项目地址: https://gitcode.com/GitHub_…

作者头像 李华
网站建设 2026/9/15 20:20:08

抖音无水印视频与直播批量录制:douyin-downloader 完整上手指南

抖音无水印视频与直播批量录制&#xff1a;douyin-downloader 完整上手指南 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fallb…

作者头像 李华