BetterLink Logo 比邻
切换语言
切换主题

Next.js 15实战:我是如何用一个周末搭建出生产级博客系统的

Next.js 15全栈博客系统实战

引言

我还记得两个月前的某个周五晚上,公司项目刚上线,我打开电脑想学点新东西放松一下。Next.js 15刚发布不久,官方文档看了好几遍,各种教程视频也刷了不少,但总感觉停留在纸上谈兵的阶段——什么Server Actions、App Router,概念都懂,就是不知道实际项目里怎么用。 你肯定也有这种感觉吧?看完教程热血沸腾,关掉浏览器立马懵了,这东西到底能拿来干啥? 所以那个周末,我决定不再看教程了,直接撸起袖子做个真实的项目——一个全栈博客系统。说实话,一开始我也没信心能做出来,毕竟涉及前端、后端、数据库、部署,听起来挺吓人的。但两天后,当我看到自己搭建的博客成功部署到Vercel,Lighthouse性能评分达到96分的时候,那种成就感真的无法形容。 这篇文章我会把整个过程分享给你,不是那种复制粘贴代码就完事的教程,而是真正带你理解为什么要这么做,以及我踩过哪些坑。技术栈是Next.js 15 + Server Actions + Prisma + PostgreSQL,都是生产级的代码,不是玩具项目。 如果你也想在一个周末掌握Next.js全栈开发,那就跟着我的节奏来吧。

为什么选择Next.js 15?

选技术栈这事儿,我纠结了好久。 Next.js 15刚出的时候,我在社区看到有人吐槽”又TM更新了,学不动了”。老实讲,我当时也有点抵触,毕竟14还没完全搞明白呢。但仔细研究了一下新特性后,我发现这次升级是真的香。

Server Actions:告别API Routes的繁琐

以前做全栈开发,最烦的就是写API Routes。创建一个api/posts/route.ts,定义POST方法,处理请求体,返回响应…每次都是这套流程,写到吐。 Server Actions彻底改变了这个玩法。你只需要在函数前加个'use server',然后直接在组件里调用就行了。我第一次看到这个特性时,心里想的是”这不就是把后端代码写前端了吗?“但试了一次后——真香! 举个例子,以前创建博客文章要这么写:

// 老方法:需要创建API Route
// app/api/posts/route.ts
export async function POST(request: Request) {
  const body = await request.json()
  // 一堆处理逻辑...
}
// 前端还要fetch调用
const response = await fetch('/api/posts', {
  method: 'POST',
  body: JSON.stringify(data)
})

现在直接这样:

// app/actions/post-actions.ts
'use server'
export async function createPost(formData: FormData) {
  const title = formData.get('title')
  const content = formData.get('content')
  // 直接操作数据库,是不是很爽?
  return await prisma.post.create({
    data: { title, content }
  })
}

在组件里调用就跟调本地函数一样简单。用个生活化的比喻:以前是你要自己去餐厅取外卖(API Routes),现在是外卖直送到家(Server Actions)。

Turbopack让开发快了一倍不止

Next.js 15把Turbopack从实验性变成了稳定版。官方说本地服务器启动快76.7%,代码更新快96.3%,我一开始以为是营销数字,实际用了后发现——没吹牛。 我的项目大概有30多个组件,用Webpack的话启动要7-8秒,Turbopack直接压缩到2秒以内。改个代码,热更新几乎是瞬间完成。这对开发体验的提升,不是一点半点。

为什么这个技术栈适合博客?

选Next.js做博客,主要看中三点: SSR/SSG天然优势——博客最重要的是SEO,Next.js的服务端渲染和静态生成天生就是为这个场景设计的。Google爬虫直接拿到完整的HTML,比纯客户端渲染的React强太多了。 Prisma的类型安全——我以前用过Mongoose写MongoDB,类型定义全靠手动维护,一不小心就出bug。Prisma直接从schema生成TypeScript类型,写代码的时候有完整的智能提示,基本不会出错。 Server Actions简化开发——不用写API Routes了,代码量至少减少30%。我这个博客系统,如果用传统方式写,估计要多写好几个route.ts文件。

技术栈选型与架构设计

搞清楚为什么后,接下来就是具体怎么做了。

我的完整技术栈

  • 前端: Next.js 15 + TypeScript + Tailwind CSS
  • 后端: Next.js Server Actions + NextAuth.js
  • 数据库: PostgreSQL + Prisma ORM
  • 部署: Vercel 你可能会想,为什么不用MongoDB?其实我一开始也这么想,MongoDB确实更灵活。但后来发现Prisma对PostgreSQL的支持更好,而且关系型数据库在处理博客文章、用户、评论这种关系明确的数据时,反而更合适。

目录结构长这样

