搭建一个单词管理后台:Next.js + Supabase + Drizzle

一、这个项目是做什么的?

想象一下这个场景:你有一个收录了成千上万个英文单词的数据文件,你想把它变成一个可以在线管理的系统------小编可以增删改查单词书,管理员可以管理账号。这就是我们要做的项目。

它包含两个部分:

  • 后台管理系统:给管理员用的网页,用来管理单词书和账号
  • H5 应用:给普通用户用的手机端页面(这部分先不展开)

这套方案可以复用到很多管理类项目上,比如商品管理、内容管理、用户管理等。接下来我会一步步拆解整个技术栈和实现思路。

二、技术栈速览

先看一下我们用了哪些东西:

技术 作用 通俗解释
Next.js 前端框架 负责网页的渲染和路由
Supabase 云端数据库 在线的 PostgreSQL 数据库,不用自己搭服务器
Drizzle ORM 数据库操作工具 用 JavaScript 对象操作数据库,不用写 SQL
shadcn/ui UI 组件库 提供按钮、输入框等现成的界面组件
Tailwind CSS CSS 框架 用类名写样式,不用写传统 CSS

下面我们逐一细说。

三、Next.js:不只是 React

3.1 React 和 Next.js 的关系

React 是一个用来构建用户界面的 JavaScript 库。你可以把它想象成一套"乐高积木",每一块积木就是一个组件(比如一个按钮、一个输入框)。你把积木拼在一起,就形成了一个页面。

但 React 本身只负责"怎么拼积木",不关心"怎么把拼好的页面发给用户"。这时候就需要 Next.js 来帮忙了。

Next.js 是一个 React 框架,它在 React 的基础上增加了:

  • 路由系统:你创建一个文件,它就自动对应一个网址
  • 服务端渲染:页面可以在服务器上提前生成好 HTML,再发给浏览器
  • API 路由:可以在同一个项目里写后端接口

3.2 项目结构

一个典型的 Next.js 项目长这样:

text

bash 复制代码
my-app/
├── app/                # 页面文件(App Router)
│   ├── page.tsx       # 首页
│   └── admin/         # 后台管理页面
├── components/         # 可复用的组件
├── lib/               # 工具函数和数据库配置
├── public/            # 静态文件
└── .env              # 环境变量(数据库连接信息等)

3.3 环境变量

环境变量是用来存放敏感信息的,比如数据库地址、密钥等。这些信息不应该写死在代码里,而是放在 .env 文件中:

text

ini 复制代码
DATABASE_URL=postgresql://用户名:密码@地址:端口/数据库名

Next.js 会自动读取 .env 文件中的变量,通过 process.env.DATABASE_URL 就能拿到。

四、Supabase:不用自己搭数据库

4.1 什么是 BaaS?

BaaS 的全称是 Backend as a Service(后端即服务)。传统的开发方式是:你自己买服务器、装数据库、配置安全策略、处理备份......一套下来非常繁琐。

BaaS 把这一切都打包好了。你只需要注册一个账号,点几下鼠标,一个生产可用的数据库就建好了。Supabase 就是这样一个平台。

4.2 Supabase 的特点

Supabase 的核心是一个 PostgreSQL 数据库。PostgreSQL 是一种关系型数据库,数据以表格的形式存储,表和表之间可以建立关联。

Supabase 还提供了:

  • 自动生成的 API:不用写后端代码就能对数据库进行增删改查
  • 行级安全策略:可以控制哪个人能看到哪条数据
  • 实时订阅:数据变化时前端能自动收到通知

对于我们这个项目,Supabase 最方便的一点是:它已经帮我们把数据库搭好了,我们只需要拿到连接地址就行。

4.3 连接 Supabase

在项目里连接 Supabase 的代码如下:

typescript

javascript 复制代码
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema";

const databaseUrl = process.env.DATABASE_URL;

if (!databaseUrl) {
  throw new Error("DATABASE_URL 环境变量未设置");
}

const client = postgres(databaseUrl, { prepare: false });
export const db = drizzle({ client, schema });

这段代码做了三件事:

  1. 从环境变量读取数据库连接地址
  2. 创建一个数据库客户端(相当于打开了一条通往数据库的通道)
  3. 用 Drizzle 包装这个客户端,方便我们后续操作

这里有一个细节:{ prepare: false } 是为了禁用 PostgreSQL 的预编译语句功能。在 Supabase 的连接池模式下,这个设置可以避免一些兼容性问题。

五、Drizzle ORM:用对象操作数据库

5.1 什么是 ORM?

ORM 是 Object Relational Mapping(对象关系映射)的缩写。简单来说,它做的事情就是:

数据库里的一行记录 → 代码里的一个对象

举个例子。假设数据库里有一个 users 表:

id name email
1 张三 zhangsan@example.com

在 ORM 的世界里,你可以这样操作:

typescript

perl 复制代码
// 查询
const user = await db.select().from(users).where(eq(users.id, "1"));
// user 是一个对象:{ id: "1", name: "张三", email: "zhangsan@example.com" }

// 新增
const newUser = await db.insert(users).values({
  name: "李四",
  email: "lisi@example.com"
});

