MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Next.js 中 getStaticPaths 的实现细节与优化策略

2021-06-243.3k 阅读

Next.js 中 getStaticPaths 的基础概念

在 Next.js 开发中,getStaticPaths 是一个关键函数,主要用于生成静态页面的路径。它允许开发者在构建时确定页面所需的动态路径。

假设我们有一个博客应用,每个文章都有一个唯一的 ID。通过 getStaticPaths,我们可以为每篇文章生成对应的静态页面路径。例如,文章 ID 为 1 的文章路径可能是 /posts/1,ID 为 2 的文章路径是 /posts/2 等等。

何时使用 getStaticPaths

  1. 动态路由页面:当页面路径依赖于动态参数,且这些参数在构建时已知时,就可以使用 getStaticPaths。例如产品详情页,每个产品有唯一的 ID,我们希望为每个产品生成静态页面。
  2. 内容驱动的网站:对于博客、新闻网站等,文章数量可能很多,但它们的路径结构是固定的(如 /article/[slug]),getStaticPaths 可以在构建时生成所有文章的静态路径。

getStaticPaths 的基本语法

在 Next.js 页面组件文件(如 pages/post/[id].js)中,我们可以定义 getStaticPaths 函数:

export async function getStaticPaths() {
  // 这里返回包含 paths 和 fallback 属性的对象
  return {
    paths: [
      { params: { id: '1' } },
      { params: { id: '2' } }
    ],
    fallback: false
  };
}

在上述代码中:

  • paths 数组包含了每个页面的路径参数。每个对象的 params 属性定义了动态路由参数的值。
  • fallback 属性控制了当请求一个在构建时未生成的路径时的行为。

fallback 属性详解

  1. fallback: false:如果设置为 false,那么只有在 paths 数组中定义的路径会被预渲染。当用户请求一个不在 paths 中的路径时,会返回 404 页面。 例如,我们只在 paths 中定义了 id12 的路径,如果用户请求 /posts/3,将会看到 404 页面。
  2. fallback: true:当设置为 true 时,在构建时没有生成的路径,在第一次请求时会进行动态渲染。一旦渲染完成,该页面就会被缓存,后续请求相同路径时将直接返回缓存的页面。 假设我们的博客应用有很多文章,在构建时只生成了热门文章的路径。当用户请求一篇未预渲染的文章路径时,Next.js 会在运行时渲染该页面,并且后续访问相同路径时,会从缓存中读取页面,提高响应速度。
  3. fallback: 'blocking':与 fallback: true 类似,但在请求未预渲染的路径时,会阻塞请求,直到页面渲染完成。这意味着用户会看到一个加载指示器,而不是先看到空白页面然后页面再加载出来。这在用户体验上更为友好,尤其适用于对内容加载速度要求较高的应用场景。

getStaticPaths 与数据获取

在实际应用中,getStaticPaths 通常与数据获取相关联。我们需要从数据源(如数据库、CMS 等)获取数据来确定页面路径。

从 JSON 文件获取数据

假设我们有一个 posts.json 文件,内容如下:

[
  { "id": "1", "title": "Post 1" },
  { "id": "2", "title": "Post 2" }
]

pages/post/[id].js 中可以这样写:

import fs from 'fs';
import path from 'path';

export async function getStaticPaths() {
  const jsonFilePath = path.join(process.cwd(), 'posts.json');
  const jsonData = JSON.parse(fs.readFileSync(jsonFilePath, 'utf8'));
  const paths = jsonData.map(post => ({
    params: { id: post.id.toString() }
  }));
  return {
    paths,
    fallback: false
  };
}

export async function getStaticProps(context) {
  const jsonFilePath = path.join(process.cwd(), 'posts.json');
  const jsonData = JSON.parse(fs.readFileSync(jsonFilePath, 'utf8'));
  const post = jsonData.find(p => p.id.toString() === context.params.id);
  return {
    props: {
      post
    },
    revalidate: 60
  };
}

const PostPage = ({ post }) => {
  return (
    <div>
      <h1>{post.title}</h1>
    </div>
  );
};

export default PostPage;

在上述代码中,getStaticPathsposts.json 文件读取数据并生成路径。getStaticProps 则根据路径参数获取具体文章的数据。

从数据库获取数据

以使用 MongoDB 为例,假设我们有一个 posts 集合。首先安装 mongodb 包:

npm install mongodb

然后在 pages/post/[id].js 中:

import { MongoClient } from'mongodb';

export async function getStaticPaths() {
  const client = await MongoClient.connect('mongodb://localhost:27017');
  const db = client.db('my-blog');
  const postsCollection = db.collection('posts');
  const posts = await postsCollection.find().toArray();
  const paths = posts.map(post => ({
    params: { id: post._id.toString() }
  }));
  client.close();
  return {
    paths,
    fallback: false
  };
}

