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

React 生命周期在服务端渲染中的作用

2024-03-251.8k 阅读

React 生命周期基础回顾

在深入探讨 React 生命周期在服务端渲染(SSR)中的作用之前,我们先来回顾一下 React 生命周期的基础知识。React 组件的生命周期可以分为三个主要阶段:挂载(Mounting)、更新(Updating)和卸载(Unmounting)。

挂载阶段

  1. constructor:这是 ES6 类组件的构造函数,在组件被创建时调用。通常用于初始化 state 和绑定方法。例如:
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      data: []
    };
    this.fetchData = this.fetchData.bind(this);
  }
  //...其他方法
}
  1. getDerivedStateFromProps:这是一个静态方法,在组件挂载和更新时都会被调用。它接收 props 和 state 作为参数,并返回一个对象来更新 state。如果不需要更新 state 则返回 null。例如:
class MyComponent extends React.Component {
  static getDerivedStateFromProps(props, state) {
    if (props.someValue!== state.someValue) {
      return {
        someValue: props.someValue
      };
    }
    return null;
  }
  //...其他方法
}
  1. render:这是组件的核心方法,用于返回 JSX 描述的 UI 结构。它是纯函数,不能直接修改 state 或触发副作用。例如:
class MyComponent extends React.Component {
  render() {
    return <div>{this.state.data.map(item => <p>{item}</p>)}</div>;
  }
}
  1. componentDidMount:在组件插入到 DOM 后立即调用。通常用于需要访问 DOM 元素、发起网络请求或添加事件监听器等副作用操作。例如:
class MyComponent extends React.Component {
  componentDidMount() {
    this.fetchData();
  }
  fetchData() {
    // 模拟网络请求
    setTimeout(() => {
      this.setState({
        data: ['item1', 'item2', 'item3']
      });
    }, 1000);
  }
  //...其他方法
}

更新阶段

  1. shouldComponentUpdate:接收 nextProps 和 nextState 作为参数,返回一个布尔值,用于决定组件是否需要更新。默认情况下,只要 props 或 state 发生变化,组件就会更新。通过合理实现这个方法,可以提高性能。例如:
class MyComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    return this.props.someValue!== nextProps.someValue || this.state.someValue!== nextState.someValue;
  }
  //...其他方法
}
  1. getDerivedStateFromProps:如前文所述,在更新阶段也会被调用,用于根据新的 props 更新 state。
  2. render:同样在更新阶段被调用,返回新的 UI 结构。
  3. getSnapshotBeforeUpdate:在最近一次渲染输出(提交到 DOM 节点)之前调用。它可以用于获取一些数据,例如滚动位置,以便在 componentDidUpdate 中使用。例如:
class MyComponent extends React.Component {
  getSnapshotBeforeUpdate(prevProps, prevState) {
    if (prevState.data.length!== this.state.data.length) {
      return this.refs.scrollContainer.scrollTop;
    }
    return null;
  }
  componentDidUpdate(prevProps, prevState, snapshot) {
    if (snapshot) {
      this.refs.scrollContainer.scrollTop = snapshot;
    }
  }
  //...其他方法
}
  1. componentDidUpdate:在组件更新后调用。可以用于对比更新前后的 props 和 state,进行一些副作用操作,如更新 DOM 之外的元素。例如:
class MyComponent extends React.Component {
  componentDidUpdate(prevProps, prevState) {
    if (prevProps.someValue!== this.props.someValue) {
      // 进行一些操作,如重新请求数据
      this.fetchData();
    }
  }
  //...其他方法
}

卸载阶段

componentWillUnmount:在组件从 DOM 中移除之前调用。通常用于清理副作用,如取消网络请求、移除事件监听器等。例如:

class MyComponent extends React.Component {
  componentWillUnmount() {
    // 取消网络请求
    if (this.request) {
      this.request.abort();
    }
  }
  //...其他方法
}

服务端渲染简介

