Skip to content

Next.js

Next.js 是一个基于 React 的前端框架,旨在帮助开发者构建高性能、SEO友好的静态网站和网络应用。它扩展了 React 的功能,提供了许多内置特性,如路由、API 调用、认证等,使开发者无需依赖外部库

特性

  1. 服务端渲染(SSR)

在服务器端渲染页面,然后将 HTML 发送到客户端。适合需要 SEO 的内容页面,如博客、电商产品页。

客户端请求页面 → 服务器执行 React 组件 → 生成 HTML → 发送到客户端 → 客户端激活 JavaScript

  1. 静态站点生成(SSG)

构建时生成静态 HTML 文件,无需运行服务器即可提供服务。适合营销页面、文档网站等。

构建时执行 React 组件 → 生成静态 HTML 和 JSON → 部署到 CDN → 客户端直接请求静态资源

  1. 增量静态再生(ISR)

允许在构建后更新静态页面,无需重新构建整个站点。适合数据定期更新的场景。

类似 SSG,但允许设置重新验证时间(revalidate: 60)→ 页面过期后,第一个请求触发后台更新

  1. 自动代码分割

根据路由自动分割代码,只加载当前页面所需的 JavaScript。

  1. 文件系统路由

基于文件结构自动创建路由,无需手动配置。

  1. API 路由

内置支持创建 API 端点,无需额外配置服务器。

数据获取

Next.js 根据渲染时机和数据加载方式,提供了四种主要的数据获取方法:

SSG

  • 执行时机:构建时(Build Time)
  • 适用场景:博客文章、产品页面、营销网站等不需要实时数据的页面。
  • 优点:性能最优,可直接部署到 CDN。
  • 实现方式:通过 getStaticProps 和 getStaticPaths 函数实现。

getStaticProps(静态props)

用于在构建时获取数据并传递给页面组件:

js
// pages/products/[id].js
export async function getStaticProps(context) {
  const res = await fetch(`https://api.example.com/products/${context.params.id}`);
  const product = await res.json();

  return {
    props: {
      product, // 将数据作为props传递给组件
    },
    revalidate: 60 * 60, // 可选:设置ISR的重新验证时间(秒)
  };
}

const ProductPage = ({ product }) => {
  return <div>{product.name}</div>;
};

export default ProductPage;

getStaticPaths(动态路由路径)

用于生成动态路由的所有可能路径(仅适用于动态路由):

js
// pages/products/[id].js
export async function getStaticPaths() {
  const res = await fetch('https://api.example.com/products');
  const products = await res.json();

  const paths = products.map((product) => ({
    params: { id: product.id.toString() },
  }));

  return { paths, fallback: 'blocking' }; // 或 fallback: true
}
  • fallback: 'blocking':未预渲染的路径会等待数据加载后再渲染。
  • fallback: true:未预渲染的路径会先返回 HTML 骨架,然后在客户端加载数据。

SSR

  • 执行时机:每次请求时(Request Time)
  • 适用场景:需要实时数据的页面,如用户仪表盘、购物车。
  • 优点:数据始终保持最新。
  • 实现方式:通过 getServerSideProps 函数实现。

getServerSideProps

用于在每次请求时获取数据:

js
// pages/dashboard.js
export async function getServerSideProps(context) {
  const { req, res } = context;
  
  // 从请求中获取用户会话
  const user = await getUserSession(req);
  
  // 获取用户特定数据
  const data = await fetchUserData(user.id);

  return {
    props: {
      user,
      data,
    },
  };
}

const Dashboard = ({ user, data }) => {
  return <div>Welcome, {user.name}</div>;
};

export default Dashboard;

CSR

  • 执行时机:浏览器端(Client Time)
  • 适用场景:数据频繁更新的动态页面,如社交媒体流。
  • 优点:减少服务器负载,支持增量加载。
  • 实现方式:使用 useEffect 或第三方数据获取库(如 SWR、React Query)

