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

React Context 在服务端渲染中的应用

2021-06-125.4k 阅读

1. 理解 React Context

在深入探讨 React Context 在服务端渲染(SSR)中的应用之前,我们首先要对 React Context 有一个清晰的理解。

React 中的 Context 提供了一种在组件之间共享数据的方式,而无需在组件树中通过层层传递 props。它对于那些需要在多个组件层级之间共享的数据(如当前认证用户、主题模式或语言偏好等)非常有用。

1.1 创建 Context

在 React 中,我们使用 createContext 函数来创建一个 Context 对象。这个函数接受一个默认值作为参数,该默认值会在没有匹配的 Provider 时被使用。例如:

import React from'react';

// 创建一个名为 ThemeContext 的 Context
const ThemeContext = React.createContext('light');

export default ThemeContext;

这里,我们创建了一个 ThemeContext,并为其设置了默认值 light

1.2 使用 Context.Provider

Context.Provider 是一个 React 组件,它接收一个 value 属性,并将这个值提供给其后代组件。任何嵌套在 Context.Provider 中的组件都可以消费这个 Context。

import React from'react';
import ThemeContext from './ThemeContext';

const App = () => {
    const theme = 'dark';
    return (
        <ThemeContext.Provider value={theme}>
            {/* 应用的其余部分 */}
        </ThemeContext.Provider>
    );
};

export default App;

在上述代码中,App 组件通过 ThemeContext.Providerdark 主题值提供给其所有后代组件。

1.3 消费 Context

有几种方式可以让组件消费 Context。最常用的方式是使用 Context.Consumer 组件或 useContext Hook(在函数式组件中)。

使用 Context.Consumer

import React from'react';
import ThemeContext from './ThemeContext';

const Button = () => {
    return (
        <ThemeContext.Consumer>
            {theme => (
                <button style={{ background: theme === 'dark'? 'black' : 'white', color: theme === 'dark'? 'white' : 'black' }}>
                    Click me
                </button>
            )}
        </ThemeContext.Consumer>
    );
};

export default Button;

Button 组件中,ThemeContext.Consumer 接受一个函数作为子元素,这个函数接收 Context 的值(即 theme),并返回一个 React 元素。

使用 useContext Hook

import React, { useContext } from'react';
import ThemeContext from './ThemeContext';

const Button = () => {
    const theme = useContext(ThemeContext);
    return (
        <button style={{ background: theme === 'dark'? 'black' : 'white', color: theme === 'dark'? 'white' : 'black' }}>
            Click me
        </button>
    );
};

export default Button;

useContext Hook 使我们可以在函数式组件中更简洁地消费 Context。它接收 Context 对象并返回当前 Context 的值。

2. 服务端渲染基础

服务端渲染(SSR)是一种将 React 应用在服务器端渲染成 HTML 字符串,然后将其发送到客户端的技术。这样做的好处是,客户端可以更快地呈现出页面内容,尤其是对于首屏加载性能有显著提升。

2.1 为什么要使用 SSR

  • SEO 友好:搜索引擎爬虫通常无法执行 JavaScript。通过 SSR,服务器返回给爬虫的是已经渲染好的 HTML 页面,使得爬虫可以更好地理解页面内容,从而提高搜索引擎排名。
  • 更快的首屏加载:在传统的客户端渲染(CSR)中,浏览器需要下载 JavaScript 代码,解析并执行后才能渲染出页面。而 SSR 可以在服务器端生成 HTML,客户端只需加载少量的 JavaScript 代码来增强交互性,大大加快了首屏的显示速度。

2.2 SSR 工作流程

  1. 客户端请求:用户在浏览器中输入 URL 并发送请求。
  2. 服务器渲染:服务器接收到请求后,根据请求的 URL 加载相应的 React 组件,并在服务器端将其渲染成 HTML 字符串。这涉及到 React 组件的生命周期方法(如 getInitialProps 在 Next.js 中)的执行和数据的获取。
  3. 发送 HTML 到客户端:服务器将渲染好的 HTML 字符串发送回客户端。
  4. 客户端注水:客户端接收到 HTML 后,会下载并执行 JavaScript 代码。React 会将已经渲染好的 HTML 与 JavaScript 代码进行“注水”,使页面具备交互性。

例如,在 Next.js 中,一个简单的页面组件可能如下:

import React from'react';

const HomePage = () => {
    return (
        <div>
            <h1>Welcome to my page</h1>
        </div>
    );
};

export default HomePage;

当服务器接收到对首页的请求时,它会渲染这个 HomePage 组件,并将生成的 HTML 发送回客户端。

3. React Context 在 SSR 中的挑战

虽然 React Context 在客户端渲染中有很多优点,但在 SSR 环境中使用它会带来一些挑战。

3.1 服务器端和客户端状态不一致

