Nextjs服务端渲染(SSR)与客户端 Hydration 的数据组装与更新机制

723 阅读3分钟

服务端渲染(SSR)与客户端 Hydration 的数据组装与更新机制

本文档详细介绍了 服务端渲染(SSR) 的工作流程,以及如何在 Next.js 中实现服务端数据的注入和客户端的 Hydration。同时,我们还会探讨如何确保 self.__next_f 的更新及时,以及在 page.js 加载在前的情况下,如何正确处理数据。

1. 服务端渲染(SSR)的工作流程

(1) 服务端获取数据

在服务端渲染时,Next.js 或其他 SSR 框架会先获取页面所需的数据(如从 API 或数据库获取)。

数据获取通常发生在页面的 getServerSideProps 或 getStaticProps 方法中(以 Next.js 为例)。

示例:


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

(2) 将数据注入 HTML

获取数据后,服务端会将数据注入到 HTML 中,通常通过以下方式:

  • script 标签:将数据序列化为 JSON,嵌入到 HTML 的 script 标签中
  • 全局变量:将数据赋值给一个全局变量(如 self.__next_f),供客户端 JavaScript 使用

(3) 发送 HTML 到客户端

服务端将包含数据的 HTML 发送给客户端。

(4) 客户端 Hydration

客户端加载 React 和页面 JavaScript 后,会从 HTML 中提取服务端注入的数据,并使用 ReactDOM.hydrate 方法将静态 HTML “激活”为可交互的页面。

2. 实现方式

(1) 服务端注入数据

在服务端渲染时,将数据序列化为 JSON,并嵌入到 HTML 的 script 标签中。

示例

import React from 'react';
import ReactDOMServer from 'react-dom/server';

function App({ data }) {
  return <div>{data.message}</div>;
}

export async function getServerSideProps() {
  // 模拟获取数据
  const data = { message: 'Hello, world!' };
  return { props: { data } };
}

export function renderToStringWithData(App, props) {
  const html = ReactDOMServer.renderToString(<App {...props} />);
  // 将数据注入到 self.__next_f 中
  const dataScript = `<script>self.__next_f = ${JSON.stringify(props)};</script>`;
  return `
    <!DOCTYPE html>
    <html>
      <head><title>SSR Example</title></head>
      <body>
        <div id="root">${html}</div>
        ${dataScript}
        <script src="/client.js"></script>
      </body>
    </html>
  `;
}

(2) 客户端 Hydration

客户端加载 JavaScript 后,从 self.__next_f 中提取数据,并使用 ReactDOM.hydrate 方法激活页面。

示例

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

// 从 self.__next_f 中提取服务端注入的数据
const data = self.__next_f; 
ReactDOM.hydrate(<App {...data} />, document.getElementById('root'));

(3) 确保 self.__next_f 更新及时

在 Next.js 中,self.__next_f 是用于存储服务端数据的全局变量。为了确保 self.__next_f 更新及时,我们可以通过以下方式实现:

(1) 使用 Proxy 监听 self.__next_f 的变化 通过 Proxy,我们可以监听 self.__next_f 的变化,并在数据更新时触发 Hydration。

示例

// 初始化 self.__next_f
self.__next_f = self.__next_f || [];

// 创建 Proxy 监听 self.__next_f 的变化
self.__next_f = new Proxy(self.__next_f, {
  set(target, prop, value) {
    // 调用默认的赋值操作
    const result = Reflect.set(target, prop, value);

    // 如果数据被挂载,触发 Hydration
    if (prop === 'length' && value > 0) {
      console.log('Data has been mounted:', target);
      hydrateApp(target);
    }

    return result;
  }
});

// Hydration 逻辑
function hydrateApp(data) {
  const parsedData = JSON.parse(data[0][1]);
  console.log('Hydrating with data:', parsedData);
  ReactDOM.hydrate(<App data={parsedData} />, document.getElementById('root'));
}

(2) 在 page.js 中检查数据

在 page.js 中,我们可以检查 self.__next_f 是否包含数据。如果数据尚未嵌入,page.js 会等待数据可用后再执行相关逻辑。

示例

function waitForData() {
  return new Promise((resolve) => {
    if (self.__next_f && self.__next_f.length > 0) {
      resolve();
    } else {
      const observer = new MutationObserver(() => {
        if (self.__next_f && self.__next_f.length > 0) {
          observer.disconnect();
          resolve();
        }
      });
      observer.observe(document.body, { childList: true, subtree: true });
    }
  });
}

waitForData().then(() => {
  const data = JSON.parse(self.__next_f[0][1]);
  console.log('Hydrating with data:', data);
  ReactDOM.hydrate(<App data={data} />, document.getElementById('root'));
});

(4) 处理 page.js 加载在前的情况

如果 page.js 在数据嵌入之前加载完成,我们可以通过以下方式确保数据正确传递:

(1) 初始化 self.__next_f 在 HTML 的头部,初始化 self.__next_f 为一个空数组:

<script>
  self.__next_f = self.__next_f || [];
</script>

运行 HTML (2) 数据嵌入 在 HTML 的后面,嵌入服务端数据:

<script>
  self.__next_f.push([1, "{\"name\":\"The Octocat\"}"]);
</script>

(3) page.js 检查数据

在 page.js 中,检查 self.__next_f 是否包含数据。如果数据尚未嵌入,page.js 会等待数据可用后再执行相关逻辑。

示例

function waitForData() {
  return new Promise((resolve) => {
    if (self.__next_f && self.__next_f.length > 0) {
      resolve();
    } else {
      const observer = new MutationObserver(() => {
        if (self.__next_f && self.__next_f.length > 0) {
          observer.disconnect();
          resolve();
        }
      });
      observer.observe(document.body, { childList: true, subtree: true });
    }
  });
}

waitForData().then(() => {
  const data = JSON.parse(self.__next_f[0][1]);
  console.log('Hydrating with data:', data);
  ReactDOM.hydrate(<App data={data} />, document.getElementById('root'));
});

5. 总结

通过以上方式,我们可以确保:

服务端数据正确注入到 HTML 中。

客户端能够及时获取并处理数据。

即使 page.js 加载在前,也能通过监听机制确保数据正确传递。

这种机制使得 SSR 和 Hydration 的过程更加可靠和高效。 😊