ISR(增量静态再生)

  • 执行时机:构建时 + 后台定期更新
  • 适用场景:需要定期更新的静态页面,如新闻列表、商品目录。
  • 优点:兼具 SSG 的性能和数据时效性。
  • 实现方式:在 getStaticProps 中配置 revalidate 选项。

通过在 getStaticProps 中设置 revalidate 选项,可以实现静态页面的定期更新:

js
export async function getStaticProps() {
  const res = await fetch('https://api.example.com/blog');
  const posts = await res.json();

  return {
    props: { posts },
    revalidate: 60, // 每60秒重新验证一次
  };
}
  • 当第一个用户访问过期页面时,Next.js 会:
    1. 返回旧页面的缓存版本;
    2. 在后台重新生成页面;
    3. 更新缓存,后续请求将使用新页面。

总结

函数执行环境执行时机适用场景
getStaticProps服务端构建时SSSG/ISG
getStaticPaths服务端构建时动态路由的SSG页面
getServerSideProps服务端每次请求时SSR
useEffect客户端组件挂载后CSR

水合(Hydration)

水合是指将静态 HTML(由服务端生成)转换为可交互的 React 应用的过程。具体来说:

  1. 服务端生成完整的 HTML 页面并发送到客户端;
  2. 客户端加载 JavaScript 代码后,React 会:
    • 匹配已有的 DOM 结构与 React 组件树;
    • 绑定事件处理函数;
    • 恢复组件状态(如useState、useReducer)。

这个过程称为水合,因为它给静态 HTML “注入” 了动态交互能力,就像给脱水的物质重新补水一样。

实现原理

1. 服务端渲染阶段

当用户首次请求页面时:

  1. Next.js 服务器执行 React 组件(包括getServerSideProps或getStaticProps);
  2. React 将组件转换为 HTML 字符串;
  3. Next.js 在 HTML 中嵌入特殊标记(如data-reactroot、data-reactid)和状态序列化数据;
  4. 最终 HTML 被发送到客户端,包含:
    • 完整的 DOM 结构;
    • 内联的初始状态(如__NEXT_DATA__脚本);
    • 指向 JavaScript bundle 的引用。

2. 客户端水合阶段

当客户端加载页面时:

  1. 浏览器解析 HTML并渲染静态内容;
  2. JavaScript bundle(如main.js)加载完成后,React 启动;
  3. React 执行组件逻辑,生成虚拟 DOM;
  4. React比对虚拟 DOM 与现有 DOM:
    • 如果匹配,直接绑定事件(不重建 DOM);
    • 如果不匹配,替换不匹配的部分(可能导致性能损耗);
  5. 组件状态(如useState)从序列化数据中恢复;
  6. 页面变为完全交互的 React 应用。

关键技术细节

1. 状态序列化与恢复

Next.js 通过 __NEXT_DATA__ 全局变量将服务端状态传递给客户端:

html
<script id="__NEXT_DATA__" type="application/json">
  {
    "props": {
      "pageProps": {
        "user": { "name": "John", "age": 30 }
      }
    },
    "pathname": "/profile",
    "buildId": "12345"
  }
</script>

客户端 React 通过此数据恢复组件状态,避免重复请求数据。

2. 事件处理绑定

水合过程中,React 不会重新创建 DOM 节点,而是:

  • 遍历已有 DOM;
  • 为匹配的节点绑定事件处理函数(如onClick);
  • 使用事件委托机制提高效率。

3. 避免DOM重建

React 通过 Diff 算法确保:

  • 如果服务端生成的 HTML 与客户端虚拟 DOM 匹配,直接复用 DOM;
  • 如果不匹配,React 会标记不匹配的节点并重建(可能导致闪烁或性能问题)。

水合优化策略

1. 使用next/dynamic实现懒加载

对于大型组件或仅客户端需要的模块(如地图、图表),使用动态导入:

js
import dynamic from 'next/dynamic';

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

2. 优化图像

