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

深入理解 Next.js 的 getServerSideProps 数据获取方法

2021-09-057.3k 阅读

Next.js 中 getServerSideProps 概述

在 Next.js 应用程序开发中,数据获取是一个关键环节。getServerSideProps 是 Next.js 提供的一种强大的数据获取方法,它允许我们在服务器端渲染(SSR)页面时获取数据。这意味着,当用户请求一个页面时,Next.js 会在服务器上运行 getServerSideProps 函数,获取所需的数据,然后将数据与页面组件一起渲染,最后将完全渲染好的 HTML 发送到客户端。

getServerSideProps 主要用于需要从外部数据源(如数据库、API 等)获取数据,并在每次请求时确保数据是最新的场景。与客户端数据获取(如在 useEffect 中使用 fetch)不同,getServerSideProps 在服务器端运行,这有几个重要的优点:

  1. SEO 友好:搜索引擎爬虫通常无法执行 JavaScript,因此客户端渲染的数据可能无法被爬虫获取。而使用 getServerSideProps,数据在服务器端就已经包含在渲染的 HTML 中,有利于搜索引擎索引页面内容。
  2. 实时数据:由于 getServerSideProps 在每次请求时运行,它可以确保用户看到的是最新的数据,适用于数据频繁变化的应用场景,如新闻网站、实时股票行情等。

基本使用方法

  1. 函数定义 getServerSideProps 是一个导出函数,它必须在页面组件文件中定义。该函数接收一个上下文对象 context,并返回一个包含 props 对象的 Promise。

以下是一个简单的示例,假设我们有一个 API 端点 https://example.com/api/posts,返回博客文章列表:

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

  return {
    props: {
      posts
    }
  };
}

const BlogPage = ({ posts }) => {
  return (
    <div>
      <h1>Blog Posts</h1>
      {posts.map(post => (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </div>
      ))}
    </div>
  );
};

export default BlogPage;

在这个例子中,getServerSideProps 函数通过 fetch 获取博客文章数据,然后将数据作为 props 传递给 BlogPage 组件。

  1. 上下文对象 context context 对象包含了关于当前请求的有用信息,比如 params(用于动态路由)、query(URL 查询参数)、req(Node.js 请求对象)和 res(Node.js 响应对象)。

动态路由示例

假设我们有一个动态路由页面 pages/post/[id].js,用于显示特定文章的详细内容。我们可以使用 context.params.id 获取文章的 ID,并从 API 获取相应的数据:

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

  return {
    props: {
      post
    }
  };
}

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

export default PostPage;

查询参数示例

如果我们想根据查询参数来筛选数据,例如 pages/posts?category=tech,我们可以这样做:

export async function getServerSideProps(context) {
  const { query } = context;
  const category = query.category;
  let url = 'https://example.com/api/posts';
  if (category) {
    url += `?category=${category}`;
  }
  const res = await fetch(url);
  const posts = await res.json();

  return {
    props: {
      posts
    }
  };
}

const FilteredBlogPage = ({ posts }) => {
  return (
    <div>
      <h1>Filtered Blog Posts</h1>
      {posts.map(post => (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </div>
      ))}
    </div>
  );
};

export default FilteredBlogPage;

