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

Next.js动态路由在数据驱动场景下的实现

2023-04-192.9k 阅读

Next.js 动态路由基础

什么是动态路由

在 Web 开发中,动态路由允许我们根据不同的参数来生成不同的页面。传统的静态路由每个页面都有一个固定的路径,而动态路由使得我们可以复用相同的页面结构来展示不同的数据。例如,在一个博客应用中,每篇文章可能都需要一个独立的页面,使用动态路由就可以通过一个页面组件来展示不同文章的内容,只需要传入不同的文章 ID 等参数。

在 Next.js 中,动态路由是其强大的路由功能的重要组成部分。Next.js 通过文件系统路由约定来实现动态路由,这种方式非常直观且易于理解和使用。

Next.js 动态路由的基本实现

在 Next.js 项目中,我们通过在页面文件名中使用方括号 [] 来定义动态路由参数。例如,如果我们有一个 pages/post/[id].js 文件,其中 [id] 就是动态路由参数。这意味着这个页面可以根据不同的 id 值来展示不同的内容。

下面是一个简单的 [id].js 页面组件示例:

import React from'react';

const Post = ({ id }) => {
  return (
    <div>
      <h1>Post {id}</h1>
    </div>
  );
};

export async function getStaticProps(context) {
  const id = context.params.id;
  return {
    props: {
      id
    },
    revalidate: 60 // 每60秒重新验证(如果使用增量静态再生)
  };
}

export async function getStaticPaths() {
  // 这里我们假设从 API 获取所有文章的 ID
  const res = await fetch('https://example.com/api/posts');
  const posts = await res.json();

  const paths = posts.map(post => ({
    params: { id: post.id.toString() }
  }));

  return { paths, fallback: false };
}

export default Post;

在这个例子中,getStaticPaths 函数用于生成所有可能的路径。在实际应用中,我们通常会从数据库或 API 中获取这些路径信息。getStaticProps 函数则用于在构建时(或根据 revalidate 设定的时间间隔进行增量静态再生时)获取每个路径对应的具体数据,并将数据作为属性传递给组件。

数据驱动场景下的动态路由需求

复杂数据结构与动态路由

在数据驱动的场景中,数据结构往往比较复杂。比如,我们可能有一个电商应用,商品不仅有 ID,还可能有分类、子分类等多层次的结构。假设我们有一个商品详情页面,不仅要根据商品 ID 来展示商品,还需要考虑商品所属的分类路径。例如,路径可能是 /category/electronics/subcategory/laptops/product/123,其中 electronics 是大类,laptops 是子分类,123 是商品 ID。

为了实现这样的动态路由,我们可以在 Next.js 中定义类似 pages/category/[category]/subcategory/[subcategory]/product/[productId].js 的页面结构。然后在 getStaticPathsgetStaticProps 函数中,根据实际的数据来源(如数据库查询)来生成路径和获取数据。

数据实时性与动态路由

在某些数据驱动场景下,数据的实时性非常重要。比如股票交易应用,股票价格需要实时更新。在 Next.js 中,对于这种实时数据需求,我们可以结合 getStaticPropsrevalidate 选项以及 getServerSideProps 来实现。

如果数据更新频率不是特别高,可以使用 revalidate 选项进行增量静态再生。例如,对于一些新闻类应用,文章内容可能每隔几分钟更新一次,我们可以设置 revalidate: 300(每 5 分钟重新验证一次)。

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

  return {
    props: {
      news
    },
    revalidate: 300
  };
}

而对于实时性要求极高的数据,比如股票价格,我们可以使用 getServerSideProps。这个函数会在每次请求页面时在服务器端运行,确保获取到最新的数据。

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

  return {
    props: {
      stock
    }
  };
}

动态路由在数据驱动场景下的实现步骤

定义动态路由结构

首先,我们需要根据数据结构来定义合适的动态路由结构。如前文提到的电商应用示例,我们已经定义了 pages/category/[category]/subcategory/[subcategory]/product/[productId].js 的页面结构。

