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

React组件的服务器端渲染

2024-09-252.1k 阅读

理解 React 组件的服务器端渲染

在前端开发中,React 以其高效构建用户界面的能力而闻名。然而,传统的客户端渲染模式在首屏加载性能上存在一些挑战。当用户访问一个网站时,浏览器需要先下载 JavaScript 代码,解析并运行它,然后 React 才能开始渲染页面。这一过程可能导致较长的白屏时间,影响用户体验。

服务器端渲染(Server - Side Rendering,SSR)应运而生,它允许在服务器端将 React 组件渲染为 HTML 字符串,然后将这个 HTML 发送到客户端。这样,用户在首次访问页面时就能快速看到完整的页面内容,而不必等待 JavaScript 完全加载和执行。

从本质上讲,服务器端渲染打破了 React 仅在客户端运行的局限,利用服务器强大的计算能力提前生成页面。这不仅改善了首屏加载速度,对于搜索引擎优化(SEO)也非常友好,因为搜索引擎爬虫通常不会执行 JavaScript,而服务器端渲染生成的 HTML 可以直接被爬虫抓取和索引。

React 服务器端渲染的基本原理

React 的服务器端渲染依赖于 react - dom/server 模块。这个模块提供了几个关键的方法来实现服务器端渲染:

  1. renderToString:该方法将 React 组件渲染为一个 HTML 字符串。它接收一个 React 元素作为参数,并返回对应的 HTML 字符串。例如:
import React from 'react';
import ReactDOMServer from'react - dom/server';
import App from './App';

const html = ReactDOMServer.renderToString(<App />);
console.log(html);

在这个例子中,App 是一个 React 组件,renderToString 方法将 App 组件渲染成一个 HTML 字符串并打印出来。

  1. renderToStaticMarkup:与 renderToString 类似,但它不会在生成的 HTML 中包含 React 用于 hydration(水合,将静态 HTML 转化为可交互的 React 应用的过程)的额外属性。这对于生成不需要交互的静态页面(如博客文章)非常有用,因为生成的 HTML 会更简洁。例如:
import React from'react';
import ReactDOMServer from'react - dom/server';
import Article from './Article';

const staticMarkup = ReactDOMServer.renderToStaticMarkup(<Article />);
console.log(staticMarkup);

搭建服务器端渲染环境

要实现 React 组件的服务器端渲染,我们需要搭建一个 Node.js 服务器环境,并配置相应的工具和依赖。

  1. 初始化项目:首先,创建一个新的项目目录,并初始化 package.json 文件:
mkdir react - ssr - project
cd react - ssr - project
npm init -y
  1. 安装依赖:我们需要安装 reactreact - domexpress(用于搭建服务器)以及 @babel/core@babel/node@babel/preset - react 等 Babel 相关的包来处理 React 代码:
npm install react react - dom express @babel/core @babel/node @babel/preset - react
  1. 配置 Babel:在项目根目录下创建一个 .babelrc 文件,并添加以下内容:
{
    "presets": ["@babel/preset - react"]
}
  1. 创建 React 组件:在 src 目录下创建一个简单的 React 组件,例如 App.js
import React from'react';

const App = () => {
    return (
        <div>
            <h1>Server - Side Rendering with React</h1>
            <p>This is a simple example of React SSR.</p>
        </div>
    );
};

export default App;
  1. 创建服务器文件:在项目根目录下创建 server.js 文件,编写服务器端代码:
import express from 'express';
import React from'react';
import ReactDOMServer from'react - dom/server';
import App from './src/App';

const app = express();
const port = 3000;

app.get('*', (req, res) => {
    const html = ReactDOMServer.renderToString(<App />);
    const page = `
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF - 8">
            <meta name="viewport" content="width=device - width, initial - scale = 1.0">
            <title>React SSR Example</title>
        </head>
        <body>
            <div id="root">${html}</div>
            <script src="/client - bundle.js"></script>
        </body>
        </html>
    `;
    res.send(page);
});

app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在上述代码中,我们使用 Express 搭建了一个简单的服务器。当客户端请求任何路径时,服务器将 App 组件渲染为 HTML 字符串,并将其嵌入到一个完整的 HTML 页面中返回给客户端。

处理数据获取

在实际应用中,React 组件通常需要从服务器获取数据。在服务器端渲染的场景下,数据获取变得更加复杂,因为我们需要在服务器端就获取到数据,以便在渲染组件时使用。

  1. 在服务器端获取数据:假设我们有一个需要从 API 获取数据的组件 Post.js
import React, { useState, useEffect } from'react';

const Post = () => {
    const [post, setPost] = useState(null);

    useEffect(() => {
        const fetchPost = async () => {
            const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
            const data = await response.json();
            setPost(data);
        };
        fetchPost();
    }, []);

    if (!post) {
        return <div>Loading...</div>;
    }

    return (
        <div>
            <h2>{post.title}</h2>
            <p>{post.body}</p>
        </div>
    );
};

export default Post;