在 SSR 中,服务器和客户端都需要渲染组件。如果 Context 的值在服务器端和客户端不同步,可能会导致页面在服务器端渲染出一种状态,而在客户端“注水”后呈现出另一种状态。例如,假设我们有一个用于显示当前用户信息的 Context。如果在服务器端没有正确设置用户信息,而在客户端通过异步请求获取到了用户信息并设置到 Context 中,就会出现服务器端渲染的页面没有用户信息,而客户端渲染后有用户信息的不一致情况。

3.2 数据获取和初始化

在 SSR 中,数据获取通常在服务器端进行。如果 Context 依赖的数据需要在服务器端获取,如何正确地将这些数据传递到 Context 中并确保在客户端也能正确使用是一个问题。例如,我们有一个 Context 用于存储购物车信息,购物车数据需要从后端 API 获取。在服务器端,我们需要在渲染组件之前获取购物车数据并设置到 Context 中,同时在客户端也要确保能够正确地复用这些数据。

3.3 性能问题

在 SSR 中,每一个请求都需要在服务器端重新渲染组件树。如果 Context 被过度使用或者在 Context 中传递了大量的数据,可能会导致服务器端渲染性能下降。因为每次渲染都需要处理 Context 的更新和传递,这会增加服务器的计算负担。

4. 解决 React Context 在 SSR 中的问题

为了有效地在 SSR 中使用 React Context,我们需要采取一些策略来解决上述提到的问题。

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

  • 使用同构代码:编写可以在服务器端和客户端都运行的代码。确保 Context 的初始化逻辑在两端是相同的。例如,如果 Context 依赖于某个全局配置,确保这个配置在服务器端和客户端都以相同的方式加载。
  • 传递初始状态:在服务器端渲染时,将 Context 的初始状态作为 props 传递给顶级组件。然后在客户端“注水”时,使用这些初始状态来初始化 Context。在 Next.js 中,可以通过 getInitialProps 方法获取数据并传递给组件。
import React from'react';
import ThemeContext from './ThemeContext';

const App = ({ initialTheme }) => {
    return (
        <ThemeContext.Provider value={initialTheme}>
            {/* 应用的其余部分 */}
        </ThemeContext.Provider>
    );
};

App.getInitialProps = async () => {
    // 假设这里从某个 API 获取主题数据
    const theme = await getThemeFromAPI();
    return { initialTheme: theme };
};

export default App;

这样,服务器端获取的主题数据会作为 initialTheme props 传递给 App 组件,然后用于初始化 ThemeContext

4.2 数据获取和初始化策略

  • 在服务器端获取数据:在服务器端渲染之前,通过 API 调用获取 Context 所需的数据。可以使用中间件(如 Express 中间件)来在请求处理过程中获取数据。例如:
const express = require('express');
const React = require('react');
const ReactDOMServer = require('react-dom/server');
const App = require('./App');

const app = express();

app.get('*', async (req, res) => {
    // 获取购物车数据
    const cartData = await getCartDataFromAPI();
    const html = ReactDOMServer.renderToString(<App initialCart={cartData} />);
    res.send(`
        <!DOCTYPE html>
        <html>
            <head>
                <title>My App</title>
            </head>
            <body>
                <div id="root">${html}</div>
                <script src="/client.js"></script>
            </body>
        </html>
    `);
});

const port = process.env.PORT || 3000;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在上述代码中,服务器在渲染 App 组件之前获取了购物车数据,并将其作为 initialCart props 传递。

  • 客户端复用数据:在客户端,使用传递下来的初始数据来初始化 Context。可以在组件的 useEffect Hook 中进行初始化。
import React, { useEffect } from'react';
import CartContext from './CartContext';

const App = ({ initialCart }) => {
    useEffect(() => {
        // 使用 initialCart 初始化 CartContext
        CartContext.Provider.value = initialCart;
    }, []);

    return (
        <CartContext.Provider value={initialCart}>
            {/* 应用的其余部分 */}
        </CartContext.Provider>
    );
};

export default App;

这样,客户端可以复用服务器端获取的数据来初始化 Context。

4.3 优化性能

  • 减少 Context 传递的数据量:只在 Context 中传递必要的数据。如果某些数据只是部分组件需要,考虑使用局部状态或者其他方式传递数据,而不是放入 Context 中。
  • 使用 memoization:在使用 Context 的组件中,可以使用 React.memo(对于函数式组件)或 shouldComponentUpdate(对于类组件)来防止不必要的重新渲染。例如:
import React from'react';
import ThemeContext from './ThemeContext';

const Button = React.memo(() => {
    const theme = useContext(ThemeContext);
    return (
        <button style={{ background: theme === 'dark'? 'black' : 'white', color: theme === 'dark'? 'white' : 'black' }}>
            Click me
        </button>
    );
});

export default Button;