在定义动态路由结构时,需要充分考虑数据的层次关系和访问需求。如果我们还有品牌相关的信息,可能还需要在路径中添加品牌参数,如 pages/brand/[brand]/category/[category]/subcategory/[subcategory]/product/[productId].js

生成静态路径

  1. 从数据源获取数据:在 getStaticPaths 函数中,我们首先要从数据源(如数据库、API 等)获取生成路径所需的数据。假设我们有一个 API 可以获取所有商品的详细信息,包括分类、子分类和 ID。
export async function getStaticPaths() {
  const res = await fetch('https://example.com/api/products');
  const products = await res.json();

  const paths = [];
  products.forEach(product => {
    const { category, subcategory, id } = product;
    const path = {
      params: {
        category,
        subcategory,
        productId: id.toString()
      }
    };
    paths.push(path);
  });

  return { paths, fallback: false };
}
  1. 处理复杂数据关系:如果数据存在复杂的关系,比如一个分类下可能有多个子分类,每个子分类又有多个商品,我们需要进行多层循环来生成所有可能的路径。
export async function getStaticPaths() {
  const res = await fetch('https://example.com/api/categories');
  const categories = await res.json();

  const paths = [];
  categories.forEach(category => {
    const categoryPath = { params: { category: category.name } };
    paths.push(categoryPath);

    const subcategoryRes = await fetch(`https://example.com/api/categories/${category.id}/subcategories`);
    const subcategories = await subcategoryRes.json();

    subcategories.forEach(subcategory => {
      const subcategoryPath = {
        params: {
          category: category.name,
          subcategory: subcategory.name
        }
      };
      paths.push(subcategoryPath);

      const productRes = await fetch(`https://example.com/api/subcategories/${subcategory.id}/products`);
      const products = await productRes.json();

      products.forEach(product => {
        const productPath = {
          params: {
            category: category.name,
            subcategory: subcategory.name,
            productId: product.id.toString()
          }
        };
        paths.push(productPath);
      });
    });
  });

  return { paths, fallback: false };
}

获取页面数据

  1. 使用 getStaticProps:在获取页面数据时,如果数据相对静态或者可以接受一定时间的缓存,我们使用 getStaticProps。以电商商品详情页为例:
export async function getStaticProps(context) {
  const { category, subcategory, productId } = context.params;
  const productRes = await fetch(`https://example.com/api/products/${productId}`);
  const product = await productRes.json();

  const categoryRes = await fetch(`https://example.com/api/categories/${category}`);
  const categoryData = await categoryRes.json();

  const subcategoryRes = await fetch(`https://example.com/api/subcategories/${subcategory}`);
  const subcategoryData = await subcategoryRes.json();

  return {
    props: {
      product,
      category: categoryData,
      subcategory: subcategoryData
    },
    revalidate: 3600 // 每小时重新验证
  };
}
  1. 使用 getServerSideProps:当数据实时性要求高时,如前文提到的股票价格页面,我们使用 getServerSideProps
export async function getServerSideProps(context) {
  const id = context.params.id;
  const res = await fetch(`https://example.com/api/stock/${id}`);
  const stock = await res.json();

  return {
    props: {
      stock
    }
  };
}

处理动态路由中的参数校验与错误处理

参数校验

  1. 在 getStaticPaths 中校验:在 getStaticPaths 中,我们可以对从数据源获取的数据进行校验,确保生成的路径参数是有效的。例如,在电商应用中,商品 ID 应该是一个有效的数字。
export async function getStaticPaths() {
  const res = await fetch('https://example.com/api/products');
  const products = await res.json();

  const paths = [];
  products.forEach(product => {
    const { id } = product;
    if (typeof id === 'number' && id > 0) {
      const path = {
        params: { productId: id.toString() }
      };
      paths.push(path);
    }
  });

  return { paths, fallback: false };
}
  1. 在页面组件中校验:在页面组件内部,我们也可以对传入的参数进行二次校验。比如在 [id].js 页面组件中:
