在您自己的服务器上部署 TanStack Start 与 SQLite
Source: Dev.to
为什么选择 TanStack Start + SQLite?
- 简洁 – SQLite 与你的应用运行在同一进程中,消除了网络延迟和复杂的数据库集群。开发只需
pnpm install,即可开始编码。 - 性能 – 读取和写入直接命中本地文件系统,速度往往超过网络附加的数据库,能够满足典型的网页请求。
- 易于部署 – 将应用服务器和数据库文件(挂载为持久卷)一起部署到单个地区,使用 Haloy 即可。
你将构建的内容
一个使用以下技术的全栈 React 应用:
- TanStack Start – 具备文件路由和服务器函数的 React 元框架
- SQLite – 轻量级、基于文件的数据库
- Drizzle ORM – 支持 TypeScript 的类型安全数据库查询 ORM
- Haloy – 简单的自有服务器部署方案
前置条件
- 已安装 Node.js 20+
- 已安装 Haloy(参见 Haloy 快速入门)
- 一台 Linux 服务器(VPS 或独立服务器)
- 一个域名或子域名
- 基本的 React 与 TypeScript 使用经验
项目初始化
mkdir my-tanstack-app
cd my-tanstack-app
pnpm init
tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx",
"moduleResolution": "Bundler",
"module": "ESNext",
"target": "ES2022",
"skipLibCheck": true,
"strictNullChecks": true
}
}
安装核心依赖
pnpm add @tanstack/react-start @tanstack/react-router react react-dom nitro
安装开发依赖
pnpm add -D vite @vitejs/plugin-react typescript @types/react @types/react-dom @types/node vite-tsconfig-paths
安装 Drizzle 与 SQLite 客户端
pnpm add drizzle-orm @libsql/client dotenv drizzle-kit
注意:
drizzle-kit作为生产依赖(而非-D)安装,因为它必须在 Docker 容器中可用,以在启动时运行迁移。
package.json 脚本
{
// ...
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"start": "node .output/server/index.mjs",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate"
}
}
配置
vite.config.ts
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";
import tsConfigPaths from "vite-tsconfig-paths";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import viteReact from "@vitejs/plugin-react";
export default defineConfig({
server: {
port: 3000,
},
plugins: [
tsConfigPaths(),
tanstackStart(),
nitro(),
// React 的 Vite 插件必须放在 Start 的 Vite 插件之后
viteReact(),
],
nitro: {},
});
TanStack Start 使用 Nitro 作为服务器引擎。本次部署使用默认的 Node.js 预设,能够完美配合 Haloy。无需额外的 Nitro 配置;空的 nitro: {} 对象即可。
drizzle.config.ts
import { config } from "dotenv";
import { defineConfig } from "drizzle-kit";
config();
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
throw new Error("DATABASE_URL is not set");
}
export default defineConfig({
out: "./drizzle",
schema: "./src/db/schema.ts",
dialect: "sqlite",
dbCredentials: {
url: databaseUrl,
},
});
数据库客户端 (src/db/index.ts)
import "dotenv/config";
import { createClient } from "@libsql/client";
import { drizzle } from "drizzle-orm/libsql";
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
throw new Error("DATABASE_URL is not set");
}
const client = createClient({ url: databaseUrl });
const db = drizzle({ client });
export { client, db };
数据模型 (src/db/schema.ts)
import { sql } from "drizzle-orm";
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const todos = sqliteTable("todos", {
id: integer("id", { mode: "number" }).primaryKey({
autoIncrement: true,
}),
title: text("title").notNull(),
createdAt: integer("created_at", { mode: "timestamp" }).default(
sql`(unixepoch())`
),
});
本地开发环境变量 (.env)
DATABASE_URL=file:local.db
生成并运行迁移:
pnpm db:generate
pnpm db:migrate
这些命令会在 drizzle/ 目录下创建迁移文件,随后会在生产环境中使用。
路由
src/router.tsx
import { createRouter } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen";
export function getRouter() {
const router = createRouter({
routeTree,
scrollRestoration: true,
defaultNotFoundComponent: () => 404 - not found,
});
return router;
}
注意: 你可能会看到关于
./routeTree.gen未找到的 TypeScript 错误。这是正常现象——TanStack Start 会在你运行开发服务器时自动生成该文件。
根路由 (src/routes/__root.tsx)
///
import {
createRootRoute,
HeadContent,
Outlet,
Scripts,
} from "@tanstack/react-router";
import type { ReactNode } from "react";
export const Route = createRootRoute({
head: () => ({
meta: [
{ charSet: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
{ title: "TanStack Start Starter" },
],
}),
component: RootComponent,
});
function RootComponent() {
return (
);
}
function RootDocument({ children }: Readonly) {
return (
{children}
);
}
Todo 路由 (src/routes/index.tsx)
import { createFileRoute, useRouter } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";
import { eq } from "drizzle-orm";
import { db } from "../db";
import { todos } from "../db/schema";
const getTodos = createServerFn({
method: "GET",
}).handler(async () => await db.select().from(todos));
const addTodo = createServerFn({ method: "POST" })
.inputValidator((data: FormData) => {
if (!(data instanceof FormData)) {
throw new Error("Expected FormData");
}
return {
title: data.get("title")?.toString() || "",
};
})
.handler(async ({ data }) => {
await db.insert(todos).values({ title: data.title });
});
const deleteTodo = createServerFn({ method: "POST" })
.inputValidator((data: number) => data)
.handler(async ({ data }) => {
await db.delete(todos).where(eq(todos.id, data));
});
export const Route = createFileRoute("/")({
component: RouteComponent,
loader: async () => await getTodos(),
});
function RouteComponent() {
const router = useRouter();
const todos = Route.useLoaderData();
return (
{todos.map((todo) => (
{todo.title}
{
await deleteTodo({ data: todo.id });
router.invalidate();
}}
>
X
))}
## Add todo
{
e.preventDefault();
const form = e.currentTarget;
const data = new FormData(form);
await addTodo({ data });
router.invalidate();
form.reset();
}}
>
Add
);
}
现在,你已经拥有一个完整的 TanStack Start + SQLite 入门项目,可用于本地开发,也可通过 Haloy 部署到生产环境。按照 Haloy 文档构建 Docker 镜像、为 local.db 配置持久卷,并使用自动 HTTPS 暴露服务。祝编码愉快!