# snippets **Repository Path**: training-demo/snippets ## Basic Information - **Project Name**: snippets - **Description**: 使用 Next.js 编写的一个发布代码片段的小网站,以此来掌握这个全栈框架的核心操作。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-03-14 - **Last Updated**: 2026-03-14 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Snippets — Next.js + Prisma v7 + SQLite 一个代码片段管理应用,支持创建、查看、编辑、删除代码片段。使用 Next.js 16 (App Router) + Prisma v7 + SQLite 构建。 ## 技术栈 - **Next.js 16** — React 全栈框架 (App Router, Server Components, Server Actions) - **Prisma v7** — 类型安全的 ORM - **SQLite** — 嵌入式数据库 (通过 better-sqlite3 驱动) - **Tailwind CSS v4** — 原子化 CSS 框架 - **Monaco Editor** — VS Code 同款代码编辑器 - **TypeScript** — 类型安全 ## 项目结构 ``` nextjs-prisma/ ├── actions/ │ └── index.ts # Server Actions (CRUD 操作) ├── app/ │ ├── generated/prisma/ # Prisma Client 生成目录 (git ignored) │ ├── snippets/ │ │ ├── [id]/ │ │ │ ├── edit/ │ │ │ │ └── page.tsx # 编辑页 │ │ │ ├── loading.tsx # 加载骨架屏 │ │ │ ├── not-found.tsx # 404 页面 │ │ │ └── page.tsx # 详情页 │ │ └── new/ │ │ ├── error.tsx # 错误页面 │ │ └── page.tsx # 创建页 │ ├── globals.css │ ├── layout.tsx # 全局布局 (导航栏) │ └── page.tsx # 首页 (列表) ├── components/ │ ├── snippet-del-button.tsx # 删除按钮组件 (客户端) │ └── snippet-edit-form.tsx # 编辑表单组件 (Monaco Editor) ├── lib/ │ └── prisma.ts # Prisma Client 单例 ├── prisma/ │ ├── migrations/ # 数据库迁移文件 │ ├── schema.prisma # 数据模型定义 │ └── seed.ts # 种子数据 ├── prisma.config.ts # Prisma 配置 ├── dev.db # SQLite 数据库文件 ├── .env # 环境变量 └── package.json ``` --- ## 从零开始的完整开发过程 ### 第 1 步:创建 Next.js 项目 ```bash npx create-next-app@latest nextjs-prisma cd nextjs-prisma ``` 创建时选择默认选项:TypeScript、ESLint、Tailwind CSS、App Router。 ### 第 2 步:安装 Prisma 相关依赖 ```bash npm install prisma tsx @types/better-sqlite3 --save-dev npm install @prisma/client @prisma/adapter-better-sqlite3 better-sqlite3 dotenv ``` 各包的作用: | 包名 | 作用 | |------|------| | `prisma` | Prisma CLI (迁移、生成) | | `tsx` | 运行 TypeScript 脚本 (seed) | | `@prisma/client` | Prisma 客户端 | | `@prisma/adapter-better-sqlite3` | SQLite 驱动适配器 | | `better-sqlite3` | SQLite 驱动 | | `dotenv` | 加载 .env 环境变量 | | `@types/better-sqlite3` | 类型定义 | > **Prisma v7 要点:** v7 移除了内置 Rust 查询引擎,所有数据库(包括 SQLite)都必须通过 JavaScript 驱动适配器连接。 ### 第 3 步:初始化 Prisma ```bash npx prisma init --output ../app/generated/prisma ``` `--output` 指定 Prisma Client 生成到 `app/generated/prisma/` 目录。 该命令创建: - `prisma/schema.prisma` — 数据模型定义 - `prisma.config.ts` — Prisma 配置 - `.env` — 环境变量 ### 第 4 步:配置环境变量 编辑 `.env`: ```env DATABASE_URL="file:./dev.db" ``` ### 第 5 步:定义数据模型 编辑 `prisma/schema.prisma`: ```prisma generator client { provider = "prisma-client" output = "../app/generated/prisma" } datasource db { provider = "sqlite" } model Snippet { id Int @id @default(autoincrement()) title String code String } ``` > **Prisma v7 注意:** `datasource` 中不能写 `url`,数据库 URL 统一在 `prisma.config.ts` 中配置。 ### 第 6 步:配置 prisma.config.ts ```typescript import "dotenv/config"; import { defineConfig, env } from "prisma/config"; export default defineConfig({ schema: "prisma/schema.prisma", migrations: { path: "prisma/migrations", seed: "tsx prisma/seed.ts", }, datasource: { url: env("DATABASE_URL"), }, }); ``` ### 第 7 步:运行迁移 + 生成客户端 ```bash npx prisma migrate dev --name init npx prisma generate ``` > **Prisma v7 注意:** `migrate dev` 不会自动执行 `generate`,需要手动运行。 ### 第 8 步:创建种子数据 创建 `prisma/seed.ts`: ```typescript import "dotenv/config"; import { PrismaClient, Prisma } from "../app/generated/prisma/client"; import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3"; const adapter = new PrismaBetterSqlite3({ url: process.env.DATABASE_URL!, }); const prisma = new PrismaClient({ adapter }); const snippetData: Prisma.SnippetCreateInput[] = [ { title: "Hello World", code: 'console.log("Hello World");', }, { title: "Arrow Function", code: "const add = (a: number, b: number) => a + b;", }, { title: "Fetch Data", code: 'const res = await fetch("/api/data");\nconst data = await res.json();', }, ]; async function main() { for (const s of snippetData) { await prisma.snippet.create({ data: s }); } } main(); ``` 运行: ```bash npx prisma db seed ``` > **Prisma v7 注意:** `new PrismaClient()` 不能空参调用,必须传入 `{ adapter }`。独立脚本需要 `import "dotenv/config"` 加载环境变量。 ### 第 9 步:设置 Prisma Client 单例 创建 `lib/prisma.ts`: ```typescript import { PrismaClient } from "../app/generated/prisma/client"; import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3"; const globalForPrisma = global as unknown as { prisma: PrismaClient; }; const adapter = new PrismaBetterSqlite3({ url: process.env.DATABASE_URL!, }); const prisma = globalForPrisma.prisma || new PrismaClient({ adapter }); if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; export default prisma; ``` 挂载到 `global` 防止 Next.js 开发模式热重载创建多个连接。Next.js 会自动加载 `.env`,这里不需要 `import "dotenv/config"`。 ### 第 10 步:创建 Server Actions 创建 `actions/index.ts`,集中管理所有数据库操作: ```typescript "use server"; import prisma from "@/lib/prisma"; import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; export async function createSnippet( formState: { message: string }, formData: FormData, ) { try { const title = formData.get("title"); const code = formData.get("code"); if (typeof title !== "string" || title.length < 3) { return { message: "Title must be longer" }; } if (typeof code !== "string" || code.length < 3) { return { message: "Code must be longer" }; } await prisma.snippet.create({ data: { title, code } }); } catch (err: unknown) { if (err instanceof Error) { return { message: err.message }; } return { message: "Something went wrong..." }; } revalidatePath("/"); redirect("/"); } export async function editSnippet(id: number, code: string) { await prisma.snippet.update({ where: { id }, data: { code }, }); revalidatePath(`/snippets/${id}`); redirect(`/snippets/${id}`); } export async function deleteSnippet(id: number) { await prisma.snippet.delete({ where: { id } }); revalidatePath("/"); redirect("/"); } ``` 关键点: - 文件顶部 `"use server"` 标记所有导出函数为 Server Actions - `revalidatePath` 清除页面缓存,确保生产模式下数据变更后页面能刷新 - `createSnippet` 使用 `useActionState` 兼容的签名 `(prevState, formData)`,支持返回验证错误 ### 第 11 步:全局布局 + 导航栏 编辑 `app/layout.tsx`: ```tsx import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import Link from "next/link"; import "./globals.css"; const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] }); const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] }); export const metadata: Metadata = { title: "Snippets", description: "Create and share code snippets", }; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { return (
{children}
); } ``` ### 第 12 步:首页 — Snippet 列表 编辑 `app/page.tsx`: ```tsx import prisma from "@/lib/prisma"; import Link from "next/link"; export default async function Home() { const snippets = await prisma.snippet.findMany(); return (

All Snippets

+ New Snippet
{snippets.length === 0 ? (

No snippets yet

Create your first snippet to get started

) : (
{snippets.map((snippet) => (
{snippet.title}
View →
))}
)}
); } ``` 这是一个 **Server Component**,直接在服务端查询数据库,无需 API 层。 ### 第 13 步:创建页面 — 带表单验证 创建 `app/snippets/new/page.tsx`: ```tsx "use client"; import * as actions from "@/actions"; import Link from "next/link"; import { useActionState, useState } from "react"; const initState = { message: "", pending: false }; export default function SnippetCreatePage() { const [state, formAction] = useActionState(actions.createSnippet, initState); const [title, setTitle] = useState(""); const [code, setCode] = useState(""); return (
← Back to Snippets

Create a Snippet

setTitle(e.target.value)} />