next/image是 Next.js 内置的图像优化组件,可自动处理压缩、格式转换、懒加载等,避免图像加载阻塞水合过程。

  • 自动压缩:图片会被压缩到合适尺寸(如 800x600 的图不会加载原始 4K 分辨率)。
  • 格式转换:根据浏览器支持自动转换为 WebP/AVIF 等高效格式,减小体积。
  • 懒加载:loading="lazy"的缩略图会在进入视口时才加载,不阻塞首屏水合。
  • 不阻塞 JS 执行:图片加载通过的异步加载机制处理,不会阻塞 React 的水合过程。

3. 懒加载非关键组件

对于不在首屏的非关键组件(如页脚评论、折叠面板内容),可通过loading="lazy"或next/dynamic延迟加载,减少初始水合的工作量。

tsx
// pages/blog/[slug].js(博客详情页)
import dynamic from 'next/dynamic';
import { getBlogPost } from '../../api/blog';

// 动态导入非首屏组件,设置懒加载
const RelatedPosts = dynamic(() => import('../../components/RelatedPosts'), {
  loading: () => <p>加载相关文章中...</p>, // 加载状态占位
});

const CommentSection = dynamic(() => import('../../components/CommentSection'), {
  loading: () => <p>加载评论区中...</p>,
});

export async function getStaticProps({ params }) {
  const post = await getBlogPost(params.slug);
  return { props: { post } };
}

export default function BlogPost({ post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} /> {/* 首屏内容 */}
      
      {/* 非首屏组件:滚动到可见区域才加载 */}
      <RelatedPosts category={post.category} />
      <CommentSection postId={post.id} />
    </article>
  );
}

核心功能

API 路由

Next.js 内置的 API 层,允许在项目中直接创建后端 API 端点,无需额外配置服务器。

核心特点

  • 文件即路由:在pages/api目录下创建的文件会自动映射为 API 路由。
  • Node.js 环境:运行在 Node.js 服务器上,可访问文件系统、数据库等。
  • 请求处理:通过req和res对象处理 HTTP 请求和响应。

示例代码

js
// pages/api/users/[id].js
export default async function handler(req, res) {
  const { id } = req.query;
  
  if (req.method === 'GET') {
    // 查询数据库获取用户
    const user = await db.user.findUnique({ where: { id } });
    res.status(200).json(user);
  } else if (req.method === 'PUT') {
    // 更新用户信息
    const updatedUser = await db.user.update({
      where: { id },
      data: req.body,
    });
    res.status(200).json(updatedUser);
  } else {
    res.status(405).end(); // 方法不允许
  }
}

应用场景

  1. 构建后端服务(如用户认证、数据 CRUD)
  2. 代理第三方 API(添加身份验证或缓存)
  3. 实现服务器端逻辑(如文件上传、支付处理)

自定义App

通过覆盖默认的App组件,可以控制页面初始化、全局状态和样式。

核心特点

  • 统一布局:为所有页面添加统一的布局(如导航栏、页脚)。
  • 全局状态:初始化状态管理库(如 Redux、Zustand)。
  • 样式注入:引入全局 CSS 或 CSS-in-JS 解决方案。
js
// pages/_app.js
import { SessionProvider } from 'next-auth/react';
import '@/styles/globals.css'; // 全局样式

function MyApp({ Component, pageProps: { session, ...pageProps } }) {
  return (
    <SessionProvider session={session}>
      <Component {...pageProps} />
    </SessionProvider>
  );
}

export default MyApp;

应用场景

  • 实现全局认证状态
  • 应用全局样式或主题
  • 添加页面过渡动画
  • 初始化第三方库(如 Sentry、Google Analytics)

自定义Document

通过覆盖默认的Document组件,可以控制HTML文档的结构和头部信息。 核心特点

  • 修改 HTML 结构:添加自定义元标签、脚本或样式。
  • 服务端渲染优化:控制页面头部资源加载顺序。
  • 非 React 元素:插入纯 HTML 元素(如 noscript 标签)。