my-blog/
├── app/
│   ├── (auth)/          # 认证相关页面
│   ├── blog/            # 博客相关页面
│   │   └── [slug]/      # 动态路由
│   ├── dashboard/       # 用户控制面板
│   ├── actions/         # Server Actions都放这里
│   └── api/auth/        # NextAuth配置
├── components/          # 可复用组件
├── lib/
│   ├── prisma.ts        # Prisma单例(这个很重要!)
│   └── utils.ts         # 工具函数
├── prisma/
│   └── schema.prisma    # 数据库Schema
└── public/              # 静态资源

这个结构我改了好几版才定下来。一开始我把Server Actions散落在各个页面文件里,后来发现维护起来太乱,专门建了个actions目录统一管理,清爽多了。

数据库Schema设计

数据库设计这块,我踩了个大坑。第一版我设计得过于复杂,什么标签、分类、阅读统计全加上了,结果写到一半发现根本用不上这么多表,又花了半天时间简化。 最后定下来的核心Schema是这样的:

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String?
  image     String?
  posts     Post[]
  createdAt DateTime @default(now())
}
model Post {
  id          String   @id @default(cuid())
  title       String
  slug        String   @unique
  content     String   @db.Text
  published   Boolean  @default(false)
  authorId    String
  author      User     @relation(fields: [authorId], references: [id])
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  @@index([slug])
  @@index([authorId])
}

注意看那两个@@index,这是性能优化的关键。博客列表页经常要按slug查询,用户主页要按authorId筛选,加上索引后查询速度能提升好几倍。

核心功能实现详解

好了,架构规划完,接下来是最刺激的部分——写代码!

3.1 环境搭建与初始化

创建Next.js 15项目超级简单:

npx create-next-app@latest my-blog
cd my-blog
npm install prisma @prisma/client zod next-auth

安装完Prisma后,初始化:

npx prisma init

这会生成prisma/schema.prisma.env文件。在.env里配置PostgreSQL连接:

DATABASE_URL="postgresql://username:password@localhost:5432/myblog?schema=public"

本地开发我用的是Docker跑PostgreSQL,一行命令搞定:

docker run --name blog-postgres -e POSTGRES_PASSWORD=mypassword -p 5432:5432 -d postgres

3.2 Prisma数据库集成

配置好Schema后,运行migration:

npx prisma migrate dev --name init

这里要特别注意一个坑——Prisma Client的单例模式。我第一次部署到Vercel的时候,完全忘了配置这个,结果网站跑了半天就报”Too many connections”错误,吓得我以为被DDoS攻击了。 原来是Next.js开发环境会热重载,每次重载都创建新的Prisma Client实例,连接池很快就爆了。正确的做法是这样:

// lib/prisma.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma
}

这段代码看起来有点绕,但它保证了开发环境下只有一个Prisma实例,生产环境不受影响。记住了,这是Prisma官方推荐的最佳实践!

3.3 Server Actions实战应用

创建博客文章是核心功能,来看看Server Actions怎么写:

// app/actions/post-actions.ts
'use server'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
import { z } from 'zod'
// 用Zod做数据验证,类型安全拉满
const PostSchema = z.object({
  title: z.string().min(1, '标题不能为空').max(100),
  content: z.string().min(10, '内容至少10个字'),
  slug: z.string().regex(/^[a-z0-9-]+$/, 'slug只能包含小写字母、数字和横杠')
})
export async function createPost(formData: FormData) {
  // 验证数据
  const validatedFields = PostSchema.safeParse({
    title: formData.get('title'),
    content: formData.get('content'),
    slug: formData.get('slug')
  })
  if (!validatedFields.success) {
    return { error: '数据验证失败' }
  }
  try {
    const post = await prisma.post.create({
      data: {
        ...validatedFields.data,
        authorId: 'user-id-here' // 实际项目从session获取
      }
    })
    // 重新验证缓存,这样新文章立即显示
    revalidatePath('/blog')
    return { success: true, post }
  } catch (error) {
    return { error: '创建文章失败' }
  }
}

在组件里直接用:

// app/dashboard/new-post/page.tsx
import { createPost } from '@/app/actions/post-actions'
export default function NewPost() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="标题" />
      <textarea name="content" placeholder="内容" />
      <input name="slug" placeholder="URL slug" />
      <button type="submit">发布</button>
    </form>
  )
}

看到没?不需要useState,不需要fetch,甚至不需要onSubmit事件处理。表单直接调用Server Action,Next.js自动处理序列化、网络请求、错误处理这些脏活累活。 一开始我也搞不清楚Server Actions和API Routes的区别,后来才明白:Server Actions就像”点外卖直送”,API Routes是”自己去餐厅取”。前者方便,后者灵活。大部分场景下,Server Actions够用了。

3.4 用户认证系统

认证这块我用的是NextAuth.js,配置起来挺简单:

// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth'
import GitHubProvider from 'next-auth/providers/github'
import { PrismaAdapter } from '@auth/prisma-adapter'
import { prisma } from '@/lib/prisma'
export const authOptions = {
  adapter: PrismaAdapter(prisma),
  providers: [
    GitHubProvider({
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!
    })
  ]
}
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }

