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

Next.js 中的 getServerSideProps 实战技巧分享

2021-07-096.7k 阅读

Next.js 中 getServerSideProps 基础概念

在 Next.js 应用程序中,getServerSideProps 是一个强大的函数,它允许你在服务器端渲染(SSR)页面时获取数据。与客户端渲染(CSR)不同,SSR 会在服务器上生成 HTML 页面,然后将其发送到客户端,这对于需要实时数据或者搜索引擎优化(SEO)的应用非常重要。

getServerSideProps 函数仅在服务器端运行,不会在客户端执行。这意味着它可以访问一些仅在服务器可用的资源,如环境变量、数据库连接等。它返回的数据会作为 props 传递给页面组件,这样页面就可以在渲染时使用这些数据。

基本语法

export async function getServerSideProps(context) {
  // 在这里获取数据
  const data = await someAsyncDataFetchingFunction();
  return {
    props: {
      data
    }
  };
}

在上述代码中,context 是一个包含有关请求的信息的对象,例如 req(请求对象)和 res(响应对象)。someAsyncDataFetchingFunction 是一个异步函数,用于从 API、数据库或其他数据源获取数据。函数最后返回一个对象,其中 props 是传递给页面组件的数据。

数据获取示例

假设我们有一个博客应用,需要在页面上显示最新的文章列表。我们可以使用 getServerSideProps 从后端 API 获取文章数据。

首先,创建一个简单的 API 模拟获取文章数据。假设这个 API 地址为 https://example.com/api/articles,它返回 JSON 格式的文章列表。

export async function getServerSideProps(context) {
  const res = await fetch('https://example.com/api/articles');
  const articles = await res.json();
  return {
    props: {
      articles
    }
  };
}

function ArticleList({ articles }) {
  return (
    <div>
      <h1>最新文章</h1>
      <ul>
        {articles.map(article => (
          <li key={article.id}>{article.title}</li>
        ))}
      </ul>
    </div>
  );
}

export default ArticleList;

在上述代码中,getServerSideProps 使用 fetch 从 API 获取文章数据,然后将数据作为 props 传递给 ArticleList 组件。ArticleList 组件在渲染时使用这些文章数据显示文章标题列表。

getServerSideProps 的执行时机

getServerSideProps 在每次请求页面时都会执行。这意味着如果你的应用是面向公众的,每个用户请求页面时,getServerSideProps 都会运行,从数据源获取最新的数据。

例如,一个实时股票价格显示页面,每次用户访问页面时,getServerSideProps 会从金融数据 API 获取最新的股票价格,确保用户看到的是最新信息。

export async function getServerSideProps(context) {
  const res = await fetch('https://financial-api.com/stock-prices');
  const stockPrices = await res.json();
  return {
    props: {
      stockPrices
    }
  };
}

function StockPricePage({ stockPrices }) {
  return (
    <div>
      <h1>实时股票价格</h1>
      <ul>
        {stockPrices.map(stock => (
          <li key={stock.symbol}>{stock.symbol}: {stock.price}</li>
        ))}
      </ul>
    </div>
  );
}

export default StockPricePage;

在这个例子中,每次用户访问 StockPricePagegetServerSideProps 都会获取最新的股票价格数据,保证页面显示的价格是实时的。

使用 context 参数

getServerSideProps 函数接收一个 context 参数,这个参数包含了关于请求的重要信息。

req 和 res 对象

在 Node.js 环境下,context.reqhttp.IncomingMessage 实例,context.reshttp.ServerResponse 实例。这允许我们在服务器端渲染时操作请求和响应。

例如,我们可以根据请求头中的 Accept-Language 字段来返回不同语言版本的数据。

export async function getServerSideProps(context) {
  const lang = context.req.headers['accept-language'] || 'en';
  let data;
  if (lang.startsWith('zh')) {
    data = await fetch('https://example.com/api/articles/zh');
  } else {
    data = await fetch('https://example.com/api/articles/en');
  }
  const articles = await data.json();
  return {
    props: {
      articles
    }
  };
}

