如果您在安装过程中遇到依赖项解析问题
如果您没有使用 React Native,强制安装(使用 --force
或 --legacy-peer-deps
)应该能解决这个问题。如果您正在使用 React Native,那么您需要使用与您的 React Native 版本兼容的精确 React 版本。
本教程演示了如何使用 Drizzle ORM、Neon 数据库和 Next.js 构建 Todo 应用
。
npx create-next-app@latest --typescript
npm i drizzle-orm
npm i -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 数据库。我们将在本教程中使用它。
在项目控制台导航到 连接详情 (Connection Details) 部分以找到您的数据库连接字符串。它应该类似于这样
postgres://username:[email protected]/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(),
});
这里我们定义了 todo
表,包含字段 id
、text
和 done
,并使用 Drizzle ORM 的数据类型。
Drizzle config - 由 Drizzle Kit 使用的配置文件,包含有关你的数据库连接、迁移文件夹和 schema 文件的所有信息。
在项目的根目录中创建 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 kit push 命令 将更改直接推送到数据库
npx drizzle-kit push
在此步骤中,我们在 src/actions/todoAction.ts 文件中建立服务端函数,以处理 todo 项目的关键操作
getData
:
addTodo
:
revalidatePath("/")
触发首页重新验证。deleteTodo
:
toggleTodo
:
editTodo
:
"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
中定义一个 todo 项目的 TypeScript 类型,包含三个属性:类型为 number
的 id
,类型为 string
的 text
,以及类型为 boolean
的 done
。此类型名为 todoType
,表示您应用程序中典型 todo 项目的结构。
export type todoType = {
id: number;
text: string;
done: boolean;
};
src/components/todo.tsx
: 创建一个 Todo
组件,表示单个待办事项。它包括显示和编辑待办事项文本、使用复选框标记完成状态,以及提供编辑、保存、取消和删除待办事项的功能。src/components/addTodo.tsx
: AddTodo
组件提供了一个简单的表单,用于向待办事项应用添加新的待办事项。它包括一个用于输入待办事项文本的输入字段和一个用于触发添加新待办事项的按钮。src/components/todos.tsx
: 创建 Todos 组件,代表待办事项应用的主要界面。它管理待办事项的状态,提供创建、编辑、切换和删除待办事项的功能,并使用 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
文件,从数据库获取 todo 项目并渲染 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