// 更新
await db.update(users)
  .set({ name: "王五" })
  .where(eq(users.id, "1"));

你看,全程没有写一行 SQL。ORM 帮我们把 JavaScript 代码翻译成了 SQL 语句发给数据库。

5.2 为什么选 Drizzle?

市面上有很多 ORM 工具,比如 Prisma、TypeORM、Sequelize 等。Drizzle 的特点是:

  • 轻量:核心代码很小,不会给项目增加太多体积
  • 类型安全:如果你用 TypeScript,Drizzle 能自动推导出查询结果的类型
  • 灵活:既支持 ORM 风格的操作,也支持写原生 SQL

5.3 定义数据表结构(Schema)

在使用 Drizzle 之前,我们需要先定义数据表的结构。这个定义文件叫 schema.ts

typescript

javascript 复制代码
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { randomUUID } from "node:crypto";

// 管理员表
export const adminUsers = pgTable("admin_users", {
  id: text("id")
    .primaryKey()
    .$defaultFn(() => randomUUID()),
  name: text("name").notNull(),
  email: text("email").notNull().unique(),
  passwordHash: text("password_hash").notNull(),
  createdAt: timestamp("created_at", { withTimezone: true })
    .notNull()
    .default(sql`now()`),
  updatedAt: timestamp("updated_at", { withTimezone: true })
    .notNull()
    .default(sql`now()`)
    .$onUpdate(() => new Date()),
});

我们来逐行解读这段代码的含义:

  • pgTable("admin_users", {...}):告诉 Drizzle,我们要映射数据库中一个叫 admin_users 的表
  • id: text("id").primaryKey():定义一个叫 id 的字段,类型是文本,它是主键
  • .$defaultFn(() => randomUUID()):当插入新记录时,如果没有提供 id,就自动生成一个随机 UUID
  • name: text("name").notNull()name 字段不能为空
  • email: text("email").notNull().unique()email 不能为空,而且在整个表中不能重复
  • createdAtupdatedAt:分别记录创建时间和更新时间,$onUpdate 会在记录更新时自动刷新时间

5.4 枚举类型

schema.ts 中还有一个枚举的定义:

typescript

ini 复制代码
export const adminRole = pgEnum("admin_role", ["super", "admin"]);

枚举(Enum)是一种数据类型,它的值只能从指定的列表里选。这里我们定义了两种角色:

  • super:超级管理员,拥有所有权限
  • admin:普通管理员,权限有限

adminUsers 表中,role 字段就使用了这个枚举:

typescript

css 复制代码
role: adminRole("role").notNull().default("admin"),

这意味着每个管理员的角色要么是 super,要么是 admin,不能是其他值,默认是 admin

5.5 表关联(Relations)

管理员和管理员的会话(登录态)是一对多的关系:一个管理员可以有多个会话。

在 Drizzle 中,我们用 relations 来定义这种关联:

typescript

javascript 复制代码
import { relations } from "drizzle-orm";

export const adminUsersRelations = relations(adminUsers, ({ many }) => ({
  sessions: many(adminSessions),
}));

export const adminSessionsRelations = relations(adminSessions, ({ one }) => ({
  user: one(adminUsers, {
    fields: [adminSessions.userId],
    references: [adminUsers.id],
  }),
}));
  • many(adminSessions) 表示一个管理员对应多个会话
  • one(adminUsers) 表示一个会话只属于一个管理员
  • fieldsreferences 指定了外键关系:adminSessions.userId 指向 adminUsers.id

这样定义之后,当你在代码中查询一个管理员时,可以一并把它的所有会话也查出来。

5.6 数据库迁移

定义好 Schema 之后,还需要把这张表真正创建到数据库里。这个过程叫迁移(Migration)

Drizzle 提供了一个命令行工具 drizzle-kit,可以帮我们生成和执行迁移:

bash

perl 复制代码
# 生成迁移文件
npx drizzle-kit generate

# 执行迁移(创建表)
npx drizzle-kit migrate

# 推送变更到数据库(开发时常用)
npx drizzle-kit push

generate 命令会比较你当前的 Schema 和数据库里已有的结构,生成一个差异文件。migrate 则负责执行这个差异文件。

六、shadcn/ui:不用重复造轮子

6.1 组件库是什么?

在网页开发中,有很多界面元素是重复出现的:按钮、输入框、下拉菜单、弹窗......每次都要从头写一遍非常耗时。组件库就是把这些常用元素提前封装好,你直接拿来用就行。

6.2 shadcn/ui 的特别之处

传统的组件库(比如 Ant Design、Element UI)是以 npm 包的形式提供的。你安装之后,组件是"黑盒"的------你看不到源码,也很难修改样式。

shadcn/ui 的做法不一样:它不是以 npm 包的形式提供,而是通过命令行把组件源码直接复制到你的项目里:

bash

sql 复制代码
npx shadcn-ui@latest add button

执行这个命令后,components/ui/button.tsx 文件就会被创建,里面就是按钮的完整源码。你可以随意修改它。