tsx
// pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);
    return { ...initialProps };
  }

  render() {
    return (
      <Html lang="zh-CN">
        <Head>
          <meta name="viewport" content="width=device-width, initial-scale=1" />
          <link rel="preconnect" href="https://fonts.googleapis.com" />
        </Head>
        <body className="font-sans">
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

中间件(Middleware)

Next.js 中间件允许在请求处理流程中插入自定义逻辑,可以用于身份验证、重定向、日志记录等。

核心特点

  • 请求拦截:在请求到达页面之前执行逻辑。
  • 无状态:中间件不维护状态,适合处理简单逻辑。
tsx
// middleware.js
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // 检查用户是否登录
  const token = request.cookies.get('token');
  
  // 未登录用户访问需要认证的路径时重定向到登录页
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
  
  // 修改请求头
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set('x-user-id', token?.value || 'guest');
  
  return NextResponse.next({
    request: { headers: requestHeaders },
  });
}

// 仅对特定路径应用中间件
export const config = {
  matcher: ['/dashboard/:path*', '/api/:path*'],
};

应用场景

  • 身份验证和权限控制
  • 响应缓存策略
  • 请求头修改(如添加 CORS)
  • 基于地理位置的内容重定向

国际化(i18n)

Next.js 内置国际化支持,允许轻松构建多语言应用。

核心特点

  • 自动语言检测:根据用户浏览器偏好自动选择语言。
  • 路由前缀:为不同语言设置路由前缀(如/en/about、/zh/about)。
  • 语言切换:提供 API 在运行时切换语言。
js
// next.config.js
module.exports = {
  i18n: {
    locales: ['en', 'zh', 'ja'],
    defaultLocale: 'en',
    localeDetection: true, // 自动检测用户语言
  },
};
js
// 页面组件中获取当前语言
function HomePage({ locale, locales }) {
  return (
    <div>
      <h1>{t('welcome')}</h1> {/* 使用国际化文本 */}
      
      {/* 语言切换按钮 */}
      {locales.map((l) => (
        <button key={l} onClick={() => switchLocale(l)}>
          {l}
        </button>
      ))}
    </div>
  );
}

// 动态导入语言文件
export async function getStaticProps({ locale }) {
  const translations = await import(`../locales/${locale}.json`);
  return { props: { t: translations.default } };
}

自定义webpack

Next.js 允许通过next.config.js自定义底层 Webpack 配置。

示例:

js
// next.config.js
const path = require('path');

module.exports = {
  webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
    // 添加模块别名
    config.resolve.alias['@components'] = path.join(__dirname, 'components');
    
    // 添加自定义loader
    config.module.rules.push({
      test: /\.svg$/,
      use: ['@svgr/webpack'],
    });
    
    // 开发环境添加调试插件
    if (dev) {
      config.plugins.push(new webpack.HotModuleReplacementPlugin());
    }
    
    return config;
  },
};

最佳实践

1. 怎么解决水合不匹配

  1. 服务端执行了依赖客户端环境的代码
  • 解决方案:将相关代码放在useEffect中,确保只在客户端执行。
  1. 如果服务端渲染时使用的数据(如 API 返回值、用户信息)与客户端水合时的数据不同,会导致 HTML 结构不匹配。
  • 解决方案:
    • 确保服务端和客户端使用相同的数据源,或在useEffect中获取数据以避免水合不匹配。
    • 使用getServerSideProps或getStaticProps获取数据,确保服务端和客户端渲染时使用相同的数据。
  1. 如果服务端渲染时使用了随机数、时间戳等动态数据,会导致水合不匹配。

    • 但是不一定会导致水合失败,可能只是警告。如react会在控制台抛出错误
    • vue会丢弃服务端渲染的内容,重新渲染客户端内容。可能导致页面闪烁。
  2. 水合失败的降级方式:

  • 回退到纯客户端渲染,重新加载页面。
js
// React示例:水合失败后降级为CSR
try {
  ReactDOM.hydrateRoot(document.getElementById('root'), <App />);
} catch (error) {
  console.error('Hydration failed:', error);
  // 清空DOM并重新渲染
  document.getElementById('root').innerHTML = '';
  ReactDOM.createRoot(document.getElementById('root')).render(<App />);
}
  • 使用错误边界捕获水合错误,并在组件中处理降级逻辑。