import React from'react';

const Post = ({ id }) => {
  const isValidId = typeof id ==='string' &&!isNaN(parseInt(id));
  if (!isValidId) {
    return <div>Invalid post ID</div>;
  }

  return (
    <div>
      <h1>Post {id}</h1>
    </div>
  );
};

export async function getStaticProps(context) {
  const id = context.params.id;
  return {
    props: {
      id
    },
    revalidate: 60
  };
}

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

  const paths = posts.map(post => ({
    params: { id: post.id.toString() }
  }));

  return { paths, fallback: false };
}

export default Post;

错误处理

  1. 数据获取错误:在 getStaticPropsgetServerSideProps 中,如果数据获取失败,我们需要进行适当的错误处理。例如,在获取商品数据时:
export async function getStaticProps(context) {
  const { productId } = context.params;
  try {
    const productRes = await fetch(`https://example.com/api/products/${productId}`);
    if (!productRes.ok) {
      throw new Error('Failed to fetch product');
    }
    const product = await productRes.json();

    return {
      props: {
        product
      },
      revalidate: 3600
    };
  } catch (error) {
    return {
      notFound: true
    };
  }
}
  1. 路由错误:如果用户访问了一个不存在的动态路由路径,我们可以通过 fallback 选项在 getStaticPaths 中进行处理。如果 fallback: false,Next.js 会直接返回 404 页面。如果 fallback: true,当用户访问一个不存在的路径时,Next.js 会先显示一个加载状态,然后在后台生成该页面并重新渲染。如果 fallback: 'blocking',Next.js 会在服务器端等待页面生成完成后再返回给用户。
export async function getStaticPaths() {
  const res = await fetch('https://example.com/api/posts');
  const posts = await res.json();

  const paths = posts.map(post => ({
    params: { id: post.id.toString() }
  }));

  return { paths, fallback: true };
}

动态路由与 SEO 的结合

优化动态路由页面的元数据

  1. 设置页面标题与描述:对于动态路由页面,我们需要根据页面展示的数据来设置合适的元数据,以提高搜索引擎优化。在 Next.js 中,我们可以使用 next/head 组件来设置元数据。例如,在博客文章详情页:
import React from'react';
import Head from 'next/head';

const Post = ({ post }) => {
  return (
    <div>
      <Head>
        <title>{post.title}</title>
        <meta name="description" content={post.excerpt} />
      </Head>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </div>
  );
};

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

  return {
    props: {
      post
    },
    revalidate: 3600
  };
}

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

  const paths = posts.map(post => ({
    params: { id: post.id.toString() }
  }));

  return { paths, fallback: false };
}

export default Post;
  1. 结构化数据标记:为了让搜索引擎更好地理解页面内容,我们可以添加结构化数据标记(如 JSON - LD)。以产品详情页为例:
import React from'react';
import Head from 'next/head';

const Product = ({ product }) => {
  const jsonLd = {
    "@context": "https://schema.org",
    "@type": "Product",
    "name": product.name,
    "description": product.description,
    "image": product.imageUrl,
    "brand": {
      "@type": "Brand",
      "name": product.brand
    },
    "offers": {
      "@type": "Offer",
      "price": product.price,
      "priceCurrency": "USD"
    }
  };

  return (
    <div>
      <Head>
        <script type="application/ld+json">{JSON.stringify(jsonLd)}</script>
        <title>{product.name}</title>
        <meta name="description" content={product.description} />
      </Head>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p>Price: {product.price}</p>
    </div>
  );
};

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

  return {
    props: {
      product
    },
    revalidate: 3600
  };
}

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

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

  return { paths, fallback: false };
}

export default Product;