服务端渲染(SSR)是一种将 React 应用在服务器端渲染成 HTML 字符串,然后将其发送到客户端的技术。传统的 React 应用是在客户端渲染,即浏览器下载 JavaScript 代码,解析并执行后渲染出页面。而 SSR 可以在服务器端生成完整的 HTML 页面,直接返回给客户端,客户端只需要进行简单的 hydration(将静态 HTML 转换为可交互的 React 应用)即可。

SSR 的优势

  1. 首屏加载速度:对于用户来说,更快的首屏加载速度意味着更好的体验。SSR 可以直接返回已渲染好的 HTML,减少了客户端等待 JavaScript 加载和执行的时间,特别是在网络条件较差的情况下效果更为明显。例如,一个电商产品列表页面,如果使用客户端渲染,用户可能需要等待几秒钟才能看到商品列表,而 SSR 可以让用户几乎瞬间看到商品的基本信息。
  2. 搜索引擎优化(SEO):搜索引擎爬虫通常不会执行 JavaScript 代码。通过 SSR,服务器返回的 HTML 包含了完整的页面内容,搜索引擎可以直接抓取和索引,提高网站在搜索结果中的排名。例如,对于一个新闻网站,使用 SSR 可以让新闻内容更容易被搜索引擎收录,从而获得更多的流量。

SSR 的实现方式

在 React 生态中,常用的 SSR 框架有 Next.js 和 Gatsby。Next.js 是一个基于 React 的轻量级框架,它提供了简单的路由系统和 SSR 支持。Gatsby 则侧重于构建高性能的静态网站,它可以在构建时生成静态 HTML 文件。

以 Next.js 为例,创建一个基本的 Next.js 应用非常简单。首先,使用 npx create - next - app my - app 命令创建一个新的应用。然后,在 pages 目录下创建页面文件,如 index.js

import React from'react';

const HomePage = () => {
  return (
    <div>
      <h1>Welcome to my Next.js app</h1>
    </div>
  );
};

export default HomePage;

Next.js 会自动将 pages 目录下的文件映射为路由。运行 npm run dev 即可启动开发服务器,访问 http://localhost:3000 就能看到应用。

React 生命周期在服务端渲染中的作用

挂载阶段生命周期在 SSR 中的作用

  1. constructor:在 SSR 环境下,constructor 同样用于初始化 state 和绑定方法。但是需要注意的是,由于服务端没有 DOM,一些依赖于 DOM 的初始化操作不能在这里进行。例如,不能在 constructor 中直接操作 document 对象。
class MySSRComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      serverData: null
    };
    this.fetchServerData = this.fetchServerData.bind(this);
  }
  //...其他方法
}
  1. getDerivedStateFromProps:这个方法在 SSR 时同样会被调用,用于根据 props 更新 state。在 SSR 场景下,它可以确保组件在服务器端和客户端渲染时保持一致的 state 初始化。例如,组件接收一个从服务器传递过来的初始数据作为 prop,getDerivedStateFromProps 可以将这个 prop 转化为组件的 state。
class MySSRComponent extends React.Component {
  static getDerivedStateFromProps(props, state) {
    if (props.initialData) {
      return {
        serverData: props.initialData
      };
    }
    return null;
  }
  //...其他方法
}
  1. render:在 SSR 中,render 方法的作用与客户端渲染基本相同,都是返回 JSX 描述的 UI 结构。但是需要注意的是,在服务器端渲染时,所有的渲染逻辑都必须是纯函数,不能依赖于浏览器特定的 API。例如,不能在 render 中使用 window 对象。
class MySSRComponent extends React.Component {
  render() {
    return (
      <div>
        {this.state.serverData && <p>{this.state.serverData.message}</p>}
      </div>
    );
  }
}
  1. componentDidMount:在 SSR 中,componentDidMount 不会在服务器端执行,因为服务器端没有 DOM 可供挂载。这个方法主要用于客户端,在组件挂载到 DOM 后执行一些副作用操作,如发起网络请求或添加事件监听器。例如,在客户端需要根据页面元素的位置进行一些动画效果,就可以在 componentDidMount 中实现。
