Next.js 从零到部署完整实战¶
面向:会用 React、想要用 Next.js 做全栈项目的开发者。本篇从
npx create-next-app开始,到部署 Vercel / Docker 结束。
一、Next.js 是什么¶
Next.js = React + 服务端渲染(SSR)+ 静态生成(SSG)+ API 路由 + 文件路由,一个框架搞定前后端。
核心能力:
| 能力 | 一句话解释 |
|---|---|
| App Router | 基于目录的文件路由,page.tsx 即页面 |
| Server Components | 组件默认在服务端渲染,零 JS 发送给客户端 |
| Server Actions | 直接在组件里写后端逻辑,不需要单独建 API 路由 |
| 流式渲染 (Streaming) | 页面边渲染边输出,不等待慢数据 |
| API Routes | route.ts 文件即 API 端点 |
| 中间件 (Middleware) | 请求级别的拦截,做鉴权/重定向 |
二、环境准备与项目初始化¶
2.1 前提¶
node -v # 需要 ≥ 18.17
npm -v
2.2 创建项目¶
npx create-next-app@latest my-next-blog
按提示选择:
| 提示 | 选择 |
|---|---|
| TypeScript | Yes ✅ |
| ESLint | Yes ✅ |
| Tailwind CSS | Yes(推荐) |
src/ directory |
Yes(结构清晰) |
| App Router | Yes ✅(默认,推荐) |
| Turbopack | Yes(更快) |
| import alias | 默认 @/* 即可 |
cd my-next-blog
npm run dev
打开 http://localhost:3000,看到 Next.js 欢迎页。
2.3 初始项目结构¶
my-next-blog/
├── src/
│ └── app/
│ ├── layout.tsx # 根布局(所有页面共用)
│ ├── page.tsx # 首页(/)
│ ├── globals.css
│ └── favicon.ico
├── public/ # 静态资源(图片等)
├── next.config.ts
├── tailwind.config.ts
├── tsconfig.json
└── package.json
三、第一步:搭建博客页面¶
3.1 创建文章数据¶
src/lib/posts.ts:
export interface Post {
slug: string;
title: string;
excerpt: string;
date: string;
content: string;
}
const posts: Post[] = [
{
slug: 'hello-world',
title: 'Hello World —— 我的第一篇博客',
excerpt: '这是使用 Next.js 搭建的第一篇博客文章。',
date: '2026-08-01',
content: `
## 欢迎
这是我的第一篇博客!用 Next.js 搭建博客非常简单。
### 技术栈
- **框架**: Next.js 15 App Router
- **语言**: TypeScript
- **样式**: Tailwind CSS
- **部署**: Vercel
`.trim(),
},
{
slug: 'nextjs-ssr',
title: '理解 Next.js 的 SSR 和 SSG',
excerpt: 'Server Components、静态生成、增量静态再生成——一次讲清楚。',
date: '2026-08-02',
content: `
## SSR vs SSG vs ISR
Next.js 提供了三种渲染策略:
- **SSR (Server-Side Rendering)**:每次请求都在服务器渲染,数据永远最新
- **SSG (Static Site Generation)**:构建时生成 HTML,速度最快
- **ISR (Incremental Static Regeneration)**:静态生成,但可以定时重新生成
`.trim(),
},
];
export function getAllPosts(): Omit<Post, 'content'>[] {
return posts.map(({ content, ...rest }) => rest)
.sort((a, b) => b.date.localeCompare(a.date));
}
export function getPostBySlug(slug: string): Post | undefined {
return posts.find(p => p.slug === slug);
}
3.2 首页:文章列表¶
src/app/page.tsx:
import Link from 'next/link';
import { getAllPosts } from '@/lib/posts';
export default function HomePage() {
const posts = getAllPosts();
return (
<main className="max-w-2xl mx-auto py-12 px-4">
<h1 className="text-4xl font-bold mb-2">我的博客</h1>
<p className="text-gray-500 mb-8">用 Next.js 15 + TypeScript 搭建</p>
<div className="space-y-6">
{posts.map(post => (
<article key={post.slug} className="border rounded-lg p-6 hover:shadow-lg transition">
<time className="text-sm text-gray-400">{post.date}</time>
<h2 className="text-xl font-semibold mt-1">
<Link href={`/posts/${post.slug}`} className="hover:text-blue-600">
{post.title}
</Link>
</h2>
<p className="text-gray-500 mt-2">{post.excerpt}</p>
</article>
))}
</div>
</main>
);
}
3.3 文章详情页¶
src/app/posts/[slug]/page.tsx:
import { notFound } from 'next/navigation';
import Link from 'next/link';
import { getPostBySlug } from '@/lib/posts';
export default async function PostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = getPostBySlug(slug);
if (!post) notFound();
return (
<main className="max-w-2xl mx-auto py-12 px-4">
<Link href="/" className="text-blue-600 hover:underline mb-4 inline-block">
← 返回首页
</Link>
<article>
<time className="text-gray-400">{post.date}</time>
<h1 className="text-3xl font-bold mt-1 mb-6">{post.title}</h1>
<div className="prose" dangerouslySetInnerHTML={{ __html: post.content.replace(/\n/g, '<br/>') }} />
</article>
</main>
);
}
在浏览器测试
保存后访问 http://localhost:3000,点击文章标题跳转到详情页。
四、第二步:接数据库(Prisma + PostgreSQL)¶
4.1 安装 Prisma¶
npm install prisma @prisma/client
npx prisma init
4.2 定义数据模型¶
prisma/schema.prisma:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Post {
id Int @id @default(autoincrement())
slug String @unique
title String
excerpt String
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
.env:
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
4.3 生成数据库¶
npx prisma migrate dev --name init
npx prisma generate # 生成类型安全的客户端
4.4 创建 Prisma 客户端单例¶
src/lib/prisma.ts:
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
export const prisma = globalForPrisma.prisma || new PrismaClient();
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
4.5 从数据库读取博客¶
src/lib/posts-db.ts:
import { prisma } from './prisma';
export async function getAllPosts() {
return prisma.post.findMany({
orderBy: { createdAt: 'desc' },
select: {
slug: true,
title: true,
excerpt: true,
createdAt: true,
},
});
}
export async function getPostBySlug(slug: string) {
return prisma.post.findUnique({ where: { slug } });
}
4.6 接入真实数据库的首页¶
// src/app/page.tsx
import { getAllPosts } from '@/lib/posts-db';
export default async function HomePage() {
const posts = await getAllPosts();
// ... 渲染逻辑同上
}
五、第三步:Server Actions(添加文章)¶
不用建 API 路由,直接在组件中处理表单提交。
5.1 创建 Server Action¶
src/app/actions.ts:
'use server';
import { revalidatePath } from 'next/cache';
import { prisma } from '@/lib/prisma';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const slug = formData.get('slug') as string;
const excerpt = formData.get('excerpt') as string;
const content = formData.get('content') as string;
// 服务端校验
if (!title || !slug) {
return { error: '标题和 slug 必填' };
}
await prisma.post.create({
data: { title, slug, excerpt, content },
});
// 重新验证首页缓存
revalidatePath('/');
return { success: true };
}
5.2 创建发布页面¶
src/app/new/page.tsx:
'use client';
import { useRouter } from 'next/navigation';
import { createPost } from '@/app/actions';
export default function NewPostPage() {
const router = useRouter();
async function handleSubmit(formData: FormData) {
const result = await createPost(formData);
if (result.success) {
router.push('/');
router.refresh();
} else {
alert(result.error);
}
}
return (
<main className="max-w-2xl mx-auto py-12 px-4">
<h1 className="text-3xl font-bold mb-6">写文章</h1>
<form action={handleSubmit} className="space-y-4">
<input name="title" placeholder="标题" required className="w-full border p-2 rounded" />
<input name="slug" placeholder="slug(如 hello-world)" required className="w-full border p-2 rounded" />
<textarea name="excerpt" placeholder="摘要" className="w-full border p-2 rounded" rows={2} />
<textarea name="content" placeholder="正文(支持 Markdown)" required className="w-full border p-2 rounded" rows={8} />
<button type="submit" className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700">
发布
</button>
</form>
</main>
);
}
六、第四步:部署¶
6.1 部署到 Vercel(推荐,一键)¶
- 推代码到 GitHub:
git init && git add . && git commit -m "init"
git remote add origin https://github.com/你的用户名/my-next-blog.git
git push -u origin main
- 打开 Vercel → Import → 选择仓库
- Vercel 自动识别 Next.js,无需任何配置,点击 Deploy
- 在 Settings → Environment Variables 中添加
DATABASE_URL - 如果有自己的域名,Settings → Domains 添加
6.2 部署到 VPS(Docker)¶
Dockerfile:
FROM node:20-alpine AS base
WORKDIR /app
# 依赖安装
FROM base AS deps
COPY package*.json prisma/schema.prisma ./
RUN npm ci
RUN npx prisma generate
# 构建
FROM deps AS builder
COPY . .
RUN npm run build
# 生产镜像
FROM base AS runner
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
CMD ["node", "server.js"]
next.config.ts:
const nextConfig = {
output: 'standalone', // 独立部署模式
};
export default nextConfig;
docker build -t my-next-blog .
docker run -d -p 3000:3000 --env-file .env my-next-blog
6.3 部署安全检查¶
| 检查项 | 说明 |
|---|---|
DATABASE_URL 不提交代码 |
.env 在 .gitignore 中,通过 Vercel / Docker env 注入 |
| 构建时检查 lint | next build 包含了 lint 检查 |
| 图片优化 | 用 <Image> 替代 <img>(自动优化、懒加载) |
| CSP 头 | next.config.ts 中配置 headers() |
| 速率限制 | 配合 middleware.ts 或 Vercel WAF |
七、Next.js 安全要点¶
7.1 Server Action 安全¶
'use server';
import { auth } from '@/lib/auth'; // 你的鉴权逻辑
export async function deletePost(postId: number) {
// 1. 鉴权(必须!)
const session = await auth();
if (!session?.user) throw new Error('Unauthorized');
// 2. 权限检查
const post = await prisma.post.findUnique({ where: { id: postId } });
if (post.authorId !== session.user.id) throw new Error('Forbidden');
// 3. 执行操作
await prisma.post.delete({ where: { id: postId } });
revalidatePath('/');
}
7.2 中间件鉴权¶
src/middleware.ts:
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value;
// 保护 /dashboard 路由
if (request.nextUrl.pathname.startsWith('/dashboard')) {
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*'],
};
7.3 环境变量安全¶
NEXT_PUBLIC_* → 暴露给浏览器,任何人都能看到
非 NEXT_PUBLIC_ → 只在服务端可用
数据库密码、API 密钥、JWT Secret 都不要用 NEXT_PUBLIC_ 前缀。
八、完整项目 Checklist¶
- 项目已初始化并能在本地
npm run dev跑通 - 首页能显示文章列表(从 Prisma 数据库读取)
- 文章详情页能正常跳转
- 添加文章支持(Server Actions)
- 通过
middleware.ts保护管理页面 -
.env在.gitignore中,生产环境变量通过平台配置 - 已部署到 Vercel / VPS,能通过域名访问
- HTTPS 已配置
- API 限流已配置
- 安全头已配置
下一步 - 添加评论区 → 用 Prisma 加 Comment 模型 + Server Actions - 添加认证 → 用 NextAuth.js (Auth.js) 接 GitHub/OAuth 登录 - 添加 RSS → 用
feed包生成 RSS feed - 部署选择 → 部署总览 | 前端部署指南