function ArticleList({ articles }) {
  return (
    <div>
      <h1>最新文章</h1>
      <ul>
        {articles.map(article => (
          <li key={article.id}>{article.title}</li>
        ))}
      </ul>
    </div>
  );
}

export default ArticleList;

在上述代码中,根据请求头中的语言偏好,getServerSideProps 从不同的 API 端点获取相应语言版本的文章数据。

params 参数

当页面使用动态路由时,context.params 会包含动态路由参数。例如,我们有一个文章详情页面,路由为 /articles/[id],我们可以通过 context.params.id 获取文章的 ID,然后根据 ID 获取具体的文章内容。

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

function ArticleDetail({ article }) {
  return (
    <div>
      <h1>{article.title}</h1>
      <p>{article.content}</p>
    </div>
  );
}

export default ArticleDetail;

在这个例子中,getServerSideProps 根据动态路由参数 id 从 API 获取特定文章的详细信息,并传递给 ArticleDetail 组件。

缓存控制

由于 getServerSideProps 在每次请求时都会执行,这可能会对性能产生影响,特别是当数据获取操作比较耗时或者数据源有限制时。我们可以通过一些方法来控制缓存。

设置 HTTP 缓存头

getServerSideProps 中,我们可以通过 context.res 设置 HTTP 缓存头。例如,设置 Cache - Control 头来告诉浏览器和中间代理服务器如何缓存页面。

export async function getServerSideProps(context) {
  const res = await fetch('https://example.com/api/articles');
  const articles = await res.json();
  context.res.setHeader('Cache - Control', 'public, s - maxage = 60, stale - while - revalidate = 30');
  return {
    props: {
      articles
    }
  };
}

function ArticleList({ articles }) {
  return (
    <div>
      <h1>最新文章</h1>
      <ul>
        {articles.map(article => (
          <li key={article.id}>{article.title}</li>
        ))}
      </ul>
    </div>
  );
}

export default ArticleList;

在上述代码中,Cache - Control 头设置为 public, s - maxage = 60, stale - while - revalidate = 30,表示公共缓存,缓存有效期为 60 秒,并且在缓存过期后,客户端可以在 30 秒内继续使用过期的缓存,同时后台重新验证数据。

自定义缓存机制

除了使用 HTTP 缓存头,我们还可以实现自定义的缓存机制。例如,使用内存缓存或者外部缓存服务(如 Redis)。

假设我们使用 Node.js 的 node - cache 库来实现简单的内存缓存。

const NodeCache = require('node - cache');
const myCache = new NodeCache();

export async function getServerSideProps(context) {
  let articles = myCache.get('articles');
  if (!articles) {
    const res = await fetch('https://example.com/api/articles');
    articles = await res.json();
    myCache.set('articles', articles);
  }
  return {
    props: {
      articles
    }
  };
}

function ArticleList({ articles }) {
  return (
    <div>
      <h1>最新文章</h1>
      <ul>
        {articles.map(article => (
          <li key={article.id}>{article.title}</li>
        ))}
      </ul>
    </div>
  );
}

export default ArticleList;

在上述代码中,首先尝试从缓存中获取文章数据,如果缓存中没有数据,则从 API 获取数据并将其存入缓存。这样,在缓存有效期内,后续请求将直接从缓存中获取数据,减少了对 API 的请求次数。

错误处理

getServerSideProps 中进行数据获取时,可能会发生各种错误,如网络错误、API 响应错误等。我们需要妥善处理这些错误,以提供良好的用户体验。

捕获数据获取错误

export async function getServerSideProps(context) {
  try {
    const res = await fetch('https://example.com/api/articles');
    if (!res.ok) {
      throw new Error('Failed to fetch articles');
    }
    const articles = await res.json();
    return {
      props: {
        articles
      }
    };
  } catch (error) {
    return {
      props: {
        error: 'An error occurred while fetching articles'
      }
    };
  }
}

function ArticleList({ articles, error }) {
  if (error) {
    return <div>{error}</div>;
  }
  return (
    <div>
      <h1>最新文章</h1>
      <ul>
        {articles.map(article => (
          <li key={article.id}>{article.title}</li>
        ))}
      </ul>
    </div>
  );
}