确保动态路由可被搜索引擎抓取

  1. 合理设置 fallback 选项:当使用 fallback 选项时,我们需要确保搜索引擎可以正常抓取页面。如果 fallback: truefallback: 'blocking',我们要保证生成的页面在搜索引擎访问时已经准备好。一般来说,对于大多数数据驱动的动态路由页面,fallback: false 是一个更安全的选择,这样可以避免搜索引擎访问到未完成生成的页面。
  2. 使用 sitemap.xml:生成并提交 sitemap.xml 可以帮助搜索引擎更好地发现和抓取动态路由页面。在 Next.js 项目中,我们可以使用 next-sitemap 等库来生成 sitemap.xml。首先安装 next-sitemap
npm install next-sitemap

然后在 next.config.js 中进行配置:

const withSitemap = require('next-sitemap');

module.exports = withSitemap({
  reactStrictMode: true,
  sitemap: {
    siteUrl: 'https://example.com',
    generateRobotsTxt: true
  }
});

这样就会在项目根目录生成 sitemap.xmlrobots.txt 文件,方便搜索引擎抓取。

性能优化与动态路由

代码分割与动态路由组件

  1. 动态导入组件:在动态路由页面中,如果某些组件只在特定情况下使用,我们可以使用动态导入来实现代码分割,提高性能。例如,在一个商品详情页中,可能有一个评论组件,只有当用户点击查看评论时才需要加载。
import React, { useState } from'react';

const Product = () => {
  const [showComments, setShowComments] = useState(false);
  const CommentsComponent = React.lazy(() => import('./Comments'));

  return (
    <div>
      <h1>Product Details</h1>
      {showComments && (
        <React.Suspense fallback={<div>Loading comments...</div>}>
          <CommentsComponent />
        </React.Suspense>
      )}
      <button onClick={() => setShowComments(!showComments)}>
        {showComments? 'Hide Comments' : 'Show Comments'}
      </button>
    </div>
  );
};

export default Product;
  1. 避免不必要的渲染:在动态路由页面中,要注意避免不必要的组件渲染。可以使用 React.memo 来包裹一些纯展示组件,防止它们在父组件状态变化但自身属性未变化时重新渲染。例如:
const ProductInfo = React.memo(({ product }) => {
  return (
    <div>
      <p>Name: {product.name}</p>
      <p>Price: {product.price}</p>
    </div>
  );
});

缓存策略与动态路由数据

  1. 浏览器缓存:对于动态路由页面的数据,如果数据变化不频繁,可以利用浏览器缓存来提高性能。在 getStaticProps 中,我们可以设置合适的缓存头。例如:
export async function getStaticProps(context) {
  const { productId } = context.params;
  const res = await fetch(`https://example.com/api/products/${productId}`);
  const product = await res.json();

  return {
    props: {
      product
    },
    revalidate: 3600,
    headers: {
      'Cache - Control':'s - maxage=3600, stale - while - revalidate'
    }
  };
}
  1. 服务器端缓存:在服务器端,我们也可以使用缓存机制来减少对数据源的重复请求。例如,使用 Redis 等缓存数据库。在 getStaticPropsgetServerSideProps 中,可以先检查缓存中是否有数据,如果有则直接返回,否则从数据源获取并缓存。
import redis from'redis';

const client = redis.createClient();

export async function getServerSideProps(context) {
  const { productId } = context.params;
  return new Promise((resolve, reject) => {
    client.get(`product:${productId}`, (err, reply) => {
      if (reply) {
        resolve({
          props: {
            product: JSON.parse(reply)
          }
        });
      } else {
        fetch(`https://example.com/api/products/${productId}`)
         .then(res => res.json())
         .then(product => {
            client.setex(`product:${productId}`, 3600, JSON.stringify(product));
            resolve({
              props: {
                product
              }
            });
          })
         .catch(err => reject(err));
      }
    });
  });
}

通过以上这些方面的实现、优化和处理,我们可以在 Next.js 中有效地实现动态路由在数据驱动场景下的应用,为用户提供高效、优质的 Web 体验。无论是复杂的数据结构处理,还是实时性数据需求,亦或是 SEO 和性能优化,都有相应的策略和方法来应对。