export async function getStaticProps(context) {
  const client = await MongoClient.connect('mongodb://localhost:27017');
  const db = client.db('my-blog');
  const postsCollection = db.collection('posts');
  const post = await postsCollection.findOne({ _id: new ObjectId(context.params.id) });
  client.close();
  return {
    props: {
      post
    },
    revalidate: 60
  };
}

import { ObjectId } from 'mongodb';
const PostPage = ({ post }) => {
  return (
    <div>
      <h1>{post.title}</h1>
    </div>
  );
};

export default PostPage;

这里 getStaticPaths 从 MongoDB 中获取所有文章数据并生成路径,getStaticProps 根据路径参数从数据库获取具体文章数据。

getStaticPaths 的实现细节

  1. 构建时执行getStaticPaths 函数在构建时执行,这意味着它会在 npm run buildyarn build 命令执行期间运行。它不会在每次请求时运行,因此适用于数据相对稳定,不需要频繁更新的场景。
  2. 并行处理:Next.js 会并行处理 getStaticPaths 生成的路径,这有助于提高构建速度。例如,如果有大量路径需要生成,Next.js 会利用多核 CPU 的优势,同时处理多个路径的生成,大大缩短构建时间。
  3. 缓存机制:对于 fallback: truefallback: 'blocking' 的情况,生成的页面会被缓存。缓存机制基于文件系统,Next.js 会将渲染后的页面以文件形式存储在 .next/server/pages 目录下。当后续有相同路径的请求时,直接从该目录读取文件,提高响应速度。

getStaticPaths 的优化策略

  1. 减少路径生成数量:如果不必要生成过多路径,可以根据业务逻辑进行筛选。例如在博客应用中,如果有一些草稿文章,不需要为草稿文章生成路径,可以在获取数据时过滤掉草稿状态的文章。
  2. 合理设置 fallback:根据应用场景合理设置 fallback 属性。如果应用对性能要求极高,且大部分页面访问频率较高,可以设置 fallback: false,并在构建时生成所有可能的路径。如果页面数量巨大,且部分页面访问频率较低,可以设置 fallback: truefallback: 'blocking',以减少构建时间并提高运行时的灵活性。
  3. 优化数据获取:在 getStaticPaths 中进行数据获取时,尽量优化查询。例如在数据库查询中使用索引,减少查询返回的数据量等。对于从 API 获取数据,可以设置合理的缓存机制,避免在构建时重复请求相同数据。
  4. 增量静态再生:结合 revalidate 参数在 getStaticProps 中使用增量静态再生。即使页面是在构建时生成的,也可以设置一个时间间隔(如 revalidate: 60 表示 60 秒),在这个时间间隔后,页面会在下次请求时重新验证并更新数据,保证页面数据的新鲜度。

处理大量路径

当面临大量路径需要生成时,比如一个拥有数万篇文章的博客,以下是一些处理方法:

  1. 分页处理:在数据获取时采用分页技术。例如从数据库查询时,每次只查询一部分数据,生成相应的路径。然后通过多次查询,逐步生成所有路径。
export async function getStaticPaths() {
  const client = await MongoClient.connect('mongodb://localhost:27017');
  const db = client.db('my-blog');
  const postsCollection = db.collection('posts');
  const totalPosts = await postsCollection.countDocuments();
  const pageSize = 100;
  const totalPages = Math.ceil(totalPosts / pageSize);
  let paths = [];
  for (let i = 0; i < totalPages; i++) {
    const skip = i * pageSize;
    const posts = await postsCollection.find().skip(skip).limit(pageSize).toArray();
    const pagePaths = posts.map(post => ({
      params: { id: post._id.toString() }
    }));
    paths = paths.concat(pagePaths);
  }
  client.close();
  return {
    paths,
    fallback: false
  };
}
  1. 基于时间或热度筛选:只生成近期发布或热门文章的路径,对于其他文章路径,采用 fallback: truefallback: 'blocking' 的方式,在请求时动态生成。
import { MongoClient } from'mongodb';
import { ObjectId } from 'mongodb';

export async function getStaticPaths() {
  const client = await MongoClient.connect('mongodb://localhost:27017');
  const db = client.db('my-blog');
  const postsCollection = db.collection('posts');
  const oneWeekAgo = new Date();
  oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
  const recentPosts = await postsCollection.find({ publishedAt: { $gte: oneWeekAgo } }).toArray();
  const popularPosts = await postsCollection.find({ views: { $gt: 100 } }).toArray();
  const allPosts = recentPosts.concat(popularPosts);
  const paths = allPosts.map(post => ({
    params: { id: post._id.toString() }
  }));
  client.close();
  return {
    paths,
    fallback: true
  };
}

export async function getStaticProps(context) {
  const client = await MongoClient.connect('mongodb://localhost:27017');
  const db = client.db('my-blog');
  const postsCollection = db.collection('posts');
  const post = await postsCollection.findOne({ _id: new ObjectId(context.params.id) });
  client.close();
  return {
    props: {
      post
    },
    revalidate: 60
  };
}

