一文掌握 Next.js 13.x Pages Router

238 阅读3分钟

前言

这两年,前端技术依然日新月异,成熟的项目难免版本老旧,面对客观情况,相比代码洁癖、追求前沿,一句“能跑就行”往往更胜一筹。最近在维护一个 Next.js 项目,一看版本 13.x,那就巩固一下吧,下面是要点提炼。

Next.js 目录结构

  • _app.tsx 用于自定义每个页面的根组件。你可以在这里添加全局样式、布局、页面切换逻辑、全局状态管理等。它包裹了所有页面组件。
  • _document.tsx 用于自定义整个 HTML 文档结构(如 <html><head><body>)。这里适合放置自定义的 meta 标签、字体链接等。只在服务端渲染时运行,不处理页面交互逻辑。
  • pages 目录下的文件会自动成为路由。文件名对应路由路径,文件夹对应嵌套路由。
  • public 目录下的文件会被 Next.js 直接复制到构建输出目录。可以放置静态资源,如图片、字体等。
  • next.config.js 是 Next.js 的配置文件,可以在这里配置一些全局的设置,如环境变量、路由重写、国际化等。

脚本

  • npm run build:运行 build 脚本一次,该脚本将在 .next 文件夹中构建生产应用程序。

  • npm run start:构建完成后, start 脚本将启动一个支持混合页面的 Node.js 服务器,该服务器提供静态生成和服务器端渲染的页面以及 API 路由。

    • Tip:可以在 package.json 中自定义 start 脚本以接受 PORT 参数: "start": "next start -p $PORT"

pre-rendering

默认情况下,Next.js 会对每个页面进行预渲染。这意味着 Next.js 会提前为每个页面生成 HTML,而不是完全依赖客户端 JavaScript 来完成。预渲染可以带来更好的性能和 SEO 优化。

每个生成的 HTML 都关联着该页面所需的最少量 JavaScript 代码。当浏览器加载页面时,对应的 JavaScript 代码会执行,使页面完全可交互(这个过程称为"水合 hydration")。

Next.js 支持两种预渲染形式:

  • 静态生成(Static Generation):在构建时生成 HTML,并在每次请求时复用(推荐方式)
  • 服务端渲染(Server-side Rendering):在每次请求时生成 HTML

(注:hydration 在中文前端领域通常译为"水合"或" hydration 渲染",指将静态 HTML 与客户端 JavaScript 状态结合的过程)

两种方式:Static Generation / Server-side Rendering

  1. Static Generation:在构建时(next build时)生成 HTML,适合静态内容(如博客、文档、marketing pages、E-commerce product listings 等)。可以使用 getStaticPropsgetStaticPaths

ssg.png

  1. Server-side Rendering:在每次请求时生成 HTML,适合动态内容(如用户数据、实时数据等)。可以使用 getServerSideProps

ssr.png

pre-rendering vs no pre-rendering

  1. 预加载/提前加载

pre-rendering.png

  1. 不提前加载

no-pre-rendering.png

Static Generation with Data

在构建时,一些页面需要外部数据或者不需要。

外部数据源:

  • access the file system
  • fetch external API
  • query a database

ssg-with-data.png

getStaticProps:用于静态生成页面的数据获取。它在构建时运行,返回的数据会被序列化成 JSON,并作为 props 传递给页面组件。可以缓存在 CDN。开发环境下,每一次请求都会执行;生产环境下,只有在构建时执行。

由于 getStaticProps 只在构建时运行,意味着它不会运行在客户端,也就是说它不会被打包到浏览器中。因此,写代码时可以写数据库连接字符串、API 密钥等敏感信息,而不必担心泄露给浏览器。同时,将无法使用仅在请求时间才可用的数据,例如查询参数或 HTTP 头信息。

export default function Home(props) { ... }

export async function getStaticProps() {
  // Get external data from the file system, API, DB, etc.
  const data = ...

  // The value of the `props` key will be
  //  passed to the `Home` component
  return {
    props: ...
  }
}

getStaticProps.png

Server-side Rendering with Data

ssr-with-data.png

getServerSideProps:用于服务端渲染页面的数据获取。它在每次请求时运行,返回的数据会被序列化成 JSON,并作为 props 传递给页面组件。

export default function Home(props) { ... }

export async function getServerSideProps(context) {
  // Get external data from the file system, API, DB, etc.
  const data = ...

  // The value of the `props` key will be
  //  passed to the `Home` component
  return {
    props: {
      // props for your component
    },
  }
}

context 参数包含与当前请求相关的信息,常用属性有:

  • params:动态路由参数(如 [id].js 中的 id)。
  • req:Node.js 的 HTTP 请求对象(IncomingMessage)。
  • res:Node.js 的 HTTP 响应对象(ServerResponse)。
  • query:URL 查询参数对象。
  • resolvedUrl:请求的实际 URL(包含查询参数)。
  • localelocalesdefaultLocale:与国际化相关的信息(如启用 i18n 时)。

getServerSideProps:用于服务端渲染页面的数据获取。它在每次请求时运行,返回的数据会被序列化成 JSON,并作为 props 传递给页面组件。

export default function Home(props) { ... }

export async function getServerSideProps(context) {
  // Get external data from the file system, API, DB, etc.
  const data = ...

  // The value of the `props` key will be
  //  passed to the `Home` component
  return {
    props: {
      // props for your component
    },
  }
}