export default ArticleList;

在上述代码中,使用 try - catch 块捕获 fetch 操作可能发生的错误。如果发生错误,返回一个包含错误信息的 props,页面组件根据是否存在错误信息来显示相应的提示或数据。

处理服务器端错误

除了数据获取错误,getServerSideProps 本身也可能发生其他服务器端错误,如内存不足、数据库连接失败等。我们可以通过日志记录和适当的错误处理来应对这些情况。

import { Logger } from 'winston';
const logger = Logger.createLogger({
  level: 'error',
  format: Logger.format.json(),
  transports: [
    new Logger.transport.Console()
  ]
});

export async function getServerSideProps(context) {
  try {
    const res = await fetch('https://example.com/api/articles');
    if (!res.ok) {
      throw new Error('Failed to fetch articles');
    }
    const articles = await res.json();
    return {
      props: {
        articles
      }
    };
  } catch (error) {
    logger.error(`Error in getServerSideProps: ${error.message}`);
    return {
      props: {
        error: 'An error occurred while fetching articles'
      }
    };
  }
}

function ArticleList({ articles, error }) {
  if (error) {
    return <div>{error}</div>;
  }
  return (
    <div>
      <h1>最新文章</h1>
      <ul>
        {articles.map(article => (
          <li key={article.id}>{article.title}</li>
        ))}
      </ul>
    </div>
  );
}

export default ArticleList;

在这个例子中,使用 winston 日志库记录 getServerSideProps 中发生的错误,这样在生产环境中可以方便地排查问题。同时,返回错误信息给页面组件,让用户知道发生了错误。

与其他 Next.js 功能结合使用

与 getStaticProps 对比和结合

getStaticProps 也是 Next.js 中用于获取数据的函数,但它在构建时获取数据,适合数据变化不频繁的场景。而 getServerSideProps 在每次请求时获取数据,适合数据实时性要求高的场景。

有时候,我们可以结合使用这两个函数。例如,我们有一个博客文章列表页面,大部分文章内容变化不频繁,但有一些最新发布的文章需要实时显示。

export async function getStaticProps() {
  const res = await fetch('https://example.com/api/articles/old');
  const oldArticles = await res.json();
  return {
    props: {
      oldArticles
    },
    revalidate: 60 // 每 60 秒重新验证
  };
}

export async function getServerSideProps(context) {
  const res = await fetch('https://example.com/api/articles/new');
  const newArticles = await res.json();
  return {
    props: {
      newArticles
    }
  };
}

function ArticleList({ oldArticles, newArticles }) {
  const allArticles = [...oldArticles, ...newArticles];
  return (
    <div>
      <h1>文章列表</h1>
      <ul>
        {allArticles.map(article => (
          <li key={article.id}>{article.title}</li>
        ))}
      </ul>
    </div>
  );
}

export default ArticleList;

在上述代码中,getStaticProps 在构建时获取旧文章数据,并设置了 revalidate 参数,每 60 秒重新验证数据。getServerSideProps 在每次请求时获取最新文章数据。页面组件将两者的数据合并显示。

与 API Routes 结合

Next.js 的 API Routes 允许我们在应用内创建 API 端点。我们可以在 getServerSideProps 中调用这些 API Routes,这样可以将数据获取逻辑封装在 API Routes 中,提高代码的可维护性。

假设我们有一个 API Route 文件 pages/api/articles.js 用于获取文章数据。

// pages/api/articles.js
export default async function handler(req, res) {
  const articles = await someDatabaseQuery();
  res.status(200).json(articles);
}

// pages/articles.js
export async function getServerSideProps(context) {
  const res = await fetch('/api/articles');
  const articles = await res.json();
  return {
    props: {
      articles
    }
  };
}

function ArticleList({ articles }) {
  return (
    <div>
      <h1>最新文章</h1>
      <ul>
        {articles.map(article => (
          <li key={article.id}>{article.title}</li>
        ))}
      </ul>
    </div>
  );
}