与其他数据获取方法的比较

  1. 客户端数据获取(useEffect + fetch

    • 优点:简单直接,适用于不需要在初始渲染时就获取数据的场景,例如用户触发某个操作后才需要获取数据。同时,客户端数据获取可以利用浏览器缓存,减少不必要的请求。
    • 缺点:在初始渲染时,页面可能会显示空白,直到数据获取完成。对于 SEO 不友好,因为搜索引擎爬虫无法获取客户端渲染的数据。
  2. 静态生成(getStaticProps

    • 优点:在构建时生成 HTML 文件,性能非常好,适用于内容变化不频繁的页面,如博客文章、产品介绍页面等。可以利用增量静态再生(Incremental Static Regeneration)在一定程度上实现数据更新。
    • 缺点:如果数据频繁变化,可能无法及时反映最新数据,因为构建时生成的 HTML 不会实时更新。不适用于需要根据每个请求动态获取数据的场景,如用户特定的数据展示。

getServerSideProps 的执行流程

  1. 请求到达服务器:当用户请求一个使用 getServerSideProps 的页面时,请求首先到达 Next.js 服务器。
  2. 运行 getServerSideProps:Next.js 会在服务器上执行 getServerSideProps 函数。这个函数可以是异步的,并且可以进行各种异步操作,如 API 调用、数据库查询等。
  3. 获取数据并渲染页面getServerSideProps 获取到数据后,将数据作为 props 传递给页面组件。Next.js 会使用这些 props 渲染页面,生成完整的 HTML。
  4. 发送 HTML 到客户端:渲染好的 HTML 被发送到客户端,客户端接收到 HTML 后,Next.js 会在客户端“hydrate”(注水)页面,使页面具有交互性。

性能优化

  1. 缓存策略 虽然 getServerSideProps 每次请求都会运行,但我们可以在服务器端实现自己的缓存策略,以减少不必要的 API 调用或数据库查询。例如,我们可以使用内存缓存(如 node-cache):
import NodeCache from 'node-cache';

const myCache = new NodeCache();

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

  return {
    props: {
      posts
    }
  };
}

const BlogPage = ({ posts }) => {
  return (
    <div>
      <h1>Blog Posts</h1>
      {posts.map(post => (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </div>
      ))}
    </div>
  );
};

export default BlogPage;
  1. 批量数据获取 如果页面需要从多个 API 端点获取数据,可以使用 Promise.all 进行批量请求,以减少总请求时间:
export async function getServerSideProps(context) {
  const postRes = await fetch('https://example.com/api/posts');
  const userRes = await fetch('https://example.com/api/users');

  const [posts, users] = await Promise.all([postRes.json(), userRes.json()]);

  return {
    props: {
      posts,
      users
    }
  };
}

const PageWithMultipleData = ({ posts, users }) => {
  return (
    <div>
      <h1>Posts and Users</h1>
      {posts.map(post => (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
          <p>Author: {users.find(user => user.id === post.authorId).name}</p>
        </div>
      ))}
    </div>
  );
};

export default PageWithMultipleData;

错误处理

  1. API 请求错误getServerSideProps 中进行 API 调用时,可能会遇到各种错误,如网络错误、API 响应状态码非 200 等。我们可以使用 try - catch 块来处理这些错误:
export async function getServerSideProps(context) {
  try {
    const res = await fetch('https://example.com/api/posts');
    if (!res.ok) {
      throw new Error('Failed to fetch posts');
    }
    const posts = await res.json();

    return {
      props: {
        posts
      }
    };
  } catch (error) {
    return {
      props: {
        error: 'An error occurred while fetching posts'
      }
    };
  }
}

const BlogPageWithErrorHandling = ({ posts, error }) => {
  if (error) {
    return <div>{error}</div>;
  }
  return (
    <div>
      <h1>Blog Posts</h1>
      {posts.map(post => (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </div>
      ))}
    </div>
  );
};

export default BlogPageWithErrorHandling;
  1. 其他错误 除了 API 请求错误,getServerSideProps 中还可能出现其他错误,如数据库连接错误等。同样可以使用 try - catch 进行统一的错误处理,确保页面在发生错误时能够给用户提供有意义的反馈。

与中间件结合使用

Next.js 支持使用中间件(Middleware),我们可以将 getServerSideProps 与中间件结合,实现一些通用的功能,如身份验证、日志记录等。

  1. 身份验证示例 假设我们有一个需要用户登录才能访问的页面,我们可以在中间件中检查用户的认证状态,然后在 getServerSideProps 中根据认证结果决定是否获取数据:
// middleware.js
import { NextResponse } from 'next/server';

export function middleware(request) {
  const token = request.cookies.get('token');
  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
  return NextResponse.next();
}

// pages/protected - page.js
export async function getServerSideProps(context) {
  const res = await fetch('https://example.com/api/protected - data');
  const data = await res.json();

  return {
    props: {
      data
    }
  };
}

const ProtectedPage = ({ data }) => {
  return (
    <div>
      <h1>Protected Page</h1>
      <p>{data.message}</p>
    </div>
  );
};

export default ProtectedPage;

在这个例子中,中间件检查用户是否有有效的 token,如果没有则重定向到登录页面。如果用户已认证,getServerSideProps 会获取受保护的数据并渲染页面。

在不同环境下的使用

  1. 开发环境 在开发环境中,getServerSideProps 会在每次热重载时运行,这有助于我们实时看到数据的变化。但需要注意的是,开发环境下的 API 调用可能会因为跨域等问题而失败,我们可以使用工具如 http - proxy - middleware 来解决跨域问题:
// next.config.js
const { createProxyMiddleware } = require('http - proxy - middleware');

module.exports = {
  devServer: (configFunction) => (proxy, allowedHost) => {
    const config = configFunction(proxy, allowedHost);
    config.proxy = {
     ...config.proxy,
      '/api': createProxyMiddleware({
        target: 'https://example.com',
        changeOrigin: true
      })
    };
    return config;
  }
};
  1. 生产环境 在生产环境中,getServerSideProps 的性能优化更为重要。我们可以结合服务器端缓存、负载均衡等技术来提高应用程序的性能和可靠性。同时,需要注意监控 getServerSideProps 中的 API 调用,确保它们的稳定性和响应时间。

高级应用场景

  1. 多语言支持 我们可以利用 getServerSideProps 根据用户的语言偏好获取相应语言的数据。假设我们有一个 API 支持多语言数据获取,如 https://example.com/api/posts?lang=en
export async function getServerSideProps(context) {
  const { locale } = context;
  const res = await fetch(`https://example.com/api/posts?lang=${locale}`);
  const posts = await res.json();

  return {
    props: {
      posts
    }
  };
}

const MultilingualBlogPage = ({ posts }) => {
  return (
    <div>
      <h1>Multilingual Blog Posts</h1>
      {posts.map(post => (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </div>
      ))}
    </div>
  );
};

export default MultilingualBlogPage;
  1. A/B 测试 通过 getServerSideProps,我们可以根据用户的某些特征(如用户 ID、地理位置等)决定返回不同版本的数据,实现 A/B 测试:
export async function getServerSideProps(context) {
  const { query } = context;
  const userId = query.userId;
  let variant = 'A';
  if (userId) {
    // 根据用户 ID 决定变体
    const user = await fetch(`https://example.com/api/users/${userId}`);
    const userData = await user.json();
    if (userData.group === 'test - group - B') {
      variant = 'B';
    }
  }
  const res = await fetch(`https://example.com/api/content?variant=${variant}`);
  const content = await res.json();

  return {
    props: {
      content
    }
  };
}

const ABTestPage = ({ content }) => {
  return (
    <div>
      <h1>AB Test Page</h1>
      <p>{content.message}</p>
    </div>
  );
};

export default ABTestPage;

通过深入理解 getServerSideProps 的各种特性和使用方法,我们可以更好地利用 Next.js 进行高效、强大的前端应用开发,满足不同场景下的数据获取需求。无论是构建 SEO 友好的网站,还是实现实时数据更新的应用,getServerSideProps 都为我们提供了有力的工具。在实际项目中,我们需要根据具体的业务需求和性能要求,合理选择数据获取方法,并进行优化,以提供最佳的用户体验。