要点
Solid 系列第二篇。开山篇《SolidJS 入门》讲的是框架本体——细粒度响应式、三个原语与 props 陷阱;本篇往下走一层:元框架 SolidStart 怎么用文件树表达路由,怎么用 query/createAsync/action 处理数据,全程与 Next.js、Nuxt 3 的同类机制做文字对照,方便从这两个框架迁移过来的人对号入座。版本锚点:截至本文(2025 年 11 月中),稳定线为 solid-js 1.9.10(2025-10-27 发布)、SolidStart 1.2.0(2025-09-11 发布,1.0 已于 2024 年 5 月正式 GA)、@solidjs/router 0.15.4(2025-11-10 发布)。SolidStart 2.0 尚未发布,官方 2025 年 9 月公开的路线图正在把底层从 Vinxi 迁向纯 Vite(devinxi 分支),本篇全部按 1.x 的现行行为描述。
路由是文件树,布局是同名文件
SolidStart 没有路由配置文件,src/routes 目录本身就是路由表:每个文件对应一个 URL 段,index.tsx 对应目录本身。动态段与特殊目录共五种写法:
| 写法 | 匹配 | 示例 |
|---|---|---|
| [id].tsx | 一段任意内容 | users/[id].tsx 匹配 /users/1 |
| [[id]].tsx | 可有可无的一段 | users/[[id]].tsx 同时匹配 /users 与 /users/1 |
| […all].tsx | 剩余全部路径(含多段) | 兜底 404 页,params.all 是整段剩余路径 |
| (group) | 分组目录,不进入 URL | (static)/about/index.tsx 对应 /about |
| (name).tsx | 目录的同名简写 | socials/(socials).tsx 对应 /socials,省一个 index.tsx |
布局的规则最不像 Next/Nuxt:嵌套布局不写 layout.tsx,而是用与目录同名的文件承担——blog.tsx 是 blog/ 目录下所有页面的布局,子页面渲染在 props.children 里:
// src/routes/blog.tsx —— blog/ 子树的布局壳,blog/index.tsx 与 blog/[slug].tsx 都套它
import { RouteSectionProps } from "@solidjs/router";
export default function BlogLayout(props: RouteSectionProps) {
return (
<section>
<h1>博客</h1>
{props.children}
</section>
);
}
blog/index.tsx 与 blog.tsx 不是一回事:前者只是 /blog 这一个页面,后者是整个子树的布局。想让某个页面逃出父布局,把目录名加括号——users(details)/[id].tsx 的 URL 仍是 /users/1,但不再嵌在 users.tsx 布局里。
入口:Router、root 与不能省的 Suspense
应用入口由两个文件组成:src/index.tsx 调用 mount 挂载,src/app.tsx 把路由串起来,后者才是写逻辑的地方:
// src/app.tsx
import { Suspense } from "solid-js";
import { Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
export default function App() {
return (
<Router root={(props) => <Suspense>{props.children}</Suspense>}>
<FileRoutes />
</Router>
);
}
root 就是全应用的根布局,对应 Next 的 app/layout.tsx、Nuxt 的 layouts/default.vue;FileRoutes 把 routes 目录转成路由定义,且只收 UI 路由,API 路由不进这份配置。官方文档明确提醒 props.children 必须包在 Suspense 里:路由组件是 lazy 加载的,漏掉这一层会得到 hydration 错误——从 React 迁移过来的人最容易把它当装饰省掉。
三家的文件约定对比如下(细节以各家当前文档为准):
| 机制 | SolidStart 1.2 | Next.js(App Router) | Nuxt 3 |
|---|---|---|---|
| 路由目录 | src/routes,文件即路由 | app/ 目录 + page.tsx | pages/ 目录 |
| 嵌套布局 | 与目录同名文件 + props.children | layout.tsx 沿目录嵌套 | layouts/ 配 definePageMeta |
| 动态段 | [id] / [[id]] / […all] | [id] / […all] | [id] / […all] |
| API 端点 | 路由文件导出 GET/POST 等方法 | route.ts 端点文件 | server/api/ 目录 |
| 数据读取 | query + createAsync | async server component | useAsyncData / useFetch |
| 数据修改 | action + form method=”post” | Server Actions | $fetch 调 server routes |
文件树相似度很高,真正的分野在数据层——往下两节就是 Solid 版本的答案。
API 路由:导出方法名就是端点
一个 routes 文件默认导出组件就是页面,导出 GET/POST/PATCH/DELETE 这类与 HTTP 方法同名的函数就是 API 端点:
// src/routes/api/boards.ts —— 与 UI 路由共用同一套文件约定
import type { APIEvent } from "@solidjs/start/server";
export async function GET({ params }: APIEvent) {
return { board: params.id }; // 返回普通对象即按 JSON 输出
}
export async function POST({ request }: APIEvent) {
const body = await request.json(); // request 是标准 Web API 的 Request
return new Response("bad title", { status: 400 }); // 也可返回 Response 控制状态码
}
端点收到的 APIEvent 提供 request(标准 Request)、params(动态段)与 fetch(内部调用其他端点,免拼 origin)。同一路径同时存在页面与端点时 API 优先,GET 端点不返回值会回落到 UI 路由——按 Accept 头分流是这个设计的本意。对照 Next 的 route.ts 与 Nuxt 的 server/api:思路同源,SolidStart 把页面与端点收进了同一个 routes 目录。
读数据:query 打缓存,createAsync 接组件
router 0.15 的数据 API 是三个名字:query(按 key 给取数函数做请求级去重缓存,旧名 cache 在 0.15.0 更名后保留为弃用别名)、createAsync(把 query 结果接进组件)、action(写路径,下一节)。典型读法:
import { createAsync, query } from "@solidjs/router";
import { Suspense } from "solid-js";
const getBoard = query(async () => {
"use server"; // 该函数只在服务端执行,可直连数据库
return await db.loadBoard();
}, "board");
export default function BoardPage() {
const board = createAsync(() => getBoard());
return (
<Suspense fallback={<p>加载中</p>}>
<Board data={board()} /> {/* pending 时落入 fallback,就绪后流式补上 */}
</Suspense>
);
}
四个行为值得点名。其一,query 的缓存是请求级去重:同名同参的调用共享一份结果,同页十个组件读同一个 query 也只有一个后端请求。其二,createAsync 是 createResource 的包装,返回取值函数与 latest 属性;fetcher 里同步读到的信号自动成为依赖,参数变化即重取,不用手写监听。其三,读值落进 Suspense 就触发流式 SSR,慢数据不阻塞首屏;deferStream 选项可以反过来强制等这份数据再冲刷流。其四,对照 Next:server component 里直接 await,缓存规则由框架默认接管;Solid 把”何时缓存、何时去重”显式交给 query 声明,心智上更接近 Nuxt 的 useAsyncData——key 即缓存身份。
写数据:action、表单与自动刷新
写路径是 action(fn, "名字"):函数体首行写 "use server",返回值经 useSubmission 传回组件。表单提交不需要 onSubmit——把 action 直接交给 form:
import { action, useSubmission, redirect } from "@solidjs/router";
const addCard = action(async (formData: FormData) => {
"use server";
const title = String(formData.get("title") ?? "").trim();
if (!title) return { error: "标题必填" }; // 校验失败经 result 回传,不抛异常
await db.addCard(title);
throw redirect("/"); // 或直接 throw redirect 跳转
}, "addCard");
function AddForm() {
const submission = useSubmission(addCard);
return (
<form action={addCard} method="post"> {/* 无需 onSubmit,路由层接管提交 */}
<input name="title" />
<button disabled={submission.pending}>添加</button>
</form>
);
}
useSubmission 给出提交的四件状态:pending(进行中)、result(action 的返回值)、error(抛出的异常)、input(提交时的入参,乐观更新靠它)。关键行为是:官方文档说明 action 成功后会自动 revalidate 本页用到的 query,UI 无需手写”提交成功后刷新列表”;要精细控制,用 reload/json 的 revalidate 选项指定 key,传空数组则完全不重取。非表单场景(拖拽换列就是典型)用 useAction(addCard) 拿到可编程调用的函数;action.with() 预绑定参数。对照 Next 的 Server Actions:同为”函数即端点”,差别在于回执与 revalidate 是显式 API,看得见也改得动。
反模式点名:绕开 query/createAsync 取数。两种变体都很常见:在组件里 onMount 之后 fetch——首屏 HTML 缺数据,多一次客户端往返,SEO 抓到的还是空壳;把取数塞进 createEffect 再赋值给 signal——更新链路被副作用藏起来,还丢掉了 Suspense 的流式与 fallback。数据读取只有一条正路:query + createAsync + Suspense 组合。第二个反模式是省掉 root 里那个 Suspense:路由组件是 lazy 的,文档明确警告会造成 hydration 错误,那一行不是装饰。
下一篇与延伸
本篇的条目速查版(含 store、控制流组件与 router 0.15 全量 API)见下一篇《SolidJS 基础知识参考字典》;把这些组合成真实应用的完整过程见实战篇《用 SolidJS 做一个看板小应用》(2025-12-04 发布)。对照阅读推荐上月的SvelteKit 路由与数据加载详解——两篇的”文件路由 + 数据 API”结构几乎逐节对应,对读能把元框架数据流的共性看穿。框架本体的响应式模型遗忘时回开山篇;async/await 基础不牢先补站内Node.js 异步与 Promise,query/action 的心智全建立在这上面。
版本核实(以官方文档为准):截至本文(2025-11-17)稳定线为 solid-js 1.9.10(npm,2025-10-27)、SolidStart 1.2.0(npm,2025-09-11;1.0 GA 于 2024-05 官方博客公告)、@solidjs/router 0.15.4(npm,2025-11-10);cache 更名为 query 发生于 router 0.15.0;文件路由约定、API 路由方法名导出、APIEvent、root 需包裹 Suspense、query/createAsync/action 行为均依据 SolidStart v1 与 solid-router 官方文档;SolidStart 2.0 截至 2025-11-17 未发布,Vinxi 迁向 Vite 为官方 2025-09 路线图披露内容。安装命令不钉版本号。