使用 Neon Postgres 构建的 Todo 应用

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

本指南假定您熟悉
  • 您应该有一个现有的 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
重要提示

如果您在安装过程中遇到依赖项解析问题

如果您没有使用 React Native,强制安装(使用 --force--legacy-peer-deps)应该能解决这个问题。如果您正在使用 React Native,那么您需要使用与您的 React Native 版本兼容的精确 React 版本。

设置 Neon 和 Drizzle ORM

创建一个新的 Neon 项目

登录 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

将 Drizzle ORM 连接到您的数据库

src/db 文件夹中创建 drizzle.ts 文件并设置您的数据库配置

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!);

声明 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(),
});

这里我们定义了 todo 表,包含字段 idtextdone,并使用 Drizzle ORM 的数据类型。

设置 Drizzle 配置文件

Drizzle config - 由 Drizzle Kit 使用的配置文件,包含有关你的数据库连接、迁移文件夹和 schema 文件的所有信息。

在项目的根目录中创建 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
重要提示
Push 命令适用于您需要在本地开发环境中快速测试新 schema 设计或更改的情况,它允许快速迭代而无需管理迁移文件的开销。

建立服务端函数

在此步骤中,我们在 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("/");
};
展开

使用 Next.js 设置首页

定义 TypeScript 类型

src/types/todoType.ts 中定义一个 todo 项目的 TypeScript 类型,包含三个属性:类型为 numberid,类型为 stringtext,以及类型为 booleandone。此类型名为 todoType,表示您应用程序中典型 todo 项目的结构。

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

为待办事项应用创建首页

  1. src/components/todo.tsx 创建一个 Todo 组件,表示单个待办事项。它包括显示和编辑待办事项文本、使用复选框标记完成状态,以及提供编辑、保存、取消和删除待办事项的功能。
  2. src/components/addTodo.tsx AddTodo 组件提供了一个简单的表单,用于向待办事项应用添加新的待办事项。它包括一个用于输入待办事项文本的输入字段和一个用于触发添加新待办事项的按钮。
  3. src/components/todos.tsx 创建 Todos 组件,代表待办事项应用的主要界面。它管理待办事项的状态,提供创建、编辑、切换和删除待办事项的功能,并使用 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,
}) => {
  // 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 组件

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} />;
}

基本文件结构

本指南使用以下文件结构

📦 <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