与 API Routes 结合使用

  1. 数据获取的解耦:可以将数据获取逻辑从 getStaticPathsgetStaticProps 中分离出来,通过 API Routes 进行数据获取。这样可以使代码结构更清晰,并且便于维护和测试。 首先在 pages/api/posts.js 中创建一个 API Route:
import { MongoClient } from'mongodb';

export default async function handler(req, res) {
  const client = await MongoClient.connect('mongodb://localhost:27017');
  const db = client.db('my-blog');
  const postsCollection = db.collection('posts');
  const posts = await postsCollection.find().toArray();
  client.close();
  res.status(200).json(posts);
}

然后在 pages/post/[id].js 中:

export async function getStaticPaths() {
  const response = await fetch('/api/posts');
  const posts = await response.json();
  const paths = posts.map(post => ({
    params: { id: post._id.toString() }
  }));
  return {
    paths,
    fallback: false
  };
}

export async function getStaticProps(context) {
  const response = await fetch(`/api/posts/${context.params.id}`);
  const post = await response.json();
  return {
    props: {
      post
    },
    revalidate: 60
  };
}
  1. 动态更新数据:通过 API Routes 可以方便地实现数据的动态更新。例如,当文章数据在数据库中更新后,可以通过 API Route 触发重新验证逻辑,使页面数据及时更新。

错误处理

getStaticPaths 中进行数据获取和路径生成时,可能会遇到各种错误,如数据库连接失败、API 请求失败等。需要进行适当的错误处理。

  1. 数据库连接错误:在使用数据库时,如 MongoDB,连接可能会失败。可以在 try - catch 块中处理错误:
export async function getStaticPaths() {
  try {
    const client = await MongoClient.connect('mongodb://localhost:27017');
    const db = client.db('my-blog');
    const postsCollection = db.collection('posts');
    const posts = await postsCollection.find().toArray();
    const paths = posts.map(post => ({
      params: { id: post._id.toString() }
    }));
    client.close();
    return {
      paths,
      fallback: false
    };
  } catch (error) {
    console.error('Database connection error:', error);
    return {
      paths: [],
      fallback: false
    };
  }
}
  1. API 请求错误:当通过 fetch 从 API 获取数据时,可能会遇到网络问题或 API 响应错误。同样可以使用 try - catch
export async function getStaticPaths() {
  try {
    const response = await fetch('/api/posts');
    if (!response.ok) {
      throw new Error('API request failed');
    }
    const posts = await response.json();
    const paths = posts.map(post => ({
      params: { id: post.id.toString() }
    }));
    return {
      paths,
      fallback: false
    };
  } catch (error) {
    console.error('API request error:', error);
    return {
      paths: [],
      fallback: false
    };
  }
}

与国际化(i18n)的结合

在国际化应用中,getStaticPaths 也需要考虑多语言的路径生成。例如,不同语言的文章路径可能不同。 假设我们使用 next - i18next 进行国际化,在 next.config.js 中配置如下:

const nextI18next = require('next - i18next');

const nextI18nextInstance = nextI18next({
  defaultLocale: 'en',
  locales: ['en', 'zh - CN'],
  localePath: path.resolve('./public/locales')
});

module.exports = nextI18nextInstance({
  reactStrictMode: true
});

pages/post/[id].js 中:

import { useTranslation } from 'next - i18next';

export async function getStaticPaths() {
  const jsonFilePath = path.join(process.cwd(), 'posts.json');
  const jsonData = JSON.parse(fs.readFileSync(jsonFilePath, 'utf8'));
  const paths = jsonData.flatMap(post => {
    return ['en', 'zh - CN'].map(locale => ({
      params: { id: post.id.toString() },
      locale
    }));
  });
  return {
    paths,
    fallback: false
  };
}

export async function getStaticProps(context) {
  const { locale } = context;
  const jsonFilePath = path.join(process.cwd(), 'posts.json');
  const jsonData = JSON.parse(fs.readFileSync(jsonFilePath, 'utf8'));
  const post = jsonData.find(p => p.id.toString() === context.params.id);
  const { t } = await useTranslation('common', { locale });
  return {
    props: {
      post,
      t
    },
    revalidate: 60
  };
}

const PostPage = ({ post, t }) => {
  return (
    <div>
      <h1>{t('post.title', { title: post.title })}</h1>
    </div>
  );
};

export default PostPage;

在上述代码中,getStaticPaths 为每个文章在不同语言下生成路径,getStaticProps 根据请求的语言获取相应的翻译。

总结

getStaticPaths 在 Next.js 中是实现静态页面路径生成的重要函数。通过深入理解其实现细节和优化策略,开发者可以构建出高性能、可扩展的前端应用。从数据获取到错误处理,从大量路径处理到与其他功能(如 API Routes、国际化)的结合,都需要开发者根据具体业务场景进行合理的设计和实现,以提供最佳的用户体验。