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

深入浅出Next.js动态路由机制

2024-09-222.2k 阅读

Next.js 动态路由机制基础

动态路由的概念

在传统的 Web 开发中,路由通常是静态的,即每个页面都对应一个固定的 URL 路径。例如,一个关于产品详情的页面可能固定在 /products/product1 这样的路径上。然而,在实际应用中,我们常常需要根据不同的数据生成大量相似结构的页面,比如电商平台上每个商品都有自己的详情页,如果为每个商品都手动编写一个静态路由,那将是极其繁琐且不切实际的。

动态路由则允许我们根据变量来生成路由。以电商平台为例,我们可以创建一个动态路由 /products/[id],其中 [id] 就是一个动态参数。当用户访问 /products/123 时,123 就会作为 id 参数的值,我们可以根据这个 id 值从数据库中获取对应的商品信息并展示在页面上。

Next.js 中的动态路由实现方式

在 Next.js 中,实现动态路由非常直观。我们通过在页面文件名中使用方括号 [] 来定义动态参数。例如,创建一个名为 pages/products/[id].js 的文件,这个文件就代表了产品详情页的动态路由。

简单代码示例

假设我们有一个简单的 Next.js 项目,目录结构如下:

my-next-project/
├── pages/
│   ├── products/
│   │   ├── [id].js
│   ├── index.js

pages/products/[id].js 文件中,我们可以这样编写代码:

import React from'react';
import { useRouter } from 'next/router';

const ProductDetails = () => {
  const router = useRouter();
  const { id } = router.query;

  return (
    <div>
      <h1>Product Details: {id}</h1>
      {/* 这里可以根据 id 从 API 获取产品详细信息并展示 */}
    </div>
  );
};

export default ProductDetails;

在上述代码中,我们通过 useRouter 钩子函数获取到路由对象 router,然后从 router.query 中提取出动态参数 id。这样,当用户访问 /products/123 时,页面就会显示 “Product Details: 123”。

动态路由与数据获取

在动态路由页面获取数据

当我们有了动态路由页面后,通常需要根据动态参数从 API 或数据库中获取相应的数据。在 Next.js 中,我们可以使用 getStaticPropsgetServerSideProps 来实现这一目的。

使用 getStaticProps 获取数据

getStaticProps 是一个在构建时运行的函数,它会将获取到的数据作为 props 传递给页面组件。这意味着页面在构建时就已经生成好了,适用于数据不经常变化的情况。

继续以上面的产品详情页为例,假设我们有一个 API 可以根据产品 id 获取产品详情,我们可以这样编写 getStaticProps

import React from'react';
import { useRouter } from 'next/router';

const ProductDetails = ({ product }) => {
  return (
    <div>
      <h1>{product.title}</h1>
      <p>{product.description}</p>
    </div>
  );
};

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

  return {
    props: {
      product
    },
    revalidate: 60 * 60 * 24 // 一天后重新验证数据(仅在 Next.js 10+ 支持)
  };
}

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: false };
}

export default ProductDetails;

在上述代码中,getStaticPaths 函数用于生成所有可能的动态路由路径。它从 API 获取所有产品列表,然后为每个产品生成一个路径对象,其中 params 包含了 idfallback 设置为 false 表示如果请求的路径不在 paths 中,将返回 404 页面。

getStaticProps 函数根据传入的 id 从 API 获取产品详情,并将其作为 props 传递给页面组件。revalidate 选项设置了数据重新验证的时间间隔,这里设置为一天。

使用 getServerSideProps 获取数据

getServerSideProps 是在每次请求时运行的函数,这意味着每次用户访问页面时都会获取最新的数据。适用于数据经常变化的场景,比如实时股票价格页面。

以下是使用 getServerSideProps 的示例:

import React from'react';
import { useRouter } from 'next/router';

const ProductDetails = ({ product }) => {
  return (
    <div>
      <h1>{product.title}</h1>
      <p>{product.description}</p>
    </div>
  );
};

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

  return {
    props: {
      product
    }
  };
}

export default ProductDetails;

在这个示例中,getServerSideProps 函数在每次请求时从 API 获取产品详情,并将其传递给页面组件。与 getStaticProps 不同的是,这里没有 getStaticPaths 函数,因为路径不是在构建时生成的,而是每次请求时动态获取数据。

处理动态路由中的数据加载状态

在获取数据的过程中,我们需要向用户展示数据加载状态,以免用户以为页面无响应。在 Next.js 中,我们可以通过 useStateuseEffect 钩子函数来实现。

import React, { useState, useEffect } from'react';
import { useRouter } from 'next/router';

const ProductDetails = () => {
  const router = useRouter();
  const { id } = router.query;
  const [product, setProduct] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchProduct = async () => {
      const res = await fetch(`https://api.example.com/products/${id}`);
      const data = await res.json();
      setProduct(data);
      setLoading(false);
    };

    if (id) {
      fetchProduct();
    }
  }, [id]);

  return (
    <div>
      {loading && <p>Loading...</p>}
      {product && (
        <div>
          <h1>{product.title}</h1>
          <p>{product.description}</p>
        </div>
      )}
    </div>
  );
};