class MySSRComponent extends React.Component {
  componentDidMount() {
    // 客户端网络请求
    fetch('/api/data')
    .then(response => response.json())
    .then(data => {
        this.setState({
          clientData: data
        });
      });
  }
  //...其他方法
}

更新阶段生命周期在 SSR 中的作用

  1. shouldComponentUpdate:在 SSR 环境下,shouldComponentUpdate 的作用与客户端渲染类似,用于决定组件是否需要更新。合理实现这个方法可以减少不必要的渲染,提高 SSR 的性能。例如,如果组件接收的 props 在服务器端渲染和客户端渲染时没有变化,就可以通过 shouldComponentUpdate 阻止不必要的更新。
class MySSRComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    return this.props.someValue!== nextProps.someValue || this.state.someValue!== nextState.someValue;
  }
  //...其他方法
}
  1. getDerivedStateFromProps:在更新阶段,getDerivedStateFromProps 在 SSR 中继续发挥作用,确保 state 根据新的 props 进行正确更新。例如,当组件接收到新的 prop 数据时,这个方法可以将新数据合并到 state 中。
class MySSRComponent extends React.Component {
  static getDerivedStateFromProps(props, state) {
    if (props.newData) {
      return {
        combinedData: [...state.combinedData, props.newData]
      };
    }
    return null;
  }
  //...其他方法
}
  1. render:在更新阶段,render 方法在 SSR 中同样返回新的 UI 结构。需要注意的是,在服务器端渲染时,渲染逻辑依然要保持纯净,不依赖浏览器特定的功能。
class MySSRComponent extends React.Component {
  render() {
    return (
      <div>
        {this.state.combinedData.map(data => <p>{data}</p>)}
      </div>
    );
  }
}
  1. getSnapshotBeforeUpdate:在 SSR 中,getSnapshotBeforeUpdate 通常不会像在客户端那样用于获取 DOM 相关的信息,因为服务器端没有 DOM。但是,它可以用于在更新前保存一些组件内部状态,以便在 componentDidUpdate 中使用。例如,保存组件的某个计数器的值。
class MySSRComponent extends React.Component {
  getSnapshotBeforeUpdate(prevProps, prevState) {
    if (prevState.counter!== this.state.counter) {
      return prevState.counter;
    }
    return null;
  }
  componentDidUpdate(prevProps, prevState, snapshot) {
    if (snapshot) {
      // 根据之前的计数器值进行一些操作
    }
  }
  //...其他方法
}
  1. componentDidUpdate:在 SSR 中,componentDidUpdate 同样在组件更新后被调用。可以用于在客户端进行一些与更新相关的副作用操作,如更新第三方库的状态。例如,更新 Chart.js 图表的数据。
class MySSRComponent extends React.Component {
  componentDidUpdate(prevProps, prevState) {
    if (prevProps.chartData!== this.props.chartData) {
      // 更新 Chart.js 图表
      const chart = this.refs.chart;
      chart.data.datasets[0].data = this.props.chartData;
      chart.update();
    }
  }
  //...其他方法
}

卸载阶段生命周期在 SSR 中的作用

componentWillUnmount:在 SSR 中,componentWillUnmount 主要在客户端执行。因为服务器端在渲染完成后不会保留组件实例。在客户端,这个方法用于清理副作用,如取消网络请求、移除事件监听器等。例如,在组件卸载时取消正在进行的 WebSocket 连接。

class MySSRComponent extends React.Component {
  componentWillUnmount() {
    if (this.websocket) {
      this.websocket.close();
    }
  }
  //...其他方法
}

React 生命周期在 SSR 中面临的挑战与解决方案

