Become a Gold Sponsor

使用 Neon Postgres 的 Todo 应用

本教程演示如何使用 Drizzle ORMNeon 数据库 以及 Next.js 构建 Todo 应用

This guide assumes familiarity with:
  • 你应该有一个现有的 Next.js 项目,或者使用以下命令创建一个新项目:
npx create-next-app@latest --typescript
  • 你应该已经安装了 Drizzle ORM 和 Drizzle kit。可以通过以下命令来安装:
npm
yarn
pnpm
bun
npm i drizzle-orm
npm i -D drizzle-kit
npm
yarn
pnpm
bun
npm i @neondatabase/serverless
  • 你应该已经安装了 dotenv 包用于管理环境变量。
npm
yarn
pnpm
bun
npm i 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 文件,并设置你的数据库配置:

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 模式

src/db/schema.ts
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 表,其中包含字段 idtextdone

设置 Drizzle 配置文件

Drizzle 配置 - 这是一个用于 Drizzle Kit 的配置文件,包含有关你的数据库连接、迁移文件夹和模式文件的所有信息。

在项目根目录中创建一个 drizzle.config.ts 文件,并添加以下内容:

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 项目的重要操作:

  1. getData:
    • 从数据库获取所有现存的 todo 项目。
  2. addTodo:
    • 向数据库添加带有提供文本的新 todo 项目。
    • 使用 revalidatePath("/") 触发主页的重新验证。
  3. deleteTodo:
    • 根据唯一 ID 从数据库中删除一个 todo 项目。
    • 触发主页的重新验证。
  4. toggleTodo:
    • 切换一个 todo 项目的完成状态,相应地更新数据库。
    • 在操作之后重新验证主页。
  5. editTodo:
    • 修改数据库中由 ID 标识的 todo 项目的文本。
    • 启动主页的重新验证。
src/actions/todoAction.ts
"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("/");
};
Expand

使用 Next.js 设置主页

定义一个 TypeScript 类型

src/types/todoType.ts 中定义一个表示 todo 项目的 TypeScript 类型,包含三个属性: id 类型为 numbertext 类型为 stringdone 类型为 boolean。这个类型命名为 todoType,表示你应用中一个典型 todo 项目的结构。

src/types/todoType.ts
export type todoType = {
  id: number;
  text: string;
  done: boolean;
};

为 todo 应用创建主页

  1. src/components/todo.tsx: 创建一个 Todo 组件,表示单个 todo 项目。它包括用于显示和编辑 todo 文本、用复选框标记为完成的功能,以及提供编辑、保存、取消和删除 todo 的操作。
  2. src/components/addTodo.tsx: AddTodo 组件提供了一个简单的表单,用于向 Todo 应用添加新的 todo 项目。它包括一个输入字段用于输入 todo 文本,以及一个按钮用于触发新 todo 的添加。
  3. 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;
Expand

更新 src/app/page.tsx 文件以从数据库获取 todo 项目并渲染 Todos 组件:

src/app/page.tsx
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