MST
星途 面试题库

面试题:Next.js 服务器端渲染(SSR)场景下,页面路由与 React 组件生命周期在数据预取和 hydration 过程中的深度交互机制是怎样的?

在 Next.js 的 SSR 项目中,分析从页面路由匹配开始,到 React 组件在客户端完成 hydration 这一过程中,组件生命周期(`getServerSideProps`、`getStaticProps`、`componentDidMount` 等)如何与页面路由协同工作来完成数据预取、传递以及客户端状态恢复。并举例说明可能出现的问题以及如何解决,例如客户端与服务器端数据不一致的情况。
48.8万 热度难度
前端开发Next.js

知识考点

AI 面试

面试题答案

一键面试

1. 页面路由匹配到 React 组件在客户端完成 hydration 过程中组件生命周期与页面路由的协同工作

数据预取

  • getStaticProps:在构建时运行,用于预取不会经常变化的数据。例如一个博客文章列表页,文章数据相对稳定。当页面路由匹配时,Next.js 首先会在构建阶段调用 getStaticProps 获取数据,并将数据作为 props 传递给 React 组件。这使得组件在服务器端渲染时就已经拥有数据,提升首次渲染性能。
export async function getStaticProps() {
  const res = await fetch('https://example.com/api/blogs');
  const blogs = await res.json();
  return {
    props: {
      blogs
    },
    revalidate: 60 // 每60秒重新验证(适用于 Incremental Static Regeneration)
  };
}

const BlogList = ({ blogs }) => {
  return (
    <div>
      {blogs.map(blog => (
        <div key={blog.id}>{blog.title}</div>
      ))}
    </div>
  );
};

export default BlogList;
  • getServerSideProps:在每次请求时运行,适用于获取动态数据,如用户特定的数据。当页面路由匹配,Next.js 在服务器端接收到请求时调用 getServerSideProps,获取数据并将其作为 props 传递给组件。例如用户个人资料页,不同用户看到不同的数据。
export async function getServerSideProps(context) {
  const { req } = context;
  const user = req.user; // 假设通过中间件获取到用户信息
  const res = await fetch(`https://example.com/api/user/${user.id}`);
  const userData = await res.json();
  return {
    props: {
      userData
    }
  };
}

const UserProfile = ({ userData }) => {
  return (
    <div>
      <h1>{userData.name}</h1>
      <p>{userData.bio}</p>
    </div>
  );
};

export default UserProfile;

数据传递

  • 无论是 getStaticProps 还是 getServerSideProps 获取的数据,都会作为 props 传递给对应的 React 组件。组件在服务器端渲染时使用这些数据生成 HTML。例如上面的 BlogListUserProfile 组件在接收到 props 后渲染出相应的页面结构。

客户端状态恢复

  • componentDidMount:在客户端 hydration 完成后调用。在此阶段,可以执行一些客户端特定的操作,比如初始化第三方库、添加事件监听器等。虽然数据已经通过服务器端预取并传递到组件,但如果组件需要额外的客户端状态管理,componentDidMount 可以用来初始化这些状态。例如一个图片轮播组件,在客户端需要初始化轮播功能。
import React, { Component } from'react';

class Carousel extends Component {
  componentDidMount() {
    // 初始化图片轮播库
    new CarouselLibrary(this.carouselRef.current);
  }
  render() {
    return (
      <div ref={this.carouselRef} className="carousel">
        {this.props.images.map(image => (
          <img key={image.id} src={image.url} alt={image.alt} />
        ))}
      </div>
    );
  }
}

export default Carousel;

2. 可能出现的问题及解决方法

客户端与服务器端数据不一致

  • 问题原因:当使用 getStaticProps 且设置了 revalidate 进行增量静态再生时,在两次重新验证之间,服务器端数据可能已经更新,但客户端由于缓存原因还在使用旧数据。另外,在使用 getServerSideProps 时,如果在服务器端获取数据的逻辑与客户端后续更新数据的逻辑不一致,也可能导致数据不一致。
  • 解决方法
    • 使用 SWR(Stale-While-Revalidate):在客户端引入 SWR 库。它会首先从缓存中读取数据(即使数据可能过期),然后在后台重新验证数据。例如:
import useSWR from'swr';

const fetcher = async (url) => {
  const res = await fetch(url);
  return res.json();
};

const BlogList = () => {
  const { data, error } = useSWR('/api/blogs', fetcher);
  if (error) return <div>Error loading blogs</div>;
  if (!data) return <div>Loading...</div>;
  return (
    <div>
      {data.map(blog => (
        <div key={blog.id}>{blog.title}</div>
      ))}
    </div>
  );
};

export default BlogList;
- **版本控制与数据对比**:在服务器端返回数据时,添加版本号或者数据哈希。在客户端进行 hydration 时,对比版本号或哈希。如果不一致,触发重新获取数据逻辑。例如:
// 服务器端
export async function getStaticProps() {
  const res = await fetch('https://example.com/api/blogs');
  const blogs = await res.json();
  const dataHash = calculateHash(blogs); // 假设的计算哈希函数
  return {
    props: {
      blogs,
      dataHash
    },
    revalidate: 60
  };
}

// 客户端
import React, { useEffect } from'react';

const BlogList = ({ blogs, dataHash }) => {
  const [localDataHash, setLocalDataHash] = useState('');
  useEffect(() => {
    const newHash = calculateHash(blogs);
    setLocalDataHash(newHash);
    if (newHash!== dataHash) {
      // 重新获取数据逻辑
    }
  }, [blogs]);
  return (
    <div>
      {blogs.map(blog => (
        <div key={blog.id}>{blog.title}</div>
      ))}
    </div>
  );
};

export default BlogList;