生命周期钩子函数的执行差异

  1. 挑战:如前文所述,componentDidMountcomponentWillUnmount 等钩子函数在服务器端不会执行,这可能导致一些依赖这些钩子函数的逻辑在 SSR 中无法正常工作。例如,如果在 componentDidMount 中发起网络请求获取数据并更新 state,在服务器端就无法获取到这些数据,导致服务器端渲染的页面数据不完整。
  2. 解决方案:可以在服务器端使用其他方式来获取数据。例如,在 Next.js 中,可以使用 getServerSideProps 方法在服务器端获取数据,并将数据作为 prop 传递给组件。
export async function getServerSideProps(context) {
  const response = await fetch('/api/data');
  const data = await response.json();
  return {
    props: {
      serverData: data
    }
  };
}

const MyComponent = ({ serverData }) => {
  return (
    <div>
      {serverData && <p>{serverData.message}</p>}
    </div>
  );
};

export default MyComponent;

保持服务器端和客户端状态一致

  1. 挑战:由于 React 生命周期在服务器端和客户端的执行有所不同,可能会导致服务器端和客户端渲染的状态不一致。例如,在服务器端通过 getDerivedStateFromProps 根据 prop 设置了 state,而在客户端由于某些原因(如 prop 传递错误或钩子函数执行顺序问题)没有正确设置 state,就会出现页面显示不一致的情况。
  2. 解决方案:确保在服务器端和客户端使用相同的初始化逻辑。可以将初始化逻辑封装成一个独立的函数,在 getDerivedStateFromProps 和其他相关钩子函数中调用。同时,在传递 prop 时要确保数据的一致性。例如:
function initializeState(props) {
  if (props.initialData) {
    return {
      serverData: props.initialData
    };
  }
  return null;
}

class MyComponent extends React.Component {
  static getDerivedStateFromProps(props, state) {
    return initializeState(props);
  }
  //...其他方法
}

处理副作用操作

  1. 挑战:在 SSR 中,处理副作用操作(如网络请求、事件监听等)需要特别小心。因为在服务器端执行副作用操作可能会带来性能问题或安全风险,而在客户端执行又可能导致数据不一致。例如,在服务器端发起大量网络请求获取数据可能会影响服务器性能,而在客户端获取数据可能导致首屏加载时数据缺失。
  2. 解决方案:对于网络请求,可以在服务器端使用 getServerSidePropsgetStaticProps(适用于静态页面)在渲染前获取数据。对于事件监听等操作,可以在 componentDidMount 中进行,确保只在客户端执行。例如,在 Next.js 中:
export async function getServerSideProps(context) {
  const response = await fetch('/api/data');
  const data = await response.json();
  return {
    props: {
      serverData: data
    }
  };
}

const MyComponent = ({ serverData }) => {
  React.useEffect(() => {
    // 客户端事件监听
    window.addEventListener('scroll', () => {
      console.log('scrolling');
    });
    return () => {
      window.removeEventListener('scroll', () => {
        console.log('scroll event removed');
      });
    };
  }, []);

  return (
    <div>
      {serverData && <p>{serverData.message}</p>}
    </div>
  );
};

export default MyComponent;

实际案例分析

新闻列表应用

  1. 需求分析:开发一个新闻列表应用,需要在服务器端渲染新闻列表,提高首屏加载速度和 SEO 效果。新闻数据从 API 获取,并且在客户端可以进行一些交互操作,如点赞、评论等。
  2. 实现过程
    • 服务器端渲染:使用 Next.js 框架,在 pages/news.js 文件中编写新闻列表组件。通过 getServerSideProps 获取新闻数据,并将其作为 prop 传递给组件。
export async function getServerSideProps(context) {
  const response = await fetch('/api/news');
  const newsData = await response.json();
  return {
    props: {
      newsList: newsData
    }
  };
}

const NewsList = ({ newsList }) => {
  return (
    <div>
      {newsList.map(news => (
        <div key={news.id}>
          <h2>{news.title}</h2>
          <p>{news.summary}</p>
        </div>
      ))}
    </div>
  );
};