在客户端渲染中,上述代码通过 useEffect 钩子在组件挂载后获取数据。但在服务器端渲染时,这个逻辑就不适用了,因为服务器端没有 useEffect 所依赖的浏览器环境。

我们需要在服务器端提前获取数据。可以创建一个异步函数来获取数据,然后在渲染组件前调用它:

import React from'react';
import ReactDOMServer from'react - dom/server';
import Post from './src/Post';

const fetchPost = async () => {
    const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
    const data = await response.json();
    return data;
};

const renderPost = async () => {
    const postData = await fetchPost();
    const html = ReactDOMServer.renderToString(<Post post={postData} />);
    const page = `
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF - 8">
            <meta name="viewport" content="width=device - width, initial - scale = 1.0">
            <title>React SSR Post Example</title>
        </head>
        <body>
            <div id="root">${html}</div>
            <script src="/client - bundle.js"></script>
        </body>
        </html>
    `;
    return page;
};

renderPost().then((page) => {
    console.log(page);
});
  1. 数据预取与 hydration:当我们在服务器端获取数据并渲染组件后,将 HTML 发送到客户端。客户端需要重新获取相同的数据(以确保客户端和服务器端的数据一致性),这个过程称为 hydration。可以通过在服务器端将数据作为属性传递给组件,并在客户端利用这些数据来避免重复获取。例如:
// 服务器端
import React from'react';
import ReactDOMServer from'react - dom/server';
import Post from './src/Post';

const fetchPost = async () => {
    const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
    const data = await response.json();
    return data;
};

const renderPost = async () => {
    const postData = await fetchPost();
    const html = ReactDOMServer.renderToString(<Post post={postData} />);
    const page = `
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF - 8">
            <meta name="viewport" content="width=device - width, initial - scale = 1.0">
            <title>React SSR Post Example</title>
        </head>
        <body>
            <div id="root">${html}</div>
            <script>
                window.__INITIAL_DATA__ = ${JSON.stringify(postData)};
            </script>
            <script src="/client - bundle.js"></script>
        </body>
        </html>
    `;
    return page;
};

renderPost().then((page) => {
    console.log(page);
});

// 客户端
import React from'react';
import ReactDOM from'react - dom';
import Post from './src/Post';

const initialData = window.__INITIAL_DATA__;
delete window.__INITIAL_DATA__;

ReactDOM.hydrate(<Post post={initialData} />, document.getElementById('root'));

路由与服务器端渲染

在单页应用(SPA)中,路由是不可或缺的部分。在服务器端渲染的场景下,处理路由需要一些额外的注意事项。

  1. 使用 React Router:React Router 是 React 应用中常用的路由库。在服务器端渲染中,我们可以使用 react - router - domreact - router - server 来处理路由。

首先安装相关依赖:

npm install react - router - dom react - router - server

假设我们有一个简单的路由配置 routes.js

import React from'react';
import { BrowserRouter as Router, Routes, Route } from'react - router - dom';
import Home from './src/Home';
import About from './src/About';

const AppRoutes = () => {
    return (
        <Router>
            <Routes>
                <Route path="/" element={<Home />} />
                <Route path="/about" element={<About />} />
            </Routes>
        </Router>
    );
};

export default AppRoutes;
  1. 服务器端路由处理:在服务器端,我们需要根据客户端请求的 URL 来渲染相应的组件。可以使用 StaticRouter 来实现这一点。修改 server.js 文件:
import express from 'express';
import React from'react';
import ReactDOMServer from'react - dom/server';
import { StaticRouter } from'react - router - server';
import AppRoutes from './src/routes';

const app = express();
const port = 3000;

app.get('*', (req, res) => {
    const context = {};
    const html = ReactDOMServer.renderToString(
        <StaticRouter location={req.url} context={context}>
            <AppRoutes />
        </StaticRouter>
    );
    const page = `
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF - 8">
            <meta name="viewport" content="width=device - width, initial - scale = 1.0">
            <title>React SSR with Routes</title>
        </head>
        <body>
            <div id="root">${html}</div>
            <script src="/client - bundle.js"></script>
        </body>
        </html>
    `;
    res.send(page);
});

app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在上述代码中,StaticRouter 根据客户端请求的 URL 渲染相应的路由组件。context 对象可以用于处理重定向等情况。

处理样式

在服务器端渲染中,处理样式也有一些特殊的考虑。

  1. CSS - in - JS:像 styled - components 这样的 CSS - in - JS 库在服务器端渲染中需要一些额外的配置。首先安装 styled - components
npm install styled - components

假设我们有一个使用 styled - components 的组件 Button.js

import React from'react';
import styled from'styled - components';

const StyledButton = styled.button`
    background - color: blue;
    color: white;
    padding: 10px 20px;
    border: none;
    border - radius: 5px;
`;

const Button = () => {
    return <StyledButton>Click me</StyledButton>;
};

export default Button;

在服务器端,我们需要使用 ServerStyleSheet 来收集样式并将其插入到 HTML 中。修改 server.js

