一、这个项目是做什么的?
想象一下这个场景:你有一个收录了成千上万个英文单词的数据文件,你想把它变成一个可以在线管理的系统------小编可以增删改查单词书,管理员可以管理账号。这就是我们要做的项目。
它包含两个部分:
- 后台管理系统:给管理员用的网页,用来管理单词书和账号
- 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 });
这段代码做了三件事:
- 从环境变量读取数据库连接地址
- 创建一个数据库客户端(相当于打开了一条通往数据库的通道)
- 用 Drizzle 包装这个客户端,方便我们后续操作
这里有一个细节:{ prepare: false } 是为了禁用 PostgreSQL 的预编译语句功能。在 Supabase 的连接池模式下,这个设置可以避免一些兼容性问题。
五、Drizzle ORM:用对象操作数据库
5.1 什么是 ORM?
ORM 是 Object Relational Mapping(对象关系映射)的缩写。简单来说,它做的事情就是:
数据库里的一行记录 → 代码里的一个对象
举个例子。假设数据库里有一个 users 表:
| id | name | |
|---|---|---|
| 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,就自动生成一个随机 UUIDname: text("name").notNull():name字段不能为空email: text("email").notNull().unique():email不能为空,而且在整个表中不能重复createdAt和updatedAt:分别记录创建时间和更新时间,$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)表示一个会话只属于一个管理员fields和references指定了外键关系: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 编程助手也内置了对约定式提交的支持,会自动生成符合规范的提交信息。
八、整合起来:完整的流程
现在我们把所有知识点串起来,看看一个完整的开发流程是怎样的:
-
创建 Next.js 项目
bash
perlnpx create-next-app@latest my-app -
在 Supabase 创建数据库
- 注册 Supabase 账号
- 创建一个新项目
- 获取数据库连接地址
-
配置环境变量
- 在
.env文件中写入DATABASE_URL
- 在
-
安装 Drizzle
bash
npm install drizzle-orm postgres npm install -D drizzle-kit -
定义 Schema
- 在
db/schema.ts中定义数据表结构
- 在
-
生成并执行迁移
bash
npx drizzle-kit generate npx drizzle-kit migrate -
安装 shadcn/ui
bash
kotlinnpx shadcn-ui@latest init npx shadcn-ui@latest add button -
开发页面
- 在
app/目录下创建页面 - 用 shadcn/ui 组件搭建界面
- 用 Drizzle 操作数据库
- 在
-
提交代码
bash
scssgit commit -m "feat(admin): 完成管理员登录功能"
九、总结
这篇文章我们从一个实际项目出发,梳理了以下知识点:
- Next.js:React 框架,提供路由、服务端渲染、API 路由等能力
- Supabase:BaaS 平台,提供托管的 PostgreSQL 数据库
- Drizzle ORM:用 JavaScript 对象操作数据库,无需写 SQL
- shadcn/ui:可定制的 UI 组件库,源码直接复制到项目中
- 约定式提交:规范 Git 提交信息,让项目历史更清晰
这套技术组合的特点是:开发效率高、类型安全、部署成本低。尤其适合中小型项目和快速原型开发。希望这篇文章能帮你理清思路,动手搭建自己的管理后台。