这样做的好处是:

  • 完全可控:源码在你手里,想怎么改就怎么改
  • 按需加载:只用到了哪些组件,项目里就只包含哪些
  • AI 友好:因为代码是语义化的,AI 工具能更好地理解

6.3 与 Tailwind CSS 的配合

shadcn/ui 基于 Tailwind CSS 构建。Tailwind 是一个"原子化"的 CSS 框架,它提供了大量工具类:

html

xml 复制代码
<!-- 传统 CSS -->
<button class="btn-primary">点我</button>
<style>
  .btn-primary {
    background-color: blue;
    color: white;
    padding: 8px 16px;
    border-radius: 4px;
  }
</style>

<!-- Tailwind CSS -->
<button class="bg-blue-500 text-white px-4 py-2 rounded">
  点我
</button>

用 Tailwind,你不需要写单独的 CSS 文件,直接在 HTML 类名里描述样式就行。

七、约定式提交:让 Git 历史更清晰

7.1 什么是约定式提交?

约定式提交(Conventional Commits) 是一套 Git 提交信息的规范。它规定每次提交的信息都要遵循固定的格式:

text

xml 复制代码
<类型>(<范围>): <简短描述>

常见的类型有:

类型 含义
feat 新增功能
fix 修复 bug
docs 文档变更
style 样式变更(不影响代码逻辑)
refactor 代码重构(不改变功能)
test 测试相关
chore 构建工具、依赖等变更

7.2 为什么要用这个规范?

如果没有规范,团队的提交信息可能是这样的:

text

sql 复制代码
修复了问题
改了点东西
wip
update

这些信息对别人(甚至对未来的自己)来说毫无意义。

用了约定式提交之后,提交信息变成这样:

text

scss 复制代码
feat(admin): 添加管理员列表页面
fix(auth): 修复登录 token 过期问题
docs(readme): 更新部署文档

这样每个人都能一眼看出这次提交做了什么、涉及哪个模块。

7.3 实际使用

在项目中,你可以配合 commitlint 等工具来强制执行这个规范。不符合规范的提交会被拒绝。很多 AI 编程助手也内置了对约定式提交的支持,会自动生成符合规范的提交信息。

八、整合起来:完整的流程

现在我们把所有知识点串起来,看看一个完整的开发流程是怎样的:

  1. 创建 Next.js 项目

    bash

    perl 复制代码
    npx create-next-app@latest my-app
  2. 在 Supabase 创建数据库

    • 注册 Supabase 账号
    • 创建一个新项目
    • 获取数据库连接地址
  3. 配置环境变量

    • .env 文件中写入 DATABASE_URL
  4. 安装 Drizzle

    bash

    复制代码
    npm install drizzle-orm postgres
    npm install -D drizzle-kit
  5. 定义 Schema

    • db/schema.ts 中定义数据表结构
  6. 生成并执行迁移

    bash

    复制代码
    npx drizzle-kit generate
    npx drizzle-kit migrate
  7. 安装 shadcn/ui

    bash

    kotlin 复制代码
    npx shadcn-ui@latest init
    npx shadcn-ui@latest add button
  8. 开发页面

    • app/ 目录下创建页面
    • 用 shadcn/ui 组件搭建界面
    • 用 Drizzle 操作数据库
  9. 提交代码

    bash

    scss 复制代码
    git commit -m "feat(admin): 完成管理员登录功能"

九、总结

这篇文章我们从一个实际项目出发,梳理了以下知识点:

  • Next.js:React 框架,提供路由、服务端渲染、API 路由等能力
  • Supabase:BaaS 平台,提供托管的 PostgreSQL 数据库
  • Drizzle ORM:用 JavaScript 对象操作数据库,无需写 SQL
  • shadcn/ui:可定制的 UI 组件库,源码直接复制到项目中
  • 约定式提交:规范 Git 提交信息,让项目历史更清晰

这套技术组合的特点是:开发效率高、类型安全、部署成本低。尤其适合中小型项目和快速原型开发。希望这篇文章能帮你理清思路,动手搭建自己的管理后台。

相关推荐
柒和远方1 小时前
V077:Next.js 后台的认证与权限防线:首个超级管理员的事务锁初始化、会话令牌哈希,与四道管理员保护规则
orm·next.js
半个落月1 小时前
Next.js 16 笔记应用实战:Redis 数据链路与组件两版拆分详解
前端·redis·next.js
增量星球1 小时前
embedding专题之向量化革命:从关键词到语义检索
数据库·embedding
夕除1 小时前
redis--008
java·jvm·数据库
烬羽1 小时前
一个博客系统,为什么要拆成 6 张表?
数据库·mysql
程序员-Benothing1 小时前
MySQL 中的日志类型有哪些?binlog、redo log 和 undo log 的作用和区别是什么?
数据库·mysql
XuCoder1 小时前
你写的每条 SQL 都没加过锁,可 MySQL 凭什么不怕两个事务打架?
数据库·后端
2301_800954991 小时前
MySQL 数据库基础:数据类型、查询与备份恢复
数据库·mysql·oracle
起名真的太难了1 小时前
Mysql的数据类型,查询与备份操作
数据库·sql·mysql