js
// React错误边界示例
class HydrationErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 更新状态以触发降级渲染
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    console.error('Hydration error:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // 返回降级后的组件
      return <div>加载失败,请刷新页面。</div>;
    }
    return this.props.children;
  }
}

2. 怎么解决性能瓶颈

  1. 选择合适的渲染方式,避免使用SSR,即使用getStaticProps或getStaticPaths来预渲染页面。
  2. 代码修改
    • 使用next/dynamic实现组件懒加载,减少初始加载的 JavaScript 体积。
    • 使用 loading="lazy" 延迟加载非首屏组件
    • 使用 next/image 自动处理图像压缩、格式转换和懒加载:
  3. 使用缓存策略:
    • 如静态资源使用CDN缓存,SSG页面配置缓存策略
    • API 等使用内存缓存或Redis缓存
  4. 服务器优化
    • 优化node配置,如调整线程池大小、使用PM2管理进程
    • 使用负载均衡分担请求压力
    • 优化数据库查询,使用索引、缓存等技术
  5. 性能监控
    • 使用Next.js内置的分析工具(如next build --profile)分析构建性能
    • 使用第三方工具(如Lighthouse、WebPageTest)监控页面加载时间和资源使用情况
    • 使用日志记录和监控工具(如Sentry、New Relic)跟踪性能瓶颈和错误
  6. 传统的React优化
    • 使用React.memo、useMemo、useCallback等优化组件渲染
    • 减少不必要的状态更新和重渲染
    • 使用虚拟列表(如react-window)处理大数据量列表渲染

3. 怎么解决 SEO 问题

  1. 使用服务端渲染(SSR)或静态站点生成(SSG)
    • 确保页面在服务端渲染时生成完整的 HTML 内容,包含必要的元标签和结构。
    • 使用getServerSideProps或getStaticProps获取数据,确保页面内容在首屏加载时可见。
  2. 使用next/head设置页面元信息
    • 在页面组件中使用next/head添加标题、描述、关键词等元标签。
    • 确保每个页面都有唯一的标题和描述,避免重复内容。
  3. 针对爬虫优化
    • 确保页面内容在服务端渲染时可见,避免使用仅客户端渲染的内容。
    • 使用robots.txt文件控制爬虫访问权限,避免不必要的页面被索引。
  4. 使用next/sitemap生成站点地图
  5. 测试验证
    • 使用Google Search Console验证站点所有权,并提交站点地图。
    • 使用Lighthouse等工具检查页面的SEO表现,确保页面符合最佳实践。
    • 定期监控搜索引擎索引状态,确保重要页面被正确索引。
  6. 语义化标签
    • 使用语义化HTML标签(如header、footer、article、section等)提高页面结构的可读性。
    • 确保使用正确的标题标签(h1、h2等)来组织内容层次,帮助搜索引擎理解页面结构。

4. 怎么实现状态管理

  1. 优先React内置的状态管理
    • 使用useState、useReducer等Hooks进行局部状态管理。
    • 使用Context API实现跨组件状态共享。
  2. 使用第三方状态管理库
    • Redux:适用于复杂应用,提供全局状态管理和中间件支持。
    • Zustand:轻量级状态管理库,易于使用,适合小型到中型应用。
    • Recoil:Facebook开发的状态管理库,支持原子状态和派生状态。
  3. 服务端与客户端状态同步
    • 使用getServerSideProps或getStaticProps获取服务端数据,并在组件中初始化状态。
    • 使用 window.__NEXT_DATA__ 获取服务端渲染时的初始状态,确保客户端和服务端状态一致。(注意信息安全)
    • 会话管理:使用NextAuth.js等库处理用户认证状态,确保服务端和客户端状态同步。使用cookie存储会话信息,确保在服务端和客户端都能访问到用户状态。

5. 怎么调试Next.js

  1. 基础调试:console.log、浏览器 DevTools、React DevTools。
  2. 服务端调试:Node.js 调试器、--inspect 标志。
  3. 性能优化:Lighthouse、构建分析、next/performance。
  4. 错误处理:全局错误边界、自定义错误页面。
  5. 生产环境:日志聚合工具、生产环境调试标志。