On this page
使用 Tanstack 和 Deno 构建应用
Tanstack 是一套与框架无关的数据管理工具。 借助 Tanstack,开发者可以使用 Query 高效管理服务器状态, 使用 Table 创建强大的表格, 使用 Router 处理复杂路由, 并使用 Form 构建类型安全的表单。 这些工具能在 React、Vue、 Solid 和其他框架中无缝协作,同时保持出色的 TypeScript 支持。
在本教程中,我们将使用 Tanstack Query 和 Tanstack Router 构建一个简单的应用。 该应用将展示一份恐龙列表。你点击其中一只时,会跳转到包含更多细节的恐龙详情页面。
你可以直接跳到 源代码 或继续在下面跟着做!
从后端 API 开始 Jump to heading
在我们的主目录中,先创建一个 api/ 目录,并准备恐龙数据文件 api/data.json:
// api/data.json
[
{
"name": "Aardonyx",
"description": "蜥脚类的进化早期阶段。"
},
{
"name": "Abelisaurus",
"description": "“Abel 的蜥蜴”已根据一具单独的头骨重建。"
},
{
"name": "Abrictosaurus",
"description": "异齿龙(Heterodontosaurus)的早期近亲。"
},
...
]
这里就是我们将从中读取数据的地方。在完整应用中,这些数据 将来自数据库。
⚠️️ 在本教程中我们是把数据写死(hard code)的。但你可以连接 多种数据库 并且 甚至可以使用像 Prisma 这样的 ORM 来配合 Deno 使用。
接下来,让我们创建我们的 Hono 服务器。
我们从 JSR 使用 deno add 安装 Hono:
deno add jsr:@hono/hono
然后,让我们创建一个 api/main.ts 文件,并用下面的代码来填充它。注意
我们需要导入
@hono/hono/cors,并定义一些关键属性,
以允许前端访问 API 路由。
// api/main.ts
import { Hono } from "@hono/hono";
import { cors } from "@hono/hono/cors";
import data from "./data.json" with { type: "json" };
const app = new Hono();
app.use(
"/api/*",
cors({
origin: "http://localhost:5173",
allowMethods: ["GET", "POST", "PUT", "DELETE"],
allowHeaders: ["Content-Type", "Authorization"],
exposeHeaders: ["Content-Type", "Authorization"],
credentials: true,
maxAge: 600,
}),
);
app.get("/", (c) => {
return c.text("欢迎来到恐龙 API!");
});
app.get("/api/dinosaurs", (c) => {
return c.json(data);
});
app.get("/api/dinosaurs/:dinosaur", (c) => {
if (!c.req.param("dinosaur")) {
return c.text("未提供恐龙名称。");
}
const dinosaur = data.find((item) =>
item.name.toLowerCase() === c.req.param("dinosaur").toLowerCase()
);
if (dinosaur) {
return c.json(dinosaur);
} else {
return c.notFound();
}
});
Deno.serve(app.fetch);
Hono 服务器提供两个 API 端点:
GET /api/dinosaurs用于获取所有恐龙,以及GET /api/dinosaurs/:dinosaur用于按名称获取特定恐龙
在我们开始开发前端之前,先来更新一下 deno.json 文件中的
deno tasks。你的配置应该类似下面这样:
{
"tasks": {
"dev": "deno --allow-env --allow-net api/main.ts"
}
// ...
}
现在,当我们运行 deno task dev 时,后端服务器将会在 localhost:8000
启动。
创建一个由 Tanstack 驱动的前端 Jump to heading
让我们创建会使用这些数据的前端。首先,我们将在当前目录中使用 TypeScript 模板,用 Vite 快速脚手架一个新的 React 应用:
deno init --npm vite@latest --template react-ts ./
接着,我们安装 Tanstack 相关的依赖:
deno install npm:@tanstack/react-query npm:@tanstack/react-router
然后,更新一下 deno.json 里的 deno tasks,以添加用于启动 Vite
服务器的命令:
// deno.json
{
"tasks": {
"dev": "deno task dev:api & deno task dev:vite",
"dev:api": "deno --allow-env --allow-net api/main.ts",
"dev:vite": "deno -A npm:vite"
}
// ...
}
接下来我们可以开始构建组件。我们的应用需要两个主要页面:
DinosaurList.tsx:索引页,用于列出所有恐龙,以及Dinosaur.tsx:叶子页面,用于展示单只恐龙的信息
我们先创建一个新的 ./src/components 目录,在其中创建文件
DinosaurList.tsx:
// ./src/components/DinosaurList.tsx
import { useQuery } from "@tanstack/react-query";
import { Link } from "@tanstack/react-router";
async function fetchDinosaurs() {
const response = await fetch("http://localhost:8000/api/dinosaurs");
if (!response.ok) {
throw new Error("Failed to fetch dinosaurs");
}
return response.json();
}
export function DinosaurList() {
const {
data: dinosaurs,
isLoading,
error,
} = useQuery({
queryKey: ["dinosaurs"],
queryFn: fetchDinosaurs,
});
if (isLoading) return <div>加载中...</div>;
if (error instanceof Error) {
return <div>发生错误:{error.message}</div>;
}
return (
<div>
<h2 className="text-xl font-semibold mb-4">恐龙列表</h2>
<ul className="space-y-2">
{dinosaurs?.map((dino: { name: string; description: string }) => (
<li key={dino.name}>
<Link
to="/dinosaur/$name"
params={{ name: dino.name }}
className="text-blue-500 hover:underline"
>
{dino.name}
</Link>
</li>
))}
</ul>
</div>
);
}
这里使用了
useQuery
来自 Tanstack Query,用于自动获取并缓存恐龙数据,同时提供内置的
加载与错误状态。然后它使用
Link
来自 Tanstack Router,来创建带类型安全路由参数的客户端导航链接。
接下来,我们在 ./src/components/ 目录下创建 DinosaurDetail.tsx
组件,用于展示单只恐龙的详情:
// ./src/components/DinosaurDetail.tsx
import { useParams } from "@tanstack/react-router";
import { useQuery } from "@tanstack/react-query";
async function fetchDinosaurDetail(name: string) {
const response = await fetch(`http://localhost:8000/api/dinosaurs/${name}`);
if (!response.ok) {
throw new Error("Failed to fetch dinosaur detail");
}
return response.json();
}
export function DinosaurDetail() {
const { name } = useParams({ from: "/dinosaur/$name" });
const {
data: dinosaur,
isLoading,
error,
} = useQuery({
queryKey: ["dinosaur", name],
queryFn: () => fetchDinosaurDetail(name),
});
if (isLoading) return <div>加载中...</div>;
if (error instanceof Error) {
return <div>发生错误:{error.message}</div>;
}
return (
<div>
<h2 className="text-xl font-semibold mb-4">{name}</h2>
<p>{dinosaur?.description}</p>
</div>
);
}
同样地,这里使用 Tanstack Query 的 useQuery 来获取并缓存
单只恐龙的详情,并且
queryKey
包含恐龙名称,以确保正确的缓存行为。此外,我们使用
useParams
来自 Tanstack Router,用于安全地从 URL 中提取并为我们路由配置中定义的参数
提供类型支持。
在运行之前,我们需要将这些组件封装进一个布局(layout)中。
我们在 ./src/components/ 目录下再创建一个文件:Layout.tsx:
// ./src/components/Layout.tsx
export function Layout() {
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">恐龙百科全书</h1>
<nav className="mb-4">
<Link to="/" className="text-blue-500 hover:underline">
首页
</Link>
</nav>
<Outlet />
</div>
);
}
你可能已经注意到,新创建的布局底部附近的
Outlet
组件。这个组件来自 Tanstack Router,用于渲染子路由的内容,
从而在保持一致的布局结构的同时,实现嵌套路由。
接下来,我们需要把这个布局接入 ./src/main.tsx,它是一个重要的文件:
用于为管理服务器状态设置 Tanstack Query client,
并为处理导航设置 Tanstack Router。
// ./src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createRouter, RouterProvider } from "@tanstack/react-router";
import { routeTree } from "./routeTree";
const queryClient = new QueryClient();
const router = createRouter({ routeTree });
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
</React.StrictMode>,
);
你会注意到我们导入了
QueryClientProvider,
它会把整个应用包裹起来,从而支持查询缓存与状态管理。
我们还导入了 RouterProvider,用于把我们定义的路由连接到 React 的渲染系统。
最后,我们需要在 ./src/ 目录下定义一个
routeTree.tsx
文件。这个文件使用 Tanstack Router 的类型安全路由定义,
来描述我们应用的路由结构:
// ./src/routeTree.tsx
import { RootRoute, Route } from "@tanstack/react-router";
import { DinosaurList } from "./components/DinosaurList";
import { DinosaurDetail } from "./components/DinosaurDetail";
import { Layout } from "./components/Layout";
const rootRoute = new RootRoute({
component: Layout,
});
const indexRoute = new Route({
getParentRoute: () => rootRoute,
path: "/",
component: DinosaurList,
});
const dinosaurRoute = new Route({
getParentRoute: () => rootRoute,
path: "dinosaur/$name",
component: DinosaurDetail,
});
export const routeTree = rootRoute.addChildren([indexRoute, dinosaurRoute]);
在 ./src/routeTree.tsx 中,我们创建了一层以 Layout 作为根组件的路由层级。
然后我们再配置两个子路由:它们的路径与组件——一个用于恐龙列表,
DinosaurList;另一个用于单只恐龙详情,并带有动态参数,
DinosaurDetail。
完成这些之后,我们就可以运行这个项目了:
deno task dev
下一步 Jump to heading
这只是用 Deno 和 Tanstack 进行开发的起点。你可以添加 持久化数据存储,例如 使用像 Postgres 或 MongoDB 这样的数据库 以及像 Drizzle 或 Prisma 这样的 ORM。或者将你的应用部署到 AWS, Digital Ocean,或 Google Cloud Run
你还可以使用 Tanstack Query 的自动重新获取功能 来添加实时更新, 针对大型恐龙列表 实现无限滚动,或 通过使用 Tanstack Table 来 添加复杂的筛选和排序。 Deno 内置的 Web 标准、工具链以及原生 TypeScript 支持, 再加上 Tanstack 强大的数据管理能力,为构建健壮的 Web 应用打开了无数可能性。