export default ProductDetails;

在上述代码中,我们使用 useState 定义了 productloading 状态。useEffect 钩子函数在 id 变化时触发,通过 fetch 获取产品数据,并更新 productloading 状态。在页面渲染时,如果 loadingtrue,则显示 “Loading...”,否则显示产品详情。

嵌套动态路由

什么是嵌套动态路由

在实际项目中,我们可能会遇到更复杂的路由结构,比如一个博客系统,每个博客分类下有多个博客文章,每个文章又有多个评论。这时,我们就需要用到嵌套动态路由。

嵌套动态路由允许我们在动态路由的基础上进一步嵌套动态参数。例如,我们可以有这样的路由结构:/categories/[categoryId]/posts/[postId]/comments/[commentId]

Next.js 中实现嵌套动态路由

在 Next.js 中,实现嵌套动态路由同样通过文件名的方式。假设我们有一个博客项目,目录结构如下:

blog-project/
├── pages/
│   ├── categories/
│   │   ├── [categoryId]/
│   │   │   ├── posts/
│   │   │   │   ├── [postId].js
│   │   │   │   ├── comments/
│   │   │   │   │   ├── [commentId].js
│   ├── index.js

pages/categories/[categoryId]/posts/[postId].js 文件中,我们可以这样编写代码:

import React from'react';
import { useRouter } from 'next/router';

const PostDetails = () => {
  const router = useRouter();
  const { categoryId, postId } = router.query;

  return (
    <div>
      <h1>Post Details in Category {categoryId}: {postId}</h1>
      {/* 这里可以根据 categoryId 和 postId 获取文章详情并展示 */}
    </div>
  );
};

export default PostDetails;

pages/categories/[categoryId]/posts/[postId]/comments/[commentId].js 文件中:

import React from'react';
import { useRouter } from 'next/router';

const CommentDetails = () => {
  const router = useRouter();
  const { categoryId, postId, commentId } = router.query;

  return (
    <div>
      <h1>Comment Details in Post {postId} of Category {categoryId}: {commentId}</h1>
      {/* 这里可以根据 categoryId、postId 和 commentId 获取评论详情并展示 */}
    </div>
  );
};

export default CommentDetails;

通过上述代码,我们可以获取到嵌套的动态参数,并根据这些参数进行相应的数据获取和展示。

嵌套动态路由的数据获取策略

与普通动态路由类似,嵌套动态路由页面的数据获取也可以使用 getStaticPropsgetServerSideProps。例如,在 pages/categories/[categoryId]/posts/[postId].js 中使用 getStaticProps

import React from'react';
import { useRouter } from 'next/router';

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

export async function getStaticProps(context) {
  const { categoryId, postId } = context.params;
  const res = await fetch(`https://api.example.com/categories/${categoryId}/posts/${postId}`);
  const post = await res.json();

  return {
    props: {
      post
    }
  };
}

export async function getStaticPaths() {
  const categoryRes = await fetch('https://api.example.com/categories');
  const categories = await categoryRes.json();

  const paths = [];

  for (const category of categories) {
    const postRes = await fetch(`https://api.example.com/categories/${category.id}/posts`);
    const posts = await postRes.json();

    for (const post of posts) {
      paths.push({
        params: { categoryId: category.id.toString(), postId: post.id.toString() }
      });
    }
  }

  return { paths, fallback: false };
}

export default PostDetails;

在上述代码中,getStaticPaths 函数需要生成所有可能的嵌套动态路由路径。它首先获取所有分类,然后针对每个分类获取其下的所有文章,为每篇文章生成一个路径对象。getStaticProps 函数根据传入的 categoryIdpostId 从 API 获取文章详情并传递给页面组件。

动态路由的 fallback 机制

fallback 的作用

在 Next.js 中,fallbackgetStaticPaths 函数中的一个重要选项。它决定了当用户请求的动态路由路径不在 getStaticPaths 生成的 paths 数组中时,Next.js 的行为。

fallback 设置为 false 时,如果请求的路径不在 paths 中,Next.js 将返回 404 页面。而当 fallback 设置为 trueblocking 时,情况会有所不同。

fallback 为 true 的情况

fallbacktrue 时,如果用户请求的路径不在 paths 中,Next.js 会先返回一个占位页面,然后在后台生成该页面的静态版本。一旦生成完成,用户再次请求该页面时,就会看到完整的页面内容。

以下是一个示例:

import React from'react';
import { useRouter } from 'next/router';

const ProductDetails = ({ product }) => {
  const router = useRouter();
  if (router.isFallback) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      <h1>{product.title}</h1>
      <p>{product.description}</p>
    </div>
  );
};

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

  return {
    props: {
      product
    }
  };
}

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

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

  return { paths, fallback: true };
}