配置GitHub OAuth需要在GitHub Developer Settings创建一个应用,拿到Client ID和Secret填到.env里就行。整个过程10分钟搞定,比自己写JWT认证省事太多了。

3.5 SSR/SSG优化策略

这部分是性能优化的关键。说实话,一开始我全用SSR(服务端渲染),结果发现每次打开博客列表都要查数据库,完全没必要。后来改成混合策略: 博客列表页——SSG + ISR:

// app/blog/page.tsx
import { prisma } from '@/lib/prisma'
// 每小时重新生成一次
export const revalidate = 3600
export default async function BlogList() {
  const posts = await prisma.post.findMany({
    where: { published: true },
    orderBy: { createdAt: 'desc' }
  })
  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content.slice(0, 150)}...</p>
        </article>
      ))}
    </div>
  )
}

这个页面在构建时就生成了静态HTML,之后每小时自动重新生成。用户访问时直接拿静态文件,速度飞快。 博客详情页——动态SSR:

// app/blog/[slug]/page.tsx
export default async function Post({ params }: { params: { slug: string } }) {
  const post = await prisma.post.findUnique({
    where: { slug: params.slug }
  })
  if (!post) notFound()
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

这个每次访问都会查数据库拿最新内容,适合经常更新的文章。 性能对比数据:

  • SSG页面: 加载时间 ~200ms
  • SSR页面: 加载时间 ~800ms
  • 纯CSR: 加载时间 ~1500ms 差距一目了然吧?这就是为什么Next.js适合做博客——SEO好,性能还强。

部署上线与优化

代码写完了,最激动人心的时刻到了——部署!

Vercel部署流程

我把代码推到GitHub后,直接在Vercel导入仓库。Vercel检测到是Next.js项目,自动配置好了一切。唯一要手动配置的是环境变量:

DATABASE_URL="你的生产数据库连接"
GITHUB_ID="OAuth Client ID"
GITHUB_SECRET="OAuth Secret"
NEXTAUTH_URL="https://your-domain.vercel.app"
NEXTAUTH_SECRET="随机字符串"

生产数据库我用的是Supabase的免费PostgreSQL,每月有500MB额度,个人博客完全够用。 点击Deploy后,大概2分钟网站就上线了。第一次看到自己的域名能访问时,我盯着屏幕看了好几秒,然后忍不住截图发朋友圈。

生产环境优化

部署成功不代表结束,还有几个优化要做: Prisma连接池配置:

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
  // Vercel Serverless环境必须设置
  directUrl = env("DIRECT_URL")
}

Serverless环境下,每个函数调用都可能创建新连接,设置directUrl可以复用连接池。 图片优化:

import Image from 'next/image'
<Image
  src="/avatar.jpg"
  alt="用户头像"
  width={100}
  height={100}
  // Next.js自动压缩优化
/>

Next.js的Image组件会自动转换成WebP格式,按需加载,性能提升明显。 最终性能评分: 我用Lighthouse跑了一下,桌面端性能96分,移动端92分,SEO 100分。看到这个结果时,真的很有成就感——两天时间,从零到上线,还是生产级的性能。

扩展方向与学习资源

这个博客系统目前是MVP版本,还有很多可以加的功能:

  • Markdown编辑器: 可以集成react-md-editor
  • 评论系统: 考虑接入Giscus(基于GitHub Discussions)
  • 搜索功能: Algolia或者自己用Prisma全文搜索
  • 深色模式: 用next-themes很容易实现 我接下来打算先加个Markdown编辑器,写文章会方便很多。 推荐学习资源:
  • Next.js官方文档 - 永远是最权威的
  • Prisma文档 - 把Getting Started看一遍就够了
  • 这个项目的完整代码 - 可以直接clone下来跑 相信我,动手做一遍比看十遍文档有用。你学会了理论,但只有实战才能真正掌握。

总结

回顾一下,这个周末项目我学到了:

  1. Server Actions的强大: 真的可以替代大部分API Routes,开发效率提升不是一点半点
  2. Prisma的类型安全: 有了完整的TypeScript支持,写代码特别有安全感
  3. SSG/SSR的灵活性: 根据场景选择渲染策略,性能和SEO都能兼顾
  4. Vercel部署的简单: 从代码到上线,不到5分钟 老实讲,做完这个项目最大的收获不是技术本身,而是信心——原来全栈开发也没那么难,关键是要动手。 如果你也想搭建一个自己的博客系统,别犹豫,现在就开始吧!遇到问题很正常,我当时也是边查文档边写代码,踩了无数坑才跑通。但当你看到自己的作品上线的那一刻,所有的努力都值得。 希望这篇文章能帮你少走些弯路。有问题欢迎在评论区讨论,我会尽量回复。 期待看到你的作品!

发布于: 2025年11月24日 · 修改于: 2025年12月4日

相关文章