import express from 'express';
import React from'react';
import ReactDOMServer from'react - dom/server';
import { ServerStyleSheet } from'styled - components';
import App from './src/App';

const app = express();
const port = 3000;

app.get('*', (req, res) => {
    const sheet = new ServerStyleSheet();
    const html = ReactDOMServer.renderToString(sheet.collectStyles(<App />));
    const styleTags = sheet.getStyleTags();
    const page = `
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF - 8">
            <meta name="viewport" content="width=device - width, initial - scale = 1.0">
            <title>React SSR with Styles</title>
            ${styleTags}
        </head>
        <body>
            <div id="root">${html}</div>
            <script src="/client - bundle.js"></script>
        </body>
        </html>
    `;
    res.send(page);
});

app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});
  1. 传统 CSS 文件:如果使用传统的 CSS 文件,我们可以在服务器端通过 html - webpack - inline - source - plugin 等工具将 CSS 内联到 HTML 中,以避免额外的请求。首先安装相关依赖:
npm install html - webpack - inline - source - plugin

在 Webpack 配置中添加插件:

const HtmlWebpackInlineSourcePlugin = require('html - webpack - inline - source - plugin');

module.exports = {
    //...其他配置
    plugins: [
        new HtmlWebpackInlineSourcePlugin()
    ]
};

然后在服务器端将生成的包含内联 CSS 的 HTML 发送给客户端。

性能优化与最佳实践

  1. 缓存:在服务器端渲染中,缓存可以显著提高性能。可以对渲染结果进行缓存,尤其是对于不经常变化的页面。例如,可以使用 memory - cache 库在 Node.js 服务器端实现简单的缓存:
npm install memory - cache

修改 server.js

import express from 'express';
import React from'react';
import ReactDOMServer from'react - dom/server';
import App from './src/App';
import cache from'memory - cache';

const app = express();
const port = 3000;

const cacheKey = 'app - html - cache';
const cachedHtml = cache.get(cacheKey);

if (cachedHtml) {
    app.get('*', (req, res) => {
        res.send(cachedHtml);
    });
} else {
    const html = ReactDOMServer.renderToString(<App />);
    cache.put(cacheKey, html, 60 * 1000); // 缓存1分钟
    app.get('*', (req, res) => {
        res.send(html);
    });
}

app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});
  1. 代码拆分与懒加载:在客户端,代码拆分和懒加载可以减少初始加载的 JavaScript 体积。使用 Webpack 的 splitChunks 配置可以实现代码拆分。例如:
module.exports = {
    //...其他配置
    optimization: {
        splitChunks: {
            chunks: 'all'
        }
    }
};

在 React 组件中,可以使用 React.lazy 和 Suspense 来实现懒加载:

import React, { lazy, Suspense } from'react';

const OtherComponent = lazy(() => import('./src/OtherComponent'));

const App = () => {
    return (
        <div>
            <Suspense fallback={<div>Loading...</div>}>
                <OtherComponent />
            </Suspense>
        </div>
    );
};

export default App;
  1. 监控与分析:使用工具如 Lighthouse 来监控和分析服务器端渲染应用的性能。Lighthouse 可以提供诸如首屏加载时间、可交互时间等重要指标,并给出优化建议。可以在 Chrome 浏览器中通过开发者工具运行 Lighthouse 审计,也可以使用命令行工具:
npm install -g lighthouse
lighthouse http://localhost:3000

通过不断优化这些方面,我们可以打造出性能卓越的 React 服务器端渲染应用,为用户提供流畅的体验。同时,要持续关注 React 生态系统的发展,及时采用新的技术和最佳实践来提升应用的质量和性能。在实际项目中,还需要根据具体的业务需求和场景,灵活运用上述技术和方法,解决遇到的各种问题,以实现高效、稳定且用户友好的服务器端渲染应用。

在处理复杂业务逻辑和大规模应用时,还需考虑状态管理的问题。例如,使用 Redux 或 MobX 等状态管理库时,在服务器端渲染中要确保状态的正确初始化和同步。以 Redux 为例,需要在服务器端创建 store,并在渲染组件前将初始状态传递给客户端,客户端再根据这个初始状态创建相同的 store。同时,要注意中间件的使用,确保它们在服务器端和客户端都能正常工作。

在部署方面,要考虑服务器的负载均衡和资源分配。可以使用诸如 Nginx 这样的反向代理服务器来处理请求的分发和缓存,提高应用的可用性和性能。此外,对于高流量的应用,还可以考虑使用 CDN(内容分发网络)来加速静态资源的加载,进一步提升用户体验。

总之,React 组件的服务器端渲染是一个强大的技术,但需要综合考虑多个方面的因素,从数据获取、路由处理、样式管理到性能优化和部署,每个环节都对最终应用的质量和用户体验有着重要影响。通过深入理解和掌握这些技术,我们能够开发出高性能、可扩展且用户友好的 React 应用。