export default ProductDetails;

在上述代码中,getStaticPaths 只生成了部分产品的路径。当用户请求一个不在这些路径中的产品页面时,router.isFallbacktrue,页面会显示 “Loading...”,同时 Next.js 在后台生成该页面的静态版本。

fallback 为 blocking 的情况

fallback 设置为 blocking 时,与 fallback: true 类似,但 Next.js 会在生成静态页面完成后再返回给用户,而不是先返回占位页面。这意味着用户会看到一个加载状态,直到页面完全生成。

import React from'react';
import { useRouter } from 'next/router';

const ProductDetails = ({ product }) => {
  return (
    <div>
      <h1>{product.title}</h1>
      <p>{product.description}</p>
    </div>
  );
};

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

  return {
    props: {
      product
    }
  };
}

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

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

  return { paths, fallback: 'blocking' };
}

export default ProductDetails;

在这个示例中,当用户请求不在 paths 中的产品页面时,会一直看到加载状态,直到页面静态版本生成完成并返回给用户。

动态路由与 SEO

动态路由对 SEO 的影响

搜索引擎优化(SEO)对于网站的可见性至关重要。在动态路由的情况下,搜索引擎爬虫可能无法正确理解和索引页面内容。例如,如果页面是根据动态参数生成的,而爬虫访问的是一个不存在于预先生成路径中的动态 URL,可能会导致该页面无法被索引。

优化动态路由的 SEO

为了优化动态路由的 SEO,我们可以采取以下措施:

预渲染静态页面

通过 getStaticPropsgetStaticPaths 预先生成所有可能的动态路由页面,确保搜索引擎爬虫能够访问到完整的页面内容。例如,在电商平台中,生成所有产品详情页的静态版本,这样爬虫可以直接抓取这些静态页面。

使用规范标签(Canonical Tags)

在动态路由页面中,我们可以使用规范标签来指定页面的规范版本。这有助于搜索引擎理解不同动态 URL 实际上指向的是同一个内容。例如:

<head>
  <link rel="canonical" href="https://example.com/products/123" />
</head>

在 Next.js 中,我们可以在 next/head 组件中设置规范标签:

import React from'react';
import Head from 'next/head';
import { useRouter } from 'next/router';

const ProductDetails = () => {
  const router = useRouter();
  const { id } = router.query;

  return (
    <div>
      <Head>
        <link rel="canonical" href={`https://example.com/products/${id}`} />
      </Head>
      <h1>Product Details: {id}</h1>
    </div>
  );
};

export default ProductDetails;

提供 XML 网站地图

XML 网站地图是一个列出网站所有页面的文件,它可以帮助搜索引擎爬虫更有效地抓取网站内容。我们可以生成一个包含所有动态路由页面的 XML 网站地图,并将其提交给搜索引擎。在 Next.js 中,可以使用第三方库如 next-sitemap 来生成网站地图。

动态路由的性能优化

代码分割与懒加载

Next.js 自动进行代码分割,这意味着只有在需要时才会加载相应的代码。对于动态路由页面,这一点尤为重要。例如,当用户访问产品详情页时,只有 pages/products/[id].js 相关的代码会被加载,而不是整个应用的代码。

此外,我们还可以手动进行懒加载。比如,如果产品详情页中有一些不常用的功能模块,我们可以使用 React.lazy 和 Suspense 来实现懒加载:

import React, { lazy, Suspense } from'react';
import { useRouter } from 'next/router';

const ProductExtraInfo = lazy(() => import('./ProductExtraInfo'));

const ProductDetails = () => {
  const router = useRouter();
  const { id } = router.query;

  return (
    <div>
      <h1>Product Details: {id}</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <ProductExtraInfo />
      </Suspense>
    </div>
  );
};

export default ProductDetails;

在上述代码中,ProductExtraInfo 组件只有在渲染到它时才会被加载,fallback 用于在加载过程中显示加载状态。

缓存策略

合理设置缓存策略可以显著提高动态路由页面的性能。在使用 getStaticProps 时,我们可以通过 revalidate 选项设置数据的重新验证时间。对于不经常变化的数据,适当延长 revalidate 时间可以减少数据获取的次数。

另外,我们还可以在客户端层面利用浏览器缓存。例如,对于一些静态资源(如 CSS、JavaScript 文件),设置合适的缓存头,让浏览器在一定时间内复用这些资源,而不需要每次都重新请求。

优化数据获取

在动态路由页面获取数据时,尽量减少不必要的数据请求。如果多个动态路由页面需要相同的基础数据,可以考虑在更高层次的组件中获取并共享这些数据。例如,在博客项目中,所有文章页面可能都需要获取博客分类信息,我们可以在父组件中获取一次分类信息,然后通过 props 传递给子组件,而不是每个文章页面都单独请求分类信息。

同时,优化 API 请求本身,确保 API 响应速度快。可以采用缓存、数据库优化等手段来提高 API 的性能,从而提升动态路由页面的数据获取速度。