React18+Next.js全栈实战:从SSR到API路由
发布日期: 2026/07/26 阅读总量: 1

React18+Next.js全栈实战:从SSR到API路由

1. 真实场景:一个博客项目把我逼到崩溃

2023年底,我负责公司技术博客平台重构。原有方案是Create React App作为前端,Express提供REST API,数据库PostgreSQL。首屏加载时间在3G网络下实测3.2秒,Lighthouse SEO分数只有35分(因为SPA爬虫拿不到内容)。用户跳出率高达60%。

我尝试过预渲染(prerender.io),每月额外支出$50,效果还不太稳定。最终决定迁移到Next.js全栈方案,用React18 Server Components + App Router。结果:首屏1.1秒,SEO 98分,开发效率提升一倍。本文就是这次迁移的完整复盘。

2. 方案对比:传统SPA vs Next.js全栈

维度CRA + Express (传统SPA)Next.js App Router (全栈)
首屏加载时间 (3G)3.2s1.1s
Lighthouse SEO分数3598
数据预取方式useEffect + fetchasync component + fetch
路由定义react-router v6文件系统路由
API层单独Express项目同一项目 API Routes
部署配置前端静态托管 + Node后端单一Node服务 (Vercel/Nginx)
开发效率 (从0到上线)约2周约5天

为什么传统SPA会慢?三个核心原因

  • 客户端渲染 (CSR):所有内容需要下载完JS Bundle再渲染,爬虫只能看到一个空div。
  • 额外的网络请求:浏览器先加载HTML -> JS -> 执行fetch获取数据 -> 渲染内容。至少3次往返。
  • 没有流式渲染:必须等所有数据齐全才能显示。

Next.js App Router使用React Server Components,所有数据获取和渲染在服务端完成,只发送纯HTML+少量交互JS给客户端。同时支持流式渲染(Streaming)和增量静态生成(ISR)。

3. 完整代码实现:从零搭建博客全栈应用

以下代码基于 Node 18.17.0, Next.js 14.1.0, React 18.2.0, Prisma 5.8.0, SQLite 3(生产可换PostgreSQL)。完整项目可运行。

3.1 项目初始化

npx create-next-app@14 next-blog --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd next-blog
npm install prisma @prisma/client
npx prisma init

3.2 数据库 Schema (Prisma)

prisma/schema.prisma 中定义。

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = "file:./dev.db"
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String
  slug      String   @unique
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model User {
  id       Int     @id @default(autoincrement())
  name     String
  email    String  @unique
  posts    Post[]  // 简化,不建立关联
}

运行 npx prisma migrate dev --name init 创建数据库和表。

添加种子数据:

// prisma/seed.ts
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

async function main() {
  const post = await prisma.post.create({
    data: {
      title: 'Hello Next.js Fullstack',
      content: '这是第一篇文章的内容,讲解React Server Components。',
      slug: 'hello-nextjs'
    }
  })
  console.log('Created post:', post)
}
main()
  .catch(e => console.error(e))
  .finally(() => prisma.$disconnect())

package.json 添加 seed 脚本:"prisma": { "seed": "tsx prisma/seed.ts" },然后 npx prisma db seed

3.3 全局数据库连接 (lib/db.ts)

// src/lib/db.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

3.4 文章列表页 (SSR + Streaming)

// src/app/page.tsx
import Link from 'next/link'
import { prisma } from '@/lib/db'

// App Router 直接 async 组件
export default async function Home() {
  // 服务端获取数据
  const posts = await prisma.post.findMany({
    orderBy: { createdAt: 'desc' }
  })

  return (
    

博客列表

{/* 使用流式渲染?这里简单演示同步渲染 */} {posts.length === 0 ? (

暂无文章

) : (
    {posts.map(post => (
  • {post.title}

    发布于 {new Date(post.createdAt).toLocaleDateString('zh-CN')}

  • ))}
)}
) } /* 如果希望流式加载,可以用 Suspense + 子组件,但本示例一切从简 */

3.5 文章详情页 (SSG + ISR)

// src/app/post/[slug]/page.tsx
import { notFound } from 'next/navigation'
import { prisma } from '@/lib/db'

// 生成静态路径 (增量)
export async function generateStaticParams() {
  const posts = await prisma.post.findMany({ select: { slug: true } })
  return posts.map(post => ({ slug: post.slug }))
}

// 页面组件
export default async function PostPage({ params }: { params: { slug: string } }) {
  const post = await prisma.post.findUnique({
    where: { slug: params.slug }
  })

  if (!post) notFound()

  return (
    

{post.title}

{post.content}
) } // ISR 增量重新生成:每60秒重新验证 export const revalidate = 60

3.6 API 路由:创建文章 (POST)

// src/app/api/posts/route.ts 
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/db'

export async function POST(request: Request) {
try {
const body = await request.json()
const { title, content, slug } = body

if (!title || !content || !slug) {
return NextResponse.json({ error: 'Missing fields' }, { status: 400 })
}

const post = await prisma.post.create({
data: { title, content, slug }
})

return NextResponse.json(post, { status: 201 })
} catch (error) {
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 })
}
}

export async function GET() {
const posts = await prisma.post.findMany()
return NextResponse.json(posts)
}

3.7 客户端组件示例:创建文章表单

// src/app/create/page.tsx (客户端组件)
'use client'

import { useState } from 'react'
import { useRouter } from 'next/navigation'

export default function CreatePost() {
  const router = useRouter()
  const [form, setForm] = useState({ title: '', content: '', slug: '' })
  const [loading, setLoading] = useState(false)

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    setLoading(true)
    try {
      const res = await fetch('/api/posts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(form)
      })
      if (res.ok) {
        router.push('/')
        router.refresh()
      }
    } finally {
      setLoading(false)
    }
  }

  return (
    
setForm({ ...form, title: e.target.value })} className="w-full border p-2" required />