export default NewsList;
  • 客户端交互:在 NewsList 组件中,使用 useEffect 钩子函数在客户端添加点赞和评论的事件监听器。
const NewsList = ({ newsList }) => {
  React.useEffect(() => {
    const likeButtons = document.querySelectorAll('.like - button');
    likeButtons.forEach(button => {
      button.addEventListener('click', () => {
        // 处理点赞逻辑
        console.log('liked');
      });
    });
    return () => {
      likeButtons.forEach(button => {
        button.removeEventListener('click', () => {
          console.log('like event removed');
        });
      });
    };
  }, []);

  return (
    <div>
      {newsList.map(news => (
        <div key={news.id}>
          <h2>{news.title}</h2>
          <p>{news.summary}</p>
          <button className="like - button">Like</button>
        </div>
      ))}
    </div>
  );
};
  1. 生命周期的作用:在这个案例中,getServerSideProps 相当于在服务器端的“数据获取阶段”,类似于在客户端 componentDidMount 中获取数据,但在服务器端执行。在客户端,useEffect 模拟了 componentDidMountcomponentWillUnmount 的部分功能,用于添加和移除事件监听器,处理客户端的副作用操作。

电商产品详情页

  1. 需求分析:构建一个电商产品详情页,需要在服务器端渲染产品的基本信息,如名称、价格、描述等。同时,在客户端加载产品的图片画廊、用户评论等交互功能。
  2. 实现过程
    • 服务器端渲染:使用 Next.js,在 pages/product/[id].js 文件中编写产品详情组件。通过 getServerSideProps 根据产品 ID 获取产品基本信息,并传递给组件。
export async function getServerSideProps(context) {
  const productId = context.query.id;
  const response = await fetch(`/api/products/${productId}`);
  const productData = await response.json();
  return {
    props: {
      product: productData
    }
  };
}

const ProductDetail = ({ product }) => {
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.price}</p>
      <p>{product.description}</p>
    </div>
  );
};

export default ProductDetail;
  • 客户端交互:在 ProductDetail 组件中,使用 useEffect 加载图片画廊和用户评论相关的功能。例如,使用第三方图片画廊库,在 componentDidMount 等效的 useEffect 中初始化画廊。
import React from'react';
import Gallery from 'react - gallery - component';

const ProductDetail = ({ product }) => {
  React.useEffect(() => {
    const galleryImages = product.images.map(image => ({
      src: image.src,
      thumbnail: image.thumbnail
    }));
    new Gallery({
      images: galleryImages,
      container: document.getElementById('gallery - container')
    });
    return () => {
      // 清理画廊相关资源(如果有)
    };
  }, []);

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.price}</p>
      <p>{product.description}</p>
      <div id="gallery - container"></div>
    </div>
  );
};

export default ProductDetail;
  1. 生命周期的作用:在服务器端,getServerSideProps 确保产品基本信息在渲染前获取并传递给组件。在客户端,useEffect 模拟了 componentDidMountcomponentWillUnmount 的功能,用于初始化图片画廊和清理相关资源,实现客户端的交互功能。

总结 React 生命周期与 SSR 的关系

React 生命周期在服务端渲染中扮演着重要的角色。虽然部分生命周期钩子函数在服务器端和客户端的执行有所不同,但通过合理利用和调整,可以实现高效的 SSR 应用。在服务器端,利用 getServerSideProps 等方法获取数据,结合 getDerivedStateFromProps 等钩子函数确保 state 的正确初始化。在客户端,useEffect 等钩子函数模拟传统生命周期钩子函数的功能,处理副作用操作和交互逻辑。通过深入理解 React 生命周期在 SSR 中的作用,开发者可以构建出性能更优、用户体验更好的应用程序。同时,在实际开发中要注意解决服务器端和客户端状态一致性、副作用处理等问题,以充分发挥 SSR 的优势。