使用 SolidStart 在 Solid 中实现服务端渲染
使用 SolidStart v2 构建应用,了解服务端渲染、query 与 createAsync、路由预加载、服务端函数、渲染配置和部署。
SolidStart v2(@solidjs/start 2.x)· Solid v1 · Node.js 24+ · Vite 8
SolidStart 是 SolidJS 的全栈框架。它在 Solid 组件之上提供了基于文件的路由、服务端渲染和服务端函数,作用相当于 React 生态中的 Next.js 和 Svelte 生态中的 SvelteKit。
如果你已经熟悉 Solid 组件开发,转向全栈开发时最大的障碍反而是教程本身。大多数教程仍在使用 app.config.ts 和 Vinxi,而当前主版本中两者都已不复存在。本文将基于 SolidStart v2 构建一个小型应用,包含从服务端加载数据的页面,以及一个支持预加载的详情页。随后会介绍服务端函数、渲染配置和部署,并讨论其生态是否已足够成熟,值得放弃 React 技术栈而选择它。本文假定你已掌握 SolidJS 最佳实践中介绍的组件层面模式。
核心要点
- SolidStart v2 于 2026 年 8 月 4 日发布稳定版。它面向 Solid v1,以 Vite 的 Environment API 取代了 Vinxi,要求 Node.js 24+ 和 Vite 8,配置写在
vite.config.ts中。 - 在 SolidStart v2 中,路由数据的加载方式是:用
@solidjs/router中的query包装一个异步函数,再通过createAsync读取。 - 在 query 函数体顶部添加
"use server"指令,即可让该函数在服务端运行,从而可以读取密钥和会话数据。 - 在路由模块中导出
route.preload,可以在页面组件渲染之前启动该路由的 query。 - SolidStart v2 通过 Vite 插件(Nitro v3、Cloudflare、Netlify)进行部署,不再维护自己的托管适配器。
什么是 SolidStart?v2 现状如何?
SolidStart 是构建在 Solid 之上的元框架层。Solid 负责渲染组件,SolidStart 则围绕组件补充了路由、服务端渲染、数据加载以及构建与部署流水线。
SolidStart v2 已于 2026 年 8 月 4 日发布稳定版。Vinxi 已被移除,v2 改为基于 Vite 的 Environment API 构建。你需要 Node.js 24 或更高版本以及 Vite 8,框架配置也从 app.config.ts 迁移到了 vite.config.ts。v2 面向 Solid v1 构建。根据 Solid 2.0 Release Candidate 公告,SolidStart 今后只发布维护版本;在 Solid 2.0 中,其职能将由内置于 Solid 核心的 Start 模式接替。已经在生产环境中运行 SolidStart 的应用可以继续正常工作。下文代码面向 Solid v1 和 SolidStart v2。如果你要升级现有的 v1 应用,请参阅 v1 迁移指南。
创建 SolidStart 项目
要搭建项目脚手架,运行 Solid 初始化工具,并在提示时选择 SolidStart 模板:
npm init solid
# or: pnpm create solid
cd my-app
npm install
应用配置位于 vite.config.ts 中。入门指南中给出的最小配置如下:
import { defineConfig } from "vite";
import { solidStart } from "@solidjs/start/config";
export default defineConfig({
plugins: [solidStart()],
});
在生产环境中,需要将 solidStart() 与一个服务端插件(例如 Nitro v3)搭配使用。根据 solidStart 配置参考,Nitro 的设置不放在 solidStart() 中,而是写在单独的顶层 nitro 键下。
src/routes 下的每个文件都会成为一个页面或一个 API 端点。v2 API 路由文档说明,端点遵循与页面相同的文件到路径映射规则。框架内置的 ~ 别名指向应用根目录,因此在默认设置下,~/lib/db 对应 src/lib/db。
使用 query 和 createAsync 在服务端加载数据
在 SolidStart 中,路由数据来自一个用 @solidjs/router 的 query 包装的异步函数,组件再通过 createAsync 读取它。这正是 v2 数据获取文档所采用的模式。文档还指出,SolidStart v2 可以搭配 Solid Router v1 或 TanStack Solid Router v1 使用。本文示例使用 Solid Router。
首先在 src/lib/posts.ts 创建一个数据模块。它使用内存数组,因此示例无需数据库即可运行:
export type Post = { id: string; title: string; body: string };
const posts: Post[] = [
{ id: "1", title: "Hello SolidStart", body: "First post." },
{ id: "2", title: "Server functions", body: "Second post." },
];
export async function listPosts() {
return posts.map(({ id, title }) => ({ id, title }));
}
export async function findPost(id: string) {
return posts.find((p) => p.id === id) ?? null;
}
文章列表路由:
import { For } from "solid-js";
import { createAsync, query, type RouteDefinition } from "@solidjs/router";
import { listPosts } from "~/lib/posts";
const getPostList = query(async () => {
"use server";
return listPosts();
}, "post-list");
export const route = {
preload: () => getPostList(),
} satisfies RouteDefinition;
export default function PostsPage() {
const list = createAsync(() => getPostList());
return (
<ul>
<For each={list()}>
{(post) => (
<li>
<a href={`/posts/${post.id}`}>{post.title}</a>
</li>
)}
</For>
</ul>
);
}
详情页是一个接收 id 参数的动态路由。动态路由段的文件命名语法请参阅 v2 路由文档。
import { Show } from "solid-js";
import { createAsync, query, type RouteDefinition } from "@solidjs/router";
import { findPost } from "~/lib/posts";
const getPostById = query(async (id: string) => {
"use server";
return findPost(id);
}, "post-by-id");
export const route = {
preload: ({ params }) => getPostById(params.id),
} satisfies RouteDefinition;
export default function PostPage(props: { params: { id: string } }) {
const post = createAsync(() => getPostById(props.params.id));
return (
<Show when={post()} fallback={<p>Post not found.</p>}>
{(p) => (
<article>
<h1>{p().title}</h1>
<p>{p().body}</p>
</article>
)}
</Show>
);
}
query 的第二个参数是缓存键,因此每个 query 都应使用独立的键。导出的 route.preload 会在页面组件渲染之前启动 query,这意味着数据加载与导航同时开始。
表单、数据变更与重新验证
将数据写回服务端与读取数据的方式不同。一次数据变更(mutation)既要修改服务端状态,又要通知页面其缓存的读取结果已经过期。在 v2 中,写操作通过 Solid Router 的 action 完成;如果相关逻辑必须在服务端执行,就在 action 函数体中添加 "use server" 指令。v2 数据变更文档提供了最新的表单示例、pending 与错误状态的处理方式,以及如何通过 .with() 传递额外参数。
重新验证在读取侧的机制沿用了上一节的内容。每个 query 都以其键(上文中的 "post-list" 和 "post-by-id")进行缓存,当对应的缓存条目被重新验证时,createAsync 会重新读取数据。默认情况下,action 成功执行后,Solid Router 会重新验证页面上所有活跃的 query。如果只想刷新其中一部分,可以向 revalidate 传入键:getPostList.key 指向某个 query 的所有实例,getPostById.keyFor(id) 则只指向其中一个。数据获取文档将缓存行为交由 Solid Router 负责,路由的重新验证文档对此有详细说明。
在缩小重新验证范围时,清晰且稳定的 query 键名会非常有用。新增文章的变更只需刷新 "post-list",而编辑文章还需要刷新 "post-by-id"。
“use server” 的作用是什么?
在函数体顶部放置 "use server" 指令,可以让该函数在服务端运行,即使它是从组件中调用的。正因如此,带有该标记的 query 才能读取 process.env 中的密钥和会话数据。v2 数据获取文档的仅服务端示例中,就使用了 @solidjs/start/http 提供的 useSession 来演示这一点。
应放在 "use server" 中 | 不应放在其中 |
|---|---|
环境密钥(process.env.*) | 浏览器 API(window、localStorage、DOM) |
| 数据库与会话访问 | 存在于组件中的 UI 状态 |
@solidjs/start/http 辅助函数 | 任何需要在点击时直接运行、无需发起请求的逻辑 |
在 SolidStart 应用中,UI 状态属于客户端。服务端负责持有数据,signal 和 store 负责持有交互状态,SolidJS 中的状态管理一文对这种划分有详细介绍。
SolidStart v2 还支持 server-only 和 client-only 标记导入。在模块中添加其中一个标记后,如果从错误的一侧导入该模块(在客户端导入服务端代码,或反之),开发服务器或构建过程就会报错,这样错误就不会到达用户。配置参考列出了 @solidjs/start/http 和 @solidjs/start/middleware 已被标记为 server-only。开发期间,你还会获得一个工具栏,用于显示应用错误和服务端函数调用。它永远不会出现在生产构建中。
SolidStart 中的渲染模式
在 SolidStart v2 中,渲染行为通过 solidStart() 插件及与之搭配的部署插件进行配置,两者都声明在 vite.config.ts 中。关于服务端渲染、流式渲染和静态生成之间的概念权衡,请参阅十大渲染模式。本节只介绍这些选项在 SolidStart 中的配置位置。
由于 app.config.ts 已不复存在,旧文章中出现的 v1 时代渲染选项都无法照搬使用。关于 v2 提供了哪些选项,请查阅 solidStart() 配置参考和部署插件文档。
当页面在服务端渲染时,用户会在客户端 JavaScript 完成水合(hydration)之前就看到页面标记。SSR 应用的会话回放可以清楚地揭示这段间隙:用户点击了一个显示正常却毫无反应的按钮,等事件处理器绑定完成后又点击了一次。
部署 SolidStart v2
SolidStart v2 没有自己的托管适配器,而是通过 Vite 插件进行部署。v2 发布公告列出了三个受支持的部署目标:Nitro v3 Vite 插件(据该公告,仍处于 beta 阶段)、Cloudflare Vite 插件和 Netlify Vite 插件。入门指南中链接了一个部署插件页面,介绍了每个插件的生产运行时配置。
选择 SolidStart 而非 React 技术栈,真正的代价在于生态。React 拥有多得多的第三方组件库、认证集成和托管模板,而 Solid 团队则需要自己编写更多此类胶水代码。SolidJS 与 React 对比一文将这一差距与 Solid 的渲染模型进行了权衡比较。此外还要注意,在 Solid 2.0 向 Start 模式演进的同时,SolidStart v2 已进入维护阶段。
结语
SolidStart v2 为 Solid v1 应用提供了一种稳定的、Vite 原生的服务端渲染方案。数据加载归结为三个要素:query、createAsync 和 "use server" 指令,再配合 route.preload 实现提前获取数据。下一步,你可以用 npm init solid 搭建项目,添加上文中的两个文章路由,然后参照 v2 文档的数据变更章节实现一次数据变更。
常见问题
SolidStart v2 能与 Solid 2.0 一起使用吗?
不能。SolidStart v2 面向 Solid 1.x 构建,目前没有任何 SolidStart 版本面向 Solid 2.0。Solid 2.0 Release Candidate 让 SolidStart 退役,并将其各部分职能转移到别处:服务端函数由 Solid 核心负责,服务托管由 Start 模式负责,文件系统路由则由一个独立的、与路由库无关的包负责。现有的 SolidStart 应用可以继续运行,SolidStart 仍会发布维护版本,并且有官方迁移指南介绍如何迁移到 Start 模式。
TanStack Start 与搭配 TanStack Router 的 SolidStart 有什么区别?
它们是两个不同的选项。SolidStart v2 与路由库无关,可以搭配官方的 Solid Router v1 或 TanStack Solid Router v1 运行,因此你无需离开 SolidStart 就能使用 TanStack 路由。TanStack Start 则是一个独立的元框架,提供 Solid 版本,以 @tanstack/solid-start 发布,并且已经推出了支持 Solid 2.0 的 beta 版本。对于 Solid 2.0,fullstack-tanstack 起步模板将 Start 模式与 TanStack Router 和 TanStack Query 结合在了一起。
SolidStart 中的单次往返数据变更(single-flight mutations)是什么?
单次往返数据变更会在数据变更本身的同一个服务端响应中返回更新后的数据。当一个服务端 action 修改了当前视图已预加载的数据,或重定向到一个数据已预加载的页面时,SolidStart 会在服务端重新验证这些数据,并在同一次往返中返回。客户端无需再发起第二轮请求,因此写入之后不会产生请求瀑布。
将 SolidStart v1 应用升级到 v2 时有哪些变化?
升级需要 Node.js 24 和 Vite 8。框架配置从 app.config.ts 迁移到 vite.config.ts,solidStart() 在其中作为 Vite 插件注册,Vite 的 Environment API 取代了 Vinxi。官方迁移指南还详细介绍了导入路径、TypeScript 环境类型、服务端运行时和中间件方面的变化。请对照指南检查 v1 的导入路径和中间件代码,而不要原样照搬。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k