使用 Tanstack 和 Deno 构建应用程序
Tanstack 是一套与框架无关的数据管理工具。借助 Tanstack,开发人员可以利用 Query 高效管理服务器状态,使用 Table 创建强大的表格,通过 Router 处理复杂的路由,并使用 Form 构建类型安全的表单。这些工具在 React、Vue、Solid 和其他框架中无缝协作,同时保持出色的 TypeScript 支持。
在本教程中,我们将使用 Tanstack Query 和 Tanstack Router 构建一个简单的应用程序。该应用程序将显示一个恐龙列表。当您点击其中一个时,它会带您进入恐龙详情页,其中包含更多详细信息。
您可以直接跳转到源代码,或按照以下步骤操作!
从后端 API 开始 跳转到标题
在主目录下,让我们设置一个 api/
目录并创建我们的恐龙数据文件 api/data.json
// api/data.json
[
{
"name": "Aardonyx",
"description": "An early stage in the evolution of sauropods."
},
{
"name": "Abelisaurus",
"description": "\"Abel's lizard\" has been reconstructed from a single skull."
},
{
"name": "Abrictosaurus",
"description": "An early relative of Heterodontosaurus."
},
...
]
这将是我们数据来源的地方。在完整的应用程序中,这些数据将来自数据库。
⚠️️ 在本教程中,我们硬编码了数据。但您可以使用 Deno 连接到各种数据库,甚至使用Prisma 等 ORM。
其次,让我们创建我们的 Hono 服务器。我们首先使用 deno add
从 JSR 安装 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: "https://:5173",
allowMethods: ["GET", "POST", "PUT", "DELETE"],
allowHeaders: ["Content-Type", "Authorization"],
exposeHeaders: ["Content-Type", "Authorization"],
credentials: true,
maxAge: 600,
}),
);
app.get("/", (c) => {
return c.text("Welcome to the dinosaur API!");
});
app.get("/api/dinosaurs", (c) => {
return c.json(data);
});
app.get("/api/dinosaurs/:dinosaur", (c) => {
if (!c.req.param("dinosaur")) {
return c.text("No dinosaur name provided.");
}
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 驱动的前端 跳转到标题
让我们创建将使用这些数据的前端。首先,我们将在当前目录中使用 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("https://: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>Loading...</div>;
if (error instanceof Error) {
return <div>An error occurred: {error.message}</div>;
}
return (
<div>
<h2 className="text-xl font-semibold mb-4">Dinosaur List</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>
);
}
此组件使用 **Tanstack Query** 的 useQuery
自动获取并缓存恐龙数据,并内置加载和错误状态。然后它使用 **Tanstack Router** 的 Link
创建带有类型安全路由参数的客户端导航链接。
接下来,让我们在 `./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(`https://: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>Loading...</div>;
if (error instanceof Error) {
return <div>An error occurred: {error.message}</div>;
}
return (
<div>
<h2 className="text-xl font-semibold mb-4">{name}</h2>
<p>{dinosaur?.description}</p>
</div>
);
}
同样,此组件使用 **Tanstack Query** 的 useQuery
获取并缓存单个恐龙详情,其中 queryKey
包含恐龙名称以确保正确的缓存。此外,我们使用 **Tanstack Router** 的 useParams
安全地提取并类型化路由配置中定义的 URL 参数。
在运行之前,我们需要将这些组件封装到一个布局中。让我们在 `./src/components/` 文件夹中创建另一个文件,名为 `Layout.tsx`
// ./src/components/Layout.tsx
export function Layout() {
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">Dinosaur Encyclopedia</h1>
<nav className="mb-4">
<Link to="/" className="text-blue-500 hover:underline">
Home
</Link>
</nav>
<Outlet />
</div>
);
}
您可能会注意到我们新创建的布局底部附近的 Outlet
组件。此组件来自 **Tanstack Router**,它渲染子路由的内容,允许嵌套路由,同时保持一致的布局结构。
接下来,我们需要将此布局与 `./src/main.tsx` 连接起来,这是一个重要的文件,它设置了用于管理服务器状态的 Tanstack Query 客户端和用于处理导航的 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
后续步骤 跳转到标题
这只是使用 Deno 和 Tanstack 进行构建的开始。您可以添加持久化数据存储,例如使用 Postgres 或 MongoDB 等数据库,以及 Drizzle 或 Prisma 等 ORM。或者将您的应用程序部署到 AWS、Digital Ocean 或 Google Cloud Run
您还可以使用 Tanstack Query 的重新获取功能添加实时更新,为大型恐龙列表实现无限滚动,或者使用 **Tanstack Table** 添加复杂的过滤和排序功能。Deno 内置的 Web 标准、工具和原生 TypeScript 支持,以及 Tanstack 强大的数据管理功能相结合,为构建强大的 Web 应用程序开辟了无限可能。