Skip to content

Next.js 是一个基于 React 的全栈应用程序框架,用于构建现代化的 Web 应用程序。

1.什么是 Next.js

Next.js 是一个基于 React 的生产级框架,它提供了构建现代 Web 应用所需的所有工具和最佳实践。

1-1.核心特性

  • 服务端渲染 (SSR):提供更好的 SEO 和首屏加载性能
  • 静态站点生成 (SSG):构建时预渲染页面,部署到 CDN
  • 增量静态再生 (ISR):静态页面的按需更新
  • 自动路由:基于文件系统的路由,无需手动配置
  • API 路由:内置 API 端点支持
  • 代码分割:自动按页面进行代码分割
  • TypeScript 支持:开箱即用的 TypeScript 支持
  • 图像优化:内置的图像优化和懒加载

1-2.渲染模式

Next.js 支持多种渲染模式:

渲染模式说明适用场景
SSR服务端渲染,每次请求都在服务器生成 HTML动态内容、需要 SEO 的应用
SSG静态站点生成,构建时预渲染所有页面内容相对静态的网站、博客
ISR增量静态再生,静态页面按需更新大型网站、内容更新频繁但不实时
CSR客户端渲染,传统 SPA 模式后台管理系统、不需要 SEO 的应用

2.技术栈

Next.js 的技术栈包括:

2-1.核心依赖

  • React 18+:UI 框架
  • Webpack/Turbopack:构建工具
  • SWC:JavaScript/TypeScript 编译器
  • Node.js:服务器运行时

2-2.生态系统

  • Vercel:官方部署平台
  • Next.js Commerce:电商解决方案
  • NextAuth.js:身份验证
  • SWR/TanStack Query:数据获取

3.项目结构

3-1.App Router (推荐,Next.js 13+)

my-next-app/
├── .next/              # 构建输出目录
├── .env.local          # 环境变量
├── app/                # App Router 目录
│   ├── globals.css     # 全局样式
│   ├── layout.tsx      # 根布局
│   ├── page.tsx        # 首页
│   ├── about/
│   │   └── page.tsx    # /about 页面
│   ├── blog/
│   │   ├── page.tsx    # /blog 页面
│   │   └── [slug]/
│   │       └── page.tsx # /blog/[slug] 动态页面
│   └── api/            # API 路由
│       └── users/
│           └── route.ts # /api/users 端点
├── components/         # React 组件
├── lib/               # 工具函数
├── public/            # 静态资源
├── next.config.js     # Next.js 配置
└── package.json       # 项目依赖

3-2.Pages Router (传统方式)

my-next-app/
├── pages/              # 页面目录
│   ├── _app.tsx        # 应用入口
│   ├── _document.tsx   # HTML 文档
│   ├── index.tsx       # 首页
│   ├── about.tsx       # /about 页面
│   ├── blog/
│   │   ├── index.tsx   # /blog 页面
│   │   └── [slug].tsx  # /blog/[slug] 动态页面
│   └── api/            # API 路由
│       └── users.ts    # /api/users 端点
├── components/         # React 组件
├── styles/            # 样式文件
├── public/            # 静态资源
└── next.config.js     # Next.js 配置

4.快速开始

4-1.创建项目

bash
# 使用 create-next-app
npx create-next-app@latest my-next-app

# 使用 TypeScript
npx create-next-app@latest my-next-app --typescript

# 使用 Tailwind CSS
npx create-next-app@latest my-next-app --tailwind

# 完整配置
npx create-next-app@latest my-next-app --typescript --tailwind --eslint --app

4-2.安装依赖

bash
cd my-next-app
npm install

4-3.启动开发服务器

bash
npm run dev

4-4.构建生产版本

bash
# 构建应用
npm run build

# 启动生产服务器
npm run start

# 导出静态文件(SSG)
npm run build && npm run export

5.配置文件

5-1.基础配置

