Next.js 实战指南

165 阅读7分钟

Next.js 实战指南

初学者在接触 Next.js 时容易遇到的困惑与陷阱。本文基于官方文档核心流程,结合实际项目经验,对 Next.js 基础用法进行系统性重构,不仅保留关键操作步骤,更补充原理说明与最佳实践,帮助开发者少走弯路。

一、Next.js 核心价值与环境准备

Next.js 作为 React 生态中最成熟的服务端渲染(SSR)框架,核心优势在于首屏加载速度优化SEO 友好零配置路由系统。官方文档(nextjs.org/)是权威学习资料,但初学者常忽略环境配置细节,以下是经过实战验证的项目初始化流程。

1.1 项目初始化规范

# 1. 建议按业务域划分目录(避免直接用盘符根目录)
mkdir -p ~/workspace/frontend/next-admin
cd ~/workspace/frontend/next-admin
# 2. 初始化package.json(显式指定版本字段)
npm init -y --save-exact
# 3. 安装核心依赖(锁定版本避免兼容性问题)
npm install react@18.2.0 react-dom@18.2.0 next@14.0.3 --save-exact
# 4. 创建核心目录(pages为Next.js唯一特殊目录)
mkdir pages components config public

1.2 开发脚本配置优化

在package.json中配置脚本时,建议补充环境变量与错误捕获:

"scripts": {
  "dev": "NODE_ENV=development next dev -p 3000",
  "build": "next build",
  "start": "NODE_ENV=production next start -p 3000",
  "lint": "next lint"
}
  • -p 3000:显式指定端口,避免端口冲突
  • NODE_ENV:明确环境标识,避免开发 / 生产环境混淆
  • lint:集成代码检查,保障团队代码规范

1.3 项目启动验证

npm run dev

访问http://localhost:3000出现 404 页面,说明:

  1. Next.js 服务正常启动
  1. pages目录已被框架识别
  1. 路由系统初始化完成(无默认页面时触发 404)

二、路由系统深度解析(实战重点)

Next.js 的文件路由系统是其核心特性,但初学者常忽略客户端 / 服务端路由的差异,导致刷新 404 等问题。以下从基础到进阶,完整覆盖路由使用场景。

2.1 基础路由实现(文件即路由)

文件路径访问 URL说明
pages/index.jshttp://localhost:3000首页(默认路由)
pages/about.jshttp://localhost:3000/about关于页
pages/posts/[id].jshttp://localhost:3000/posts/123动态路由(实战高频)

基础页面示例(pages/index.js):

// 推荐使用函数组件+ES模块规范
export default function Home() {
  return (
    <main style={{ padding: '2rem' }}>
      <h1>Hello Next.js(实战版)</h1>
    </main>
  );
}

2.2 页面跳转优化(客户端 vs 服务端)

2.2.1 客户端跳转(推荐)

使用next/link组件实现无刷新跳转,减少服务端请求:

import Link from 'next/link';
import { Button } from 'antd'; // 实际项目常用UI组件
export default function Home() {
  return (
    <div>
      {/* 方式1:包裹原生标签 */}
      <Link href="/about">
        <a style={{ marginRight: '1rem' }}>关于我们</a>
      </Link>
      {/* 方式2:包裹自定义组件(需支持onClick) */}
      <Link href="/posts/123">
        <Button type="primary">查看文章</Button>
      </Link>
    </div>
  );
}
2.2.2 服务端跳转(特殊场景)

通过next/router实现编程式跳转(如登录后重定向):

import { useRouter } from 'next/router';
export default function Login() {
  const router = useRouter();
  const handleLogin = () => {
    // 登录逻辑处理...
    router.push('/dashboard'); // 客户端跳转
    // router.replace('/dashboard'); // 替换历史记录(无后退)
  };
  return <button onClick={handleLogin}>登录</button>;
}

2.3 路由参数传递(3 种实战方案)

