本教程演示如何使用 Drizzle ORM 和 Neon 数据库 以及 Next.js 构建 Todo 应用
。
This guide assumes familiarity with:
你应该有一个现有的 Next.js 项目,或者使用以下命令创建一个新项目:
npx create-next-app@latest --typescript
npm i drizzle-orm
npm i -D drizzle-kit
yarn add drizzle-orm
yarn add -D drizzle-kit
pnpm add drizzle-orm
pnpm add -D drizzle-kit
bun add drizzle-orm
bun add -D drizzle-kit
npm i @neondatabase/serverless
yarn add @neondatabase/serverless
pnpm add @neondatabase/serverless
bun add @neondatabase/serverless
你应该已经安装了 dotenv
包用于管理环境变量。
npm i dotenv
yarn add dotenv
pnpm add dotenv
bun add dotenv
IMPORTANT
如果在安装过程中遇到依赖项解析问题:
如果你没有使用 React Native,通过强制使用 --force
或 --legacy-peer-deps
来解决该问题。如果你在使用 React Native,则需要使用与 React Native 版本兼容的确切 React 版本。
设置 Neon 和 Drizzle ORM
创建一个新的 Neon 项目 登录到 Neon 控制台 并导航到项目部分。选择一个项目或点击 新建项目
按钮以创建一个新项目。
你的 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
连接 Drizzle ORM 到你的数据库 在 src/db
文件夹中创建 drizzle.ts
文件,并设置你的数据库配置:
import { config } from "dotenv" ;
import { drizzle } from 'drizzle-orm/neon-http' ;
config ({ path : ".env" }); // 或 .env.local
export const db = drizzle ( process . env . DATABASE_URL ! );
声明 todo 模式 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 的数据类型定义 todo
表,其中包含字段 id
、text
和 done
。
设置 Drizzle 配置文件 Drizzle 配置 - 这是一个用于 Drizzle Kit 的配置文件,包含有关你的数据库连接、迁移文件夹和模式文件的所有信息。
在项目根目录中创建一个 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
IMPORTANT
Push 命令适合于需要快速测试新模式设计或在本地开发环境中更改的情况,允许快速迭代,而无需管理迁移文件的开销。
建立服务器端函数
在本步骤中,我们在 src/actions/todoAction.ts 文件中建立服务器端函数,以处理 todo 项目的重要操作:
getData
:
addTodo
:
向数据库添加带有提供文本的新 todo 项目。
使用 revalidatePath("/")
触发主页的重新验证。
deleteTodo
:
根据唯一 ID 从数据库中删除一个 todo 项目。
触发主页的重新验证。
toggleTodo
:
切换一个 todo 项目的完成状态,相应地更新数据库。
在操作之后重新验证主页。
editTodo
:
修改数据库中由 ID 标识的 todo 项目的文本。
启动主页的重新验证。
"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 ( "/" );
};
使用 Next.js 设置主页
定义一个 TypeScript 类型 在 src/types/todoType.ts
中定义一个表示 todo 项目的 TypeScript 类型,包含三个属性: id
类型为 number
、 text
类型为 string
和 done
类型为 boolean
。这个类型命名为 todoType
,表示你应用中一个典型 todo 项目的结构。
export type todoType = {
id : number ;
text : string ;
done : boolean ;
};
为 todo 应用创建主页
src/components/todo.tsx
:
创建一个 Todo
组件,表示单个 todo 项目。它包括用于显示和编辑 todo 文本、用复选框标记为完成的功能,以及提供编辑、保存、取消和删除 todo 的操作。
src/components/addTodo.tsx
:
AddTodo
组件提供了一个简单的表单,用于向 Todo 应用添加新的 todo 项目。它包括一个输入字段用于输入 todo 文本,以及一个按钮用于触发新 todo 的添加。
src/components/todos.tsx
:
创建 Todos 组件,表示 Todo 应用的主要界面。它管理 todo 项目的状态,提供创建、编辑、切换和删除 todo 的功能,并使用 Todo
组件渲染各个 todo 项目。
todo.tsx
addTodo.tsx
todos.tsx
"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 ,
}) => {
// 处理编辑模式的状态
const [ editing , setEditing ] = useState ( false );
// 处理文本输入的状态
const [ text , setText ] = useState ( todo .text);
// 处理完成状态的状态
const [ isDone , setIsDone ] = useState ( todo .done);
// 文本输入变化的事件处理
const handleTextChange = (e : ChangeEvent < HTMLInputElement >) => {
setText ( e . target .value);
};
// 切换完成状态的事件处理
const handleIsDone = async () => {
toggleIsTodoDone ( todo .id , ! isDone);
setIsDone ((prev) => ! prev);
};
// 启动编辑模式的事件处理
const handleEdit = () => {
setEditing ( true );
};
// 保存编辑文本的事件处理
const handleSave = async () => {
changeTodoText ( todo .id , text);
setEditing ( false );
};
// 取消编辑模式的事件处理
const handleCancel = () => {
setEditing ( false );
setText ( todo .text);
};
// 删除 todo 项目的事件处理
const handleDelete = () => {
if ( confirm ( "你确定要删除这个 todo 吗?" )) {
deleteTodoItem ( todo .id);
}
};
// 渲染 Todo 组件
return (
< div className = "flex items-center gap-2 p-4 border-gray-200 border-solid border rounded-lg" >
{ /* 用于标记 todo 为完成的复选框 */ }
< input
type = "checkbox"
className = "text-blue-200 rounded-sm h-4 w-4"
checked = {isDone}
onChange = {handleIsDone}
/>
{ /* todo 文本的输入字段 */ }
< 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` }
/>
{ /* 编辑、保存、取消和删除的操作按钮 */ }
< 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"
>
保存
</ button >
) : (
< button
onClick = {handleEdit}
className = "bg-blue-400 text-blue-50 rounded w-14 px-2 py-1"
>
编辑
</ button >
)}
{editing ? (
< button
onClick = {handleCancel}
className = "bg-red-400 w-16 text-red-50 rounded px-2 py-1"
>
关闭
</ button >
) : (
< button
onClick = {handleDelete}
className = "bg-red-400 w-16 text-red-50 rounded px-2 py-1"
>
删除
</ button >
)}
</ div >
</ div >
);
};
export default Todo;
"use client" ;
import { ChangeEvent , FC , useState } from "react" ;
interface Props {
createTodo : (value : string ) => void ;
}
const AddTodo : FC < Props > = ({ createTodo }) => {
// 处理输入值的状态
const [ input , setInput ] = useState ( "" );
// 输入变化的事件处理
const handleInput = (e : ChangeEvent < HTMLInputElement >) => {
setInput ( e . target .value);
};
// 添加新 todo 的事件处理
const handleAdd = async () => {
createTodo (input);
setInput ( "" );
};
// 渲染 AddTodo 组件
return (
< div className = "w-full flex gap-1 mt-2" >
{ /* 输入新 todo 文本的输入字段 */ }
< input
type = "text"
className = "w-full px-2 py-1 border border-gray-200 rounded outline-none"
onChange = {handleInput}
value = {input}
/>
{ /* 添加新 todo 的按钮 */ }
< button
className = "flex items-center justify-center bg-green-600 text-green-50 rounded px-2 h-9 w-14 py-1"
onClick = {handleAdd}
>
添加
</ button >
</ div >
);
};
export default AddTodo;
"use client" ;
import { FC , useState } from "react" ;
import { todoType } from "@/types/todoType" ;
import Todo from "./todo" ;
import AddTodo from "./addTodo" ;
import { addTodo , deleteTodo , editTodo , toggleTodo } from "@/actions/todoAction" ;
interface Props {
todos : todoType [];
}
const Todos : FC < Props > = ({ todos }) => {
// 管理 todo 项目列表的状态
const [ todoItems , setTodoItems ] = useState < todoType []>(todos);
// 创建新 todo 项目的函数
const createTodo = (text : string ) => {
const id = ( todoItems .at ( - 1 )?.id || 0 ) + 1 ;
addTodo (id , text);
setTodoItems ((prev) => [ ... prev , { id : id , text , done : false }]);
};
// 更改 todo 项目文本的函数
const changeTodoText = (id : number , text : string ) => {
setTodoItems ((prev) =>
prev .map ((todo) => ( todo .id === id ? { ... todo , text } : todo))
);
editTodo (id , text);
};
// 切换 todo 项目完成状态的函数
const toggleIsTodoDone = (id : number ) => {
setTodoItems ((prev) =>
prev .map ((todo) => ( todo .id === id ? { ... todo , done : ! todo .done } : todo))
);
toggleTodo (id);
};
// 删除 todo 项目的函数
const deleteTodoItem = (id : number ) => {
setTodoItems ((prev) => prev .filter ((todo) => todo .id !== id));
deleteTodo (id);
};
// 渲染 Todo 列表组件
return (
< main className = "flex mx-auto max-w-xl w-full min-h-screen flex-col items-center p-16" >
< div className = "text-5xl font-medium" >待办事项应用</ div >
< div className = "w-full flex flex-col mt-8 gap-2" >
{ /* 遍历 todoItems,并为每个 todo 渲染 Todo 组件 */ }
{ todoItems .map ((todo) => (
< Todo
key = { todo .id}
todo = {todo}
changeTodoText = {changeTodoText}
toggleIsTodoDone = {toggleIsTodoDone}
deleteTodoItem = {deleteTodoItem}
/>
))}
</ div >
{ /* 为创建新 todos 添加 Todo 组件 */ }
< AddTodo createTodo = {createTodo} />
</ main >
);
};
export default Todos;
更新 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} />;
}
基本文件结构
本指南使用以下文件结构:
📦 <项目根目录>
├ 📂 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