javascript
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // 实验性功能
  experimental: {
    appDir: true,        // 启用 App Router
    serverActions: true  // 启用 Server Actions
  },

  // 图像配置
  images: {
    domains: ['example.com'],
    formats: ['image/webp', 'image/avif']
  },

  // 环境变量
  env: {
    CUSTOM_KEY: process.env.CUSTOM_KEY
  },

  // 重定向
  async redirects() {
    return [
      {
        source: '/old-path',
        destination: '/new-path',
        permanent: true
      }
    ]
  },

  // 重写
  async rewrites() {
    return [
      {
        source: '/api/:path*',
        destination: 'https://api.example.com/:path*'
      }
    ]
  }
}

module.exports = nextConfig

5-2.TypeScript 配置

json
// tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "es6"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "baseUrl": ".",
    "paths": {
      "@/*": ["./*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

6.页面和路由

6-1.App Router (Next.js 13+)

App Router 是 Next.js 13+ 推荐的路由方式:

app/
├── page.tsx           # / (首页)
├── about/
│   └── page.tsx       # /about
├── blog/
│   ├── page.tsx       # /blog
│   ├── [slug]/
│   │   └── page.tsx   # /blog/[slug]
│   └── [...slug]/
│       └── page.tsx   # /blog/[...slug] (捕获所有路由)
└── dashboard/
    ├── layout.tsx     # 嵌套布局
    ├── page.tsx       # /dashboard
    └── settings/
        └── page.tsx   # /dashboard/settings

6-2.页面组件

tsx
// app/blog/[slug]/page.tsx
interface PageProps {
  params: { slug: string }
  searchParams: { [key: string]: string | string[] | undefined }
}

export default function BlogPost({ params, searchParams }: PageProps) {
  return (
    <div>
      <h1>博客文章: {params.slug}</h1>
      <p>搜索参数: {JSON.stringify(searchParams)}</p>
    </div>
  )
}

// 生成静态参数 (SSG)
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(res => res.json())

  return posts.map((post: any) => ({
    slug: post.slug
  }))
}

// 生成元数据
export async function generateMetadata({ params }: PageProps) {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`)
    .then(res => res.json())

  return {
    title: post.title,
    description: post.excerpt
  }
}

6-3.布局组件

tsx
// app/layout.tsx (根布局)
import './globals.css'

export const metadata = {
  title: 'My Next.js App',
  description: 'Generated by create next app'
}

export default function RootLayout({
  children
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="zh">
      <body>
        <header>
          <nav>导航栏</nav>
        </header>
        <main>{children}</main>
        <footer>页脚</footer>
      </body>
    </html>
  )
}
tsx
// app/dashboard/layout.tsx (嵌套布局)
export default function DashboardLayout({
  children
}: {
  children: React.ReactNode
}) {
  return (
    <div className="dashboard">
      <aside>侧边栏</aside>
      <div className="content">{children}</div>
    </div>
  )
}

7.数据获取

7-1.服务端组件 (默认)

tsx
// app/posts/page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    // 缓存配置
    next: { revalidate: 3600 } // 1小时后重新验证
  })

  if (!res.ok) {
    throw new Error('Failed to fetch posts')
  }

  return res.json()
}

export default async function PostsPage() {
  const posts = await getPosts()

  return (
    <div>
      <h1>文章列表</h1>
      {posts.map((post: any) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  )
}

7-2.客户端组件

tsx
'use client'

import { useState, useEffect } from 'react'

export default function ClientPostsPage() {
  const [posts, setPosts] = useState([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetch('/api/posts')
      .then(res => res.json())
      .then(data => {
        setPosts(data)
        setLoading(false)
      })
  }, [])

  if (loading) return <div>加载中...</div>

  return (
    <div>
      <h1>文章列表</h1>
      {posts.map((post: any) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  )
}

7-3.SWR 数据获取

bash
npm install swr
tsx
'use client'

import useSWR from 'swr'

const fetcher = (url: string) => fetch(url).then(res => res.json())

export default function PostsWithSWR() {
  const { data: posts, error, isLoading } = useSWR('/api/posts', fetcher)

  if (error) return <div>加载失败</div>
  if (isLoading) return <div>加载中...</div>

  return (
    <div>
      <h1>文章列表</h1>
      {posts?.map((post: any) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  )
}

8.API 路由

8-1.App Router API

typescript
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server'

// GET /api/posts
export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams
  const page = searchParams.get('page') || '1'

  try {
    const posts = await fetchPostsFromDB(parseInt(page))
    return NextResponse.json({ posts })
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to fetch posts' },
      { status: 500 }
    )
  }
}

// POST /api/posts
export async function POST(request: NextRequest) {
  try {
    const body = await request.json()
    const post = await createPost(body)
    return NextResponse.json({ post }, { status: 201 })
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to create post' },
      { status: 500 }
    )
  }
}

8-2.动态 API 路由

typescript
// app/api/posts/[id]/route.ts
interface RouteParams {
  params: { id: string }
}

export async function GET(
  request: NextRequest,
  { params }: RouteParams
) {
  try {
    const post = await getPostById(params.id)
    if (!post) {
      return NextResponse.json(
        { error: 'Post not found' },
        { status: 404 }
      )
    }
    return NextResponse.json({ post })
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to fetch post' },
      { status: 500 }
    )
  }
}

export async function PUT(
  request: NextRequest,
  { params }: RouteParams
) {
  try {
    const body = await request.json()
    const post = await updatePost(params.id, body)
    return NextResponse.json({ post })
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to update post' },
      { status: 500 }
    )
  }
}

export async function DELETE(
  request: NextRequest,
  { params }: RouteParams
) {
  try {
    await deletePost(params.id)
    return NextResponse.json({ message: 'Post deleted' })
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to delete post' },
      { status: 500 }
    )
  }
}

8-3.Server Actions

tsx
// app/posts/actions.ts
'use server'

import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string
  const content = formData.get('content') as string

  try {
    const post = await savePostToDB({ title, content })
    revalidatePath('/posts')
    redirect(`/posts/${post.id}`)
  } catch (error) {
    throw new Error('Failed to create post')
  }
}

export async function deletePost(id: string) {
  try {
    await deletePostFromDB(id)
    revalidatePath('/posts')
  } catch (error) {
    throw new Error('Failed to delete post')
  }
}
tsx
// app/posts/create/page.tsx
import { createPost } from '../actions'

export default function CreatePost() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="标题" required />
      <textarea name="content" placeholder="内容" required />
      <button type="submit">创建文章</button>
    </form>
  )
}

9.状态管理

9-1.React Context

tsx
// lib/context/AuthContext.tsx
'use client'

import { createContext, useContext, useState, ReactNode } from 'react'

interface User {
  id: string
  name: string
  email: string
}

interface AuthContextType {
  user: User | null
  login: (email: string, password: string) => Promise<void>
  logout: () => void
}

const AuthContext = createContext<AuthContextType | undefined>(undefined)

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null)

  const login = async (email: string, password: string) => {
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password })
    })

    if (response.ok) {
      const userData = await response.json()
      setUser(userData.user)
    }
  }

  const logout = () => {
    setUser(null)
  }

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  )
}

export function useAuth() {
  const context = useContext(AuthContext)
  if (context === undefined) {
    throw new Error('useAuth must be used within an AuthProvider')
  }
  return context
}

9-2.Zustand 状态管理

bash
npm install zustand
typescript
// lib/store/useStore.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface User {
  id: string
  name: string
  email: string
}

interface AuthState {
  user: User | null
  isAuthenticated: boolean
  login: (user: User) => void
  logout: () => void
}

export const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      user: null,
      isAuthenticated: false,
      login: (user) => set({ user, isAuthenticated: true }),
      logout: () => set({ user: null, isAuthenticated: false })
    }),
    {
      name: 'auth-storage'
    }
  )
)
tsx
// components/LoginForm.tsx
'use client'

import { useAuthStore } from '@/lib/store/useStore'

export default function LoginForm() {
  const { login, isAuthenticated } = useAuthStore()

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    // 登录逻辑
    const user = { id: '1', name: 'John', email: 'john@example.com' }
    login(user)
  }

  if (isAuthenticated) {
    return <div>已登录</div>
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" placeholder="邮箱" required />
      <input type="password" placeholder="密码" required />
      <button type="submit">登录</button>
    </form>
  )
}

10.中间件

10-1.基础中间件

typescript
// middleware.ts (项目根目录)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  // 检查认证
  const token = request.cookies.get('token')

  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    if (!token) {
      return NextResponse.redirect(new URL('/login', request.url))
    }
  }

  // 添加自定义头部
  const response = NextResponse.next()
  response.headers.set('X-Custom-Header', 'value')

  return response
}

export const config = {
  matcher: [
    '/dashboard/:path*',
    '/api/:path*'
  ]
}

10-2.认证中间件

typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { jwtVerify } from 'jose'

export async function middleware(request: NextRequest) {
  const token = request.cookies.get('token')?.value

  // 保护的路由
  const protectedPaths = ['/dashboard', '/profile', '/admin']
  const isProtectedPath = protectedPaths.some(path =>
    request.nextUrl.pathname.startsWith(path)
  )

  if (isProtectedPath) {
    if (!token) {
      return NextResponse.redirect(new URL('/login', request.url))
    }

    try {
      // 验证 JWT token
      await jwtVerify(
        token,
        new TextEncoder().encode(process.env.JWT_SECRET!)
      )
    } catch (error) {
      return NextResponse.redirect(new URL('/login', request.url))
    }
  }

  return NextResponse.next()
}

11.部署

11-1.Vercel 部署 (推荐)

bash
# 安装 Vercel CLI
npm install -g vercel

# 部署
vercel

# 生产部署
vercel --prod
json
// vercel.json
{
  "functions": {
    "app/api/**/*.ts": {
      "maxDuration": 30
    }
  },
  "env": {
    "DATABASE_URL": "@database-url"
  },
  "build": {
    "env": {
      "NEXT_PUBLIC_API_URL": "https://api.example.com"
    }
  }
}

11-2.Docker 部署

dockerfile
# Dockerfile
FROM node:18-alpine AS base

# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app

COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
  if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
  elif [ -f package-lock.json ]; then npm ci; \
  elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
  else echo "Lockfile not found." && exit 1; \
  fi

# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

RUN yarn build

# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app

ENV NODE_ENV production

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"]

11-3.静态导出

javascript
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export',
  trailingSlash: true,
  images: {
    unoptimized: true
  }
}

module.exports = nextConfig
bash
npm run build

12.最佳实践

12-1.性能优化

tsx
// 图像优化
import Image from 'next/image'

export default function OptimizedImage() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero image"
      width={800}
      height={600}
      priority // 首屏图片
      placeholder="blur" // 模糊占位符
      blurDataURL="data:image/jpeg;base64,..." // 占位符数据
    />
  )
}
tsx
// 动态导入
import dynamic from 'next/dynamic'

const DynamicComponent = dynamic(() => import('../components/Heavy'), {
  loading: () => <p>Loading...</p>,
  ssr: false // 禁用服务端渲染
})

export default function Page() {
  return (
    <div>
      <DynamicComponent />
    </div>
  )
}

12-2.SEO 优化

tsx
// app/blog/[slug]/page.tsx
import { Metadata } from 'next'

interface PageProps {
  params: { slug: string }
}

export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
  const post = await getPost(params.slug)

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.image],
      type: 'article'
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
      images: [post.image]
    }
  }
}

12-3.错误处理

tsx
// app/error.tsx
'use client'

export default function Error({
  error,
  reset
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <div>
      <h2>出错了!</h2>
      <p>{error.message}</p>
      <button onClick={() => reset()}>重试</button>
    </div>
  )
}
tsx
// app/not-found.tsx
export default function NotFound() {
  return (
    <div>
      <h2>页面未找到</h2>
      <p>抱歉,您访问的页面不存在。</p>
      <a href="/">返回首页</a>
    </div>
  )
}

12-4.环境变量

bash
# .env.local
DATABASE_URL=postgresql://...
NEXTAUTH_SECRET=your-secret-here
NEXTAUTH_URL=http://localhost:3000

# 公开变量(以 NEXT_PUBLIC_ 开头)
NEXT_PUBLIC_API_URL=https://api.example.com
typescript
// 使用环境变量
const apiUrl = process.env.NEXT_PUBLIC_API_URL
const dbUrl = process.env.DATABASE_URL // 仅服务端可用

Next.js 提供了强大而灵活的开发体验,通过合理使用其特性可以构建高性能的现代 Web 应用程序。相比 Nuxt.js,Next.js 在 React 生态系统中有着更成熟的工具链和更大的社区支持。