方案 1:Query 参数(简单场景)
// 传参页
<Link href="/search?keyword=nextjs&page=1">搜索</Link>
// 接收页(pages/search.js)
import { useRouter } from 'next/router';
export default function Search() {
  const router = useRouter();
  const { keyword, page } = router.query; // 自动解析
  return (
    <div>
      <p>关键词:{keyword}</p>
      <p>页码:{page}</p>
    </div>
  );
}
方案 2:动态路由(推荐,SEO 友好)
  1. 创建动态路由文件:pages/posts/[id].js
  1. 传参跳转:
<Link href="/posts/[id]" as="/posts/123">
  <a>文章123</a>
</Link>
  1. 接收参数:
// 方式1:客户端获取
import { useRouter } from 'next/router';
export default function Post() {
  const router = useRouter();
  const { id } = router.query;
  return <p>文章ID:{id}</p>;
}
// 方式2:服务端获取(SSR场景)
export async function getServerSideProps(context) {
  const { id } = context.params; // Next.js 13+使用params
  // 服务端数据请求...
  return { props: { postId: id } };
}
方案 3:状态管理(跨页面复杂数据)

对于复杂数据传递(如表单结果),推荐使用:

  • 客户端:zustand/redux-toolkit
  • 服务端:getServerSideProps + 数据库查询

2.4 路由美化与 404 问题解决(核心避坑)

问题现象

使用as属性美化 URL 后(如/posts/123),刷新页面出现 404。

根本原因
  • 客户端跳转:as属性仅修改 URL 显示,实际路由仍为/posts?id=123
  • 服务端刷新:服务器根据美化后的 URL(/posts/123)查找对应页面,因无pages/posts/123.js文件导致 404
解决方案(2 种实战方案)
方案 1:使用 Next.js 内置动态路由(推荐,Next.js 9.3+)

直接创建pages/posts/[id].js文件,无需额外配置,框架自动处理服务端路由映射。

方案 2:自定义 Express 服务器(复杂场景)

当需要更灵活的路由控制时(如多域名、复杂路由规则),可配置 Express 服务器:

  1. 安装依赖:
npm install express@4.18.2 --save-exact
  1. 创建服务器配置(config/server.js):
const express = require('express');
const next = require('next');
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
app.prepare().then(() => {
  const server = express();
  // 自定义路由映射(解决404问题)
  server.get('/posts/:id', (req, res) => {
    const actualPage = '/posts'; // 实际页面路径
    const queryParams = { id: req.params.id }; // 参数映射
    app.render(req, res, actualPage, queryParams);
  });
  // 兜底处理所有其他路由
  server.get('*', (req, res) => handle(req, res));
  // 启动服务(监听3000端口)
  server.listen(3000, (err) => {
    if (err) throw err;
    console.log('> Ready on http://localhost:3000');
  });
});
  1. 修改启动脚本(package.json):
"scripts": {
  "dev": "node ./config/server.js",
  "build": "next build",
  "start": "NODE_ENV=production node ./config/server.js"
}

三、组件设计与复用(实战架构)

在实际项目中,组件设计直接影响代码可维护性。以下基于 6 年项目经验,总结 Next.js 组件复用的最佳实践。

3.1 组件目录规范(非强制但推荐)

src/
├── components/        # 通用组件(全局复用)
│   ├── common/        # 基础组件(按钮、输入框)
│   ├── layout/        # 布局组件(头部、底部)
│   └── business/      # 业务组件(订单卡片、商品列表)
├── pages/             # 页面组件(路由对应)
└── hooks/             # 自定义Hooks(逻辑复用)

3.2 布局组件实现(实战高频)

布局组件用于抽离页面公共部分(如导航、页脚),避免代码重复:

  1. 创建布局组件(components/layout/MainLayout.js):