React.memo 会对 Button 组件进行浅比较,如果 props 没有变化(在这种情况下,虽然没有显式的 props,但 Context 的变化也会触发重新渲染),组件不会重新渲染,从而提高性能。

5. 实际示例:使用 React Context 和 SSR 构建多语言应用

现在我们通过一个实际的示例来展示如何在 SSR 环境中使用 React Context 构建一个多语言应用。

5.1 创建语言 Context

首先,我们创建一个用于管理语言的 Context。

import React from'react';

const LanguageContext = React.createContext('en');

export default LanguageContext;

这里,我们创建了 LanguageContext 并设置默认语言为 en(英语)。

5.2 语言切换组件

我们创建一个组件用于切换语言。

import React, { useContext } from'react';
import LanguageContext from './LanguageContext';

const LanguageSwitcher = () => {
    const language = useContext(LanguageContext);
    const toggleLanguage = () => {
        // 简单的语言切换逻辑,假设只有英语和法语
        const newLanguage = language === 'en'? 'fr' : 'en';
        // 这里可以通过某种方式更新 Context 的值,例如使用 useReducer
    };

    return (
        <button onClick={toggleLanguage}>
            Switch to {language === 'en'? 'French' : 'English'}
        </button>
    );
};

export default LanguageSwitcher;

这个组件通过 useContext 获取当前语言,并提供一个按钮来切换语言。

5.3 翻译函数和数据

我们需要一个函数来根据当前语言获取相应的翻译文本,同时准备翻译数据。

const translations = {
    en: {
        welcome: 'Welcome to our app',
        about: 'About us'
    },
    fr: {
        welcome: 'Bienvenue sur notre application',
        about: 'À propos de nous'
    }
};

const getTranslation = (key, language) => {
    return translations[language][key];
};

export { getTranslation };

getTranslation 函数根据给定的语言和翻译键获取相应的翻译文本。

5.4 服务器端渲染设置

在服务器端,我们需要根据请求头中的语言偏好来设置初始语言,并渲染组件。假设我们使用 Express 作为服务器框架。

const express = require('express');
const React = require('react');
const ReactDOMServer = require('react-dom/server');
const App = require('./App');

const app = express();

app.get('*', async (req, res) => {
    const language = req.headers['accept - language'] && req.headers['accept - language'].split(',')[0].split('-')[0] || 'en';
    const html = ReactDOMServer.renderToString(<App initialLanguage={language} />);
    res.send(`
        <!DOCTYPE html>
        <html>
            <head>
                <title>Multilingual App</title>
            </head>
            <body>
                <div id="root">${html}</div>
                <script src="/client.js"></script>
            </body>
        </html>
    `);
});

const port = process.env.PORT || 3000;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

这里,服务器根据 accept - language 请求头来确定初始语言,并将其作为 initialLanguage props 传递给 App 组件。

5.5 客户端“注水”和组件渲染

在客户端,我们使用传递下来的初始语言来初始化 LanguageContext

import React, { useEffect } from'react';
import LanguageContext from './LanguageContext';
import LanguageSwitcher from './LanguageSwitcher';
import { getTranslation } from './translations';

const App = ({ initialLanguage }) => {
    useEffect(() => {
        // 使用 initialLanguage 初始化 LanguageContext
        LanguageContext.Provider.value = initialLanguage;
    }, []);

    const language = useContext(LanguageContext);
    const welcomeText = getTranslation('welcome', language);

    return (
        <div>
            <h1>{welcomeText}</h1>
            <LanguageSwitcher />
        </div>
    );
};

export default App;

App 组件中,我们通过 useEffect 使用初始语言初始化 LanguageContext,然后获取相应的翻译文本并渲染。

通过这个示例,我们展示了如何在 SSR 环境中有效地使用 React Context 来构建一个多语言应用,解决了数据初始化、状态同步和性能优化等问题。

6. 总结 React Context 在 SSR 中的应用要点

在 SSR 中使用 React Context 需要谨慎处理,以确保服务器端和客户端的一致性、数据的正确获取和初始化以及性能的优化。通过采用同构代码、传递初始状态、合理的数据获取策略以及性能优化技术,我们可以充分发挥 React Context 在 SSR 中的优势,构建出高性能、用户体验良好的应用。无论是多语言应用、主题切换应用还是其他需要共享数据的场景,正确使用 React Context 都能为我们的开发带来便利和效率提升。同时,不断关注 React 官方文档和社区的更新,有助于我们更好地掌握和应用这些技术,应对日益复杂的前端开发需求。

希望通过本文的介绍和示例,读者能够对 React Context 在服务端渲染中的应用有更深入的理解,并能够在实际项目中灵活运用这一技术。在实际开发中,可能还会遇到各种具体的问题和挑战,需要根据项目的特点和需求进行针对性的解决和优化。