export default ArticleList;

在这个例子中,getServerSideProps 通过 fetch 调用内部的 API Route 获取文章数据。这样,数据获取的逻辑可以在 API Route 中独立维护,并且可以方便地添加身份验证、日志记录等功能。

性能优化

批量数据获取

当需要从多个数据源获取数据时,可以使用 Promise.all 进行批量获取,以减少总等待时间。

例如,我们需要从一个 API 获取文章数据,从另一个 API 获取作者数据。

export async function getServerSideProps(context) {
  const articlePromise = fetch('https://example.com/api/articles');
  const authorPromise = fetch('https://example.com/api/authors');

  const [articleRes, authorRes] = await Promise.all([articlePromise, authorPromise]);

  const articles = await articleRes.json();
  const authors = await authorRes.json();

  return {
    props: {
      articles,
      authors
    }
  };
}

function ArticleList({ articles, authors }) {
  return (
    <div>
      <h1>最新文章</h1>
      <ul>
        {articles.map(article => (
          <li key={article.id}>
            {article.title} - {authors.find(author => author.id === article.authorId).name}
          </li>
        ))}
      </ul>
    </div>
  );
}

export default ArticleList;

在上述代码中,Promise.all 同时发起两个 fetch 请求,等两个请求都完成后再继续处理数据,这样比顺序执行两个请求节省了时间。

优化数据源访问

如果数据来自数据库,优化数据库查询是提高性能的关键。例如,使用索引来加速查询,避免全表扫描。

假设我们使用 MongoDB 数据库,并且有一个 articles 集合。我们可以为经常查询的字段(如 publishedAt)创建索引。

// 在数据库初始化时创建索引
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/blog');

const articleSchema = new mongoose.Schema({
  title: String,
  content: String,
  publishedAt: Date
});

articleSchema.index({ publishedAt: 1 });

const Article = mongoose.model('Article', articleSchema);

// 在 getServerSideProps 中查询数据
export async function getServerSideProps(context) {
  const articles = await Article.find({ publishedAt: { $gte: new Date('2023 - 01 - 01') } });
  return {
    props: {
      articles
    }
  };
}

function ArticleList({ articles }) {
  return (
    <div>
      <h1>2023 年以来的文章</h1>
      <ul>
        {articles.map(article => (
          <li key={article._id}>{article.title}</li>
        ))}
      </ul>
    </div>
  );
}

export default ArticleList;

在上述代码中,为 publishedAt 字段创建索引后,getServerSideProps 中的查询操作会因为索引的使用而变得更快。

部署和生产环境考虑

服务器资源利用

在生产环境中,getServerSideProps 的频繁执行可能会消耗较多的服务器资源。如果使用的是共享主机或者资源有限的服务器,需要密切关注资源使用情况。

可以通过优化数据获取逻辑、合理设置缓存等方式来减少资源消耗。同时,考虑使用自动伸缩功能,根据请求量自动调整服务器资源。

监控和日志

在生产环境中,监控 getServerSideProps 的性能和错误情况非常重要。可以使用一些监控工具,如 New Relic、Datadog 等,来跟踪数据获取的响应时间、错误率等指标。

结合前面提到的日志记录,通过分析日志和监控数据,可以及时发现和解决性能问题和错误。

安全性

getServerSideProps 中进行数据获取时,要注意安全性。例如,对外部 API 的请求要防止注入攻击,对敏感数据要进行适当的加密和保护。

如果 getServerSideProps 需要访问环境变量,要确保环境变量在生产环境中得到妥善的管理和保护,防止泄露。

总结

getServerSideProps 是 Next.js 中实现服务器端渲染数据获取的重要功能。通过合理运用它的各种特性,如利用 context 参数、控制缓存、处理错误等,以及与其他 Next.js 功能结合使用,并进行性能优化和考虑生产环境的各种因素,我们可以开发出高性能、可靠且安全的 Next.js 应用程序。在实际项目中,根据具体的业务需求和数据特点,灵活运用 getServerSideProps 的这些实战技巧,能够显著提升应用的用户体验和整体质量。