如果你在安装过程中遇到解决依赖的问题:
如果你没有使用 React Native,强制安装 --force
或 --legacy-peer-deps
应该可以解决此问题。如果你使用的是 React Native,则需要使用与你的 React Native 版本兼容的 React 版本。
本教程演示了如何使用 Drizzle ORM、Neon 数据库和 Next.js 构建 Todo app
。
npx create-next-app@latest --typescript
npm i drizzle-orm -D drizzle-kit
npm i @neondatabase/serverless
dotenv
包。npm i dotenv
如果你在安装过程中遇到解决依赖的问题:
如果你没有使用 React Native,强制安装 --force
或 --legacy-peer-deps
应该可以解决此问题。如果你使用的是 React Native,则需要使用与你的 React Native 版本兼容的 React 版本。
登录 Neon 控制台 并导航到“项目”部分。选择一个项目或点击 New Project
按钮创建一个新项目。
你的 Neon 项目附带一个名为 neondb
的即用型 Postgres 数据库。我们将在本教程中使用它。
导航到项目控制台中的“连接详细信息”部分以查找你的数据库连接字符串。它应该类似于以下内容:
postgres://username:password@ep-cool-darkness-123456.us-east-2.aws.neon.tech/neondb
将 DATABASE_URL
环境变量添加到你的 .env
或 .env.local
文件中,你将使用该文件连接到 Neon 数据库。
DATABASE_URL=NEON_DATABASE_CONNECTION_STRING
在 src/db
文件夹中创建一个 drizzle.ts
文件并设置数据库配置:
import { config } from "dotenv";
import { drizzle } from 'drizzle-orm/neon-http';
config({ path: ".env" }); // or .env.local
export const db = drizzle(process.env.DATABASE_URL!);
import { integer, text, boolean, pgTable } from "drizzle-orm/pg-core";
export const todo = pgTable("todo", {
id: integer("id").primaryKey(),
text: text("text").notNull(),
done: boolean("done").default(false).notNull(),
});
这里我们使用 Drizzle ORM 中的数据类型,定义了包含字段 id
、text
和 done
的 todo
表。
Drizzle 配置 - Drizzle 套件 使用的配置文件,包含有关数据库连接、迁移文件夹和模式文件的所有信息。
在项目根目录中创建一个 drizzle.config.ts
文件并添加以下内容:
import { config } from 'dotenv';
import { defineConfig } from "drizzle-kit";
config({ path: '.env' });
export default defineConfig({
schema: "./src/db/schema.ts",
out: "./migrations",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
你可以使用 drizzle-kit generate
命令生成迁移,然后使用 drizzle-kit migrate
命令运行它们。
生成迁移:
npx drizzle-kit generate
这些迁移存储在 drizzle/migrations
目录中,具体位置请参见你的 drizzle.config.ts
文件。此目录将包含更新数据库架构所需的 SQL 文件,以及一个 meta
文件夹,用于存储不同迁移阶段的架构快照。
生成的迁移示例:
CREATE TABLE IF NOT EXISTS "todo" (
"id" integer PRIMARY KEY NOT NULL,
"text" text NOT NULL,
"done" boolean DEFAULT false NOT NULL
);
运行迁移:
npx drizzle-kit migrate
或者,你可以使用 Drizzle 套件推送命令 将更改直接推送到数据库:
npx drizzle-kit push
在此步骤中,我们在 src/actions/todoAction.ts 文件中创建服务器端函数,以处理待办事项的关键操作:
getData
:
addTodo
:
使用提供的文本向数据库添加新的待办事项。
使用 revalidatePath("/")
启动主页的重新验证。
deleteTodo
:
根据待办事项的唯一 ID 从数据库中删除该待办事项。
触发主页的重新验证。
toggleTodo
:
切换待办事项的完成状态,并相应地更新数据库。
操作后重新验证主页。
editTodo
:
修改数据库中由其 ID 标识的待办事项文本。
启动主页的重新验证。
"use server";
import { eq, not } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { db } from "@/db/drizzle";
import { todo } from "@/db/schema";
export const getData = async () => {
const data = await db.select().from(todo);
return data;
};
export const addTodo = async (id: number, text: string) => {
await db.insert(todo).values({
id: id,
text: text,
});
};
export const deleteTodo = async (id: number) => {
await db.delete(todo).where(eq(todo.id, id));
revalidatePath("/");
};
export const toggleTodo = async (id: number) => {
await db
.update(todo)
.set({
done: not(todo.done),
})
.where(eq(todo.id, id));
revalidatePath("/");
};
export const editTodo = async (id: number, text: string) => {
await db
.update(todo)
.set({
text: text,
})
.where(eq(todo.id, id));
revalidatePath("/");
};
在 src/types/todoType.ts
中为待办事项定义一个 TypeScript 类型,并包含以下三个属性:id
属于 number
类型,text
属于 string
类型,done
属于 boolean
类型。此类型名为 todoType
,表示应用中典型待办事项的结构。
export type todoType = {
id: number;
text: string;
done: boolean;
};
src/components/todo.tsx
:创建一个代表单个待办事项的 Todo
组件。它包含显示和编辑待办事项文本、使用复选框将其标记为已完成,以及提供编辑、保存、取消和删除待办事项的操作。src/components/addTodo.tsx
:AddTodo
组件提供了一种简单的表单,用于向 Todo 应用添加新的待办事项。它包含一个用于输入待办事项文本的输入字段和一个用于触发添加新待办事项的按钮。src/components/todos.tsx
:创建 Todos 组件,用于表示 Todo 应用的主界面。它管理待办事项的状态,提供创建、编辑、切换和删除待办事项的功能,并使用 Todo
组件渲染单个待办事项。"use client";
import { ChangeEvent, FC, useState } from "react";
import { todoType } from "@/types/todoType";
interface Props {
todo: todoType;
changeTodoText: (id: number, text: string) => void;
toggleIsTodoDone: (id: number, done: boolean) => void;
deleteTodoItem: (id: number) => void;
}
const Todo: FC<Props> = ({
todo,
changeTodoText,
toggleIsTodoDone,
deleteTodoItem,
}) => {
// State for handling editing mode
const [editing, setEditing] = useState(false);
// State for handling text input
const [text, setText] = useState(todo.text);
// State for handling "done" status
const [isDone, setIsDone] = useState(todo.done);
// Event handler for text input change
const handleTextChange = (e: ChangeEvent<HTMLInputElement>) => {
setText(e.target.value);
};
// Event handler for toggling "done" status
const handleIsDone = async () => {
toggleIsTodoDone(todo.id, !isDone);
setIsDone((prev) => !prev);
};
// Event handler for initiating the edit mode
const handleEdit = () => {
setEditing(true);
};
// Event handler for saving the edited text
const handleSave = async () => {
changeTodoText(todo.id, text);
setEditing(false);
};
// Event handler for canceling the edit mode
const handleCancel = () => {
setEditing(false);
setText(todo.text);
};
// Event handler for deleting a todo item
const handleDelete = () => {
if (confirm("Are you sure you want to delete this todo?")) {
deleteTodoItem(todo.id);
}
};
// Rendering the Todo component
return (
<div className="flex items-center gap-2 p-4 border-gray-200 border-solid border rounded-lg">
{/* Checkbox for marking the todo as done */}
<input
type="checkbox"
className="text-blue-200 rounded-sm h-4 w-4"
checked={isDone}
onChange={handleIsDone}
/>
{/* Input field for todo text */}
<input
type="text"
value={text}
onChange={handleTextChange}
readOnly={!editing}
className={`${
todo.done ? "line-through" : ""
} outline-none read-only:border-transparent focus:border border-gray-200 rounded px-2 py-1 w-full`}
/>
{/* Action buttons for editing, saving, canceling, and deleting */}
<div className="flex gap-1 ml-auto">
{editing ? (
<button
onClick={handleSave}
className="bg-green-600 text-green-50 rounded px-2 w-14 py-1"
>
Save
</button>
) : (
<button
onClick={handleEdit}
className="bg-blue-400 text-blue-50 rounded w-14 px-2 py-1"
>
Edit
</button>
)}
{editing ? (
<button
onClick={handleCancel}
className="bg-red-400 w-16 text-red-50 rounded px-2 py-1"
>
Close
</button>
) : (
<button
onClick={handleDelete}
className="bg-red-400 w-16 text-red-50 rounded px-2 py-1"
>
Delete
</button>
)}
</div>
</div>
);
};
export default Todo;
更新 src/app
文件夹中的 page.tsx
文件,以便从数据库获取待办事项并渲染 Todos
组件:
import { getData } from "@/actions/todoAction";
import Todos from "@/components/todos";
export default async function Home() {
const data = await getData();
return <Todos todos={data} />;
}
本指南使用以下文件结构:
📦 <project root>
├ 📂 migrations
│ ├ 📂 meta
│ └ 📜 0000_heavy_doctor_doom.sql
├ 📂 public
├ 📂 src
│ ├ 📂 actions
│ │ └ 📜 todoActions.ts
│ ├ 📂 app
│ │ ├ 📜 favicon.ico
│ │ ├ 📜 globals.css
│ │ ├ 📜 layout.tsx
│ │ └ 📜 page.tsx
│ ├ 📂 components
│ │ ├ 📜 addTodo.tsx
│ │ ├ 📜 todo.tsx
│ │ └ 📜 todos.tsx
│ └ 📂 db
│ │ ├ 📜 drizzle.ts
│ │ └ 📜 schema.ts
│ └ 📂 types
│ └ 📜 todoType.ts
├ 📜 .env
├ 📜 .eslintrc.json
├ 📜 .gitignore
├ 📜 drizzle.config.ts
├ 📜 next-env.d.ts
├ 📜 next.config.mjs
├ 📜 package-lock.json
├ 📜 package.json
├ 📜 postcss.config.mjs
├ 📜 README.md
├ 📜 tailwind.config.ts
└ 📜 tsconfig.json