import Link from 'next/link';
import Head from 'next/head'; // 用于设置页面元信息
// 接收children属性,渲染页面内容
export default function MainLayout({ children, title = 'Next.js实战' }) {
  return (
    <div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
      {/* 页面元信息(SEO优化关键) */}
      <Head>
        <title>{title}</title>
        <meta name="description" content="Next.js实战教程(6年经验版)" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      {/* 导航栏 */}
      <header style={{ padding: '1rem 2rem', borderBottom: '1px solid #eee' }}>
        <Link href="/">
          <a style={{ marginRight: '2rem', fontWeight: 'bold' }}>首页</a>
        </Link>
        <Link href="/about">
          <a style={{ marginRight: '2rem' }}>关于我们</a>
        </Link>
        <Link href="/posts">
          <a>文章列表</a>
        </Link>
      </header>
      {/* 页面内容(核心:children属性) */}
      <main style={{ flex: 1, padding: '2rem' }}>
        {children}
      </main>
      {/* 页脚 */}
      <footer style={{ padding: '1rem 2rem', borderTop: '1px solid #eee', textAlign: 'center' }}>
        © {new Date().getFullYear()} Next.js实战教程
      </footer>
    </div>
  );
}
  1. 使用布局组件(pages/index.js):
import MainLayout from '../components/layout/MainLayout';
export default function Home() {
  return (
    <MainLayout title="首页 - Next.js实战">
      <h1>欢迎使用Next.js</h1>
      <p>这是经过6年经验优化的实战教程</p>
    </MainLayout>
  );
}

3.3 组件传值与状态管理(3 种场景)

场景 1:父子组件传值(基础)
// 父组件
import Child from './Child';
export default function Parent() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <Child 
        count={count} 
        onIncrement={() => setCount(count + 1)} 
      />
    </div>
  );
}
// 子组件
export default function Child({ count, onIncrement }) {
  return (
    <div>
      <p>计数:{count}</p>
      <button onClick={onIncrement}>+1</button>
    </div>
  );
}
场景 2:跨层级组件传值(进阶)

使用 React Context 避免 props drilling:

// 创建Context(contexts/ThemeContext.js)
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext();
// 提供器组件
export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}
// 自定义Hook(简化使用)
export function useTheme() {
  return useContext(ThemeContext);
}
场景 3:全局状态管理(复杂项目)

推荐使用zustand(轻量、易用):

npm install zustand@4.4.6 --save-exact
// stores/userStore.js
import { create } from 'zustand';
export const useUserStore = create((set) => ({
  user: null,
  login: (userInfo) => set({ user: userInfo }),
  logout: () => set({ user: null }),
}));
// 使用
import { useUserStore } from '../stores/userStore';
export default function Profile() {
  const user = useUserStore(state => state.user);
  const logout = useUserStore(state => state.logout);
  if (!user) return <p>请先登录</p>;
  return (
    <div>
      <p>用户名:{user.name}</p>
      <button onClick={logout}>退出登录</button>
    </div>
  );
}

四、数据获取策略(SSR 核心)

Next.js 提供多种数据获取方式,需根据业务场景选择。以下是实战中最常用的 3 种方案。

4.1 服务端渲染(SSR)- getServerSideProps

适用于实时性要求高的场景(如用户信息、实时数据):

// pages/dashboard.js
export default function Dashboard({ userData, stats }) {
  return (
    <div>
      <h1>用户中心</h1>
      <p>用户名:{userData.name}</p>
      <p>今日访问:{stats.today}</p>
    </div>
  );
}
// 每次请求都会在服务端执行
export async function getServerSideProps(context) {
  try {
    // 1. 获取请求上下文(如cookie、请求头)
    const { req } = context;
    const token = req.cookies.token;
    // 2. 服务端请求(避免跨域问题)
    const [userRes, statsRes] = await Promise.all([
      fetch('https://api.example.com/user', {
        headers: { Authorization: `Bearer ${token}` }
      }),
      fetch('https://api.example.com/stats')
    ]);
    // 3. 处理响应数据
    const userData = await userRes.json();
    const stats = await statsRes.json();
    // 4. 传递给页面组件
    return {
      props: { userData, stats }
    };
  } catch (error) {
    // 错误处理(如未登录重定向)
    return {
      redirect: {
        destination: '/login',
        permanent: false
      }
    };
  }
}

4.2 静态生成(SSG)- getStaticProps

适用于内容不常变化的场景(如博客、文档):

// pages/posts/[id].js
export default function Post({ post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}
// 构建时执行(仅一次)
export async function getStaticProps(context) {
  const</doubaocanvas>