context 参数包含与当前请求相关的信息,常用属性有:

  • params:动态路由参数(如 [id].js 中的 id)。
  • req:Node.js 的 HTTP 请求对象(IncomingMessage)。
  • res:Node.js 的 HTTP 响应对象(ServerResponse)。
  • query:URL 查询参数对象。
  • resolvedUrl:请求的实际 URL(包含查询参数)。
  • localelocalesdefaultLocale:与国际化相关的信息(如启用 i18n 时)。

Client-side Rendering(no pre-rendering)

  • 静态生成(预渲染) 页面中不需要外部数据的部分
  • 当页面加载时,通过客户端 JavaScript 获取外部数据,并填充剩余部分

csr.png

适用于频繁更新数据的页面,或者数据依赖于用户输入的页面。例如 dashboard 页面,因为它是一个私有的、特定用户的页面,这种页面不需要预渲染,每一次请求都需要获取最新的数据。

fetch data with SWR

SWR 是一个 React Hooks 库,用于数据获取和缓存。它的核心思想是:Stale-While-Revalidate,即先返回缓存的数据,然后异步请求最新的数据并更新缓存。

import useSWR from 'swr';

function Profile() {
  const { data, error } = useSWR('/api/user', fetch);

  if (error) return <div>failed to load</div>;
  if (!data) return <div>loading...</div>;
  return <div>hello {data.name}!</div>;
}

Dynamic Routes

动态路由是 Next.js 的一个强大特性,可以根据 URL 参数动态生成页面。可以通过文件名中的方括号来定义动态路由。例如,pages/posts/[id].js 可以匹配 /posts/1/posts/2 等 URL。

dynamic-routes.png

getStaticPaths:用于静态生成动态路由页面时,指定哪些路径需要预渲染。它在构建时运行,返回一个包含所有需要预渲染的路径的数组。同样可以获取任何外部数据源。开发环境下,每次请求都会执行。生产环境下,只有在构建时执行。

动态路由页面必须包含:

  • getStaticPaths:用于指定哪些路径需要预渲染。
  • getStaticProps:用于获取每个路径对应的数据。

how-to-statically-generate-pages-with-d-r.png

import Layout from '../../components/layout';

export default function Post(props) {
  return (
    <Layout>
      <h1>{props.data.title}</h1>
      <p>{props.data.content}</p>
    </Layout>
  );
}

export async function getStaticPaths() {
  // Return a list of possible value for id
  const paths = [{ params: { id: '1' } }, { params: { id: '2' } }, { params: { id: '3' } }];
  return {
    paths,
    fallback: false, // See the "fallback" section below
  };
}

export async function getStaticProps({ params }) {
  // Fetch necessary data for the blog post using params.id
  const data = await fetch(`https://.../${params.id}`);
  return {
    props: {
      data,
    },
  };
}
  • fallback: false:只有 paths 中返回的路径会被生成,其他路径访问会直接返回 404。
  • fallback: true:未在 paths 中的路径,首次访问时会在服务端生成静态页面,之后缓存。
  • fallback: 'blocking':和 true 类似,但用户会等待页面生成后再显示(不会看到 loading 状态)。

useRouter

useRouter 是 Next.js 提供的一个 Hook,用于获取路由信息和进行路由跳转。可以在函数组件中使用。

import { useRouter } from 'next/router';

function Post() {
  const router = useRouter();
  const { id } = router.query;

  return <p>Post: {id}</p>;
}

API Routes

API Routes 是 Next.js 提供的一种功能,可以在 pages/api 目录下创建 API 接口。每个文件对应一个 API 路由,可以处理 GET、POST、PUT、DELETE 等请求。

// pages/api/hello.ts
export default function handler(req, res) {
  res.status(200).json({ name: 'John Doe' });
}

然后访问 /api/hello 即可获取 JSON 数据。

注意:你不应该从 getStaticPropsgetStaticPaths 获取 API 路由。相反,你应该直接在 getStaticProps 或 getStaticPaths 中编写你的服务器端代码(或者调用一个辅助函数)。

原因如下:getStaticPropsgetStaticPaths 仅在服务器端运行,永远不会在客户端运行。此外,这些函数不会包含在浏览器 JS 包中。这意味着你可以编写直接执行数据库查询等代码,而无需发送到浏览器。

处理表单输入

在页面上创建一个表单,然后使用 fetch API 将数据发送到 API 路由。

export default function handler(req, res) {
  const email = req.body.email;
  // Then save email to your database, etc...
}

Headless CMS

Headless CMS(无头内容管理系统)是一种只负责内容管理和存储、不负责内容展示的 CMS。它只提供内容的 API(通常是 REST 或 GraphQL),前端页面如何展示内容完全由开发者决定。

特点:

  • 没有“头部”(即没有内置的前端模板或页面渲染)
  • 内容通过 API 提供给任何平台(Web、App、小程序等)
  • 前后端分离,灵活性高

常见 headless CMS:

  • Contentful
  • Strapi
  • Sanity
  • Prismic

适用场景:

  • 多端内容分发
  • 需要自定义前端技术栈
  • 追求内容与展示彻底分离

适合用 Preview Mode 的场景:

  • 需要在 CMS 中编辑内容,并希望在保存后立即预览效果。