29、React 服务器端渲染及路由集成全解析

React 服务器端渲染及路由集成全解析

1. 服务器端渲染的原因

服务器端渲染(SSR)是一种将 React 组件在服务器上渲染成 HTML 并发送到客户端的技术。以下是一些考虑使用 SSR 的原因:
- 搜索引擎优化(SEO) :有证据表明,服务器端渲染的应用在搜索引擎索引和抓取方面表现更好。虽然大型搜索引擎如 Google 可以执行或模拟 JavaScript 和 DOM,但无需 DOM 即可渲染动态内容的网站似乎表现更佳。对于高度依赖搜索引擎结果的公共应用,SSR 可以提高爬虫友好性。
- 静态页面生成 :如果只需要 React 的静态方面,可以使用 React - DOM 的静态渲染功能创建静态页面生成器或模板库。
- 优化用户体验 :对于需要尽快向用户显示内容的应用,服务器端渲染可以比客户端渲染更快地呈现内容。这对于依赖展示广告或其他静态付费内容的应用尤为重要。在这种情况下,更关注首次绘制(First Paint),即用户首次在浏览器中看到内容的时间。
- 速度指标优化 :除了首次绘制,感知速度指数(Speed Index)也是衡量应用渲染性能的重要指标。SSR 可以通过让浏览器在加载过程中更早地渲染更多内容,从而有助于提高速度指数。

然而,并非所有应用都适合 SSR。对于高度交互式、功能丰富的应用,如 Basecamp 或 Asana,用户能够与应用交互的时间(TTI)可能比首次绘制更重要,因此 SSR 可能并不适用。以下是不同应用场景的分析:
| 应用类型 | 关注点 | 是否适合 SSR |
| ---- | ---- | ---- |
| Basecamp(项目管理应用) | 快速交互,如搜索问题、更新待办事项和检查项目状态 | 否 |
| Medium(博客/写作应用) | 快速阅读和浏览文章 | 是 |

在考虑 SSR 时,还需要权衡服务器和客户端渲染的资源使用。如果渲染大量数据,在服务器上进行可能会导致更大的初始负载,从而延长 TTI 并消耗更多服务器资源。

2. 企业和消费应用中的服务器渲染

服务器渲染在企业和消费应用中都有不同的应用场景和考虑因素。
- 企业应用 :在企业应用中,用户通常更关注应用的交互速度、稳定性、可靠性和清晰度。例如,一个处理大量财务数据的企业应用,服务器渲染可能会引入新的安全问题,并且由于数据量巨大,可能无法实现预期的性能提升。因此,企业应用可能会优先考虑其他优化措施,如提高服务器性能、优化资产服务和延迟客户端数据获取。
- 消费应用 :消费应用如 Facebook、Twitter 和 Amazon 竞争激烈,用户对速度和性能有较高期望。在电子商务应用中,首次绘制时间和 SEO 至关重要,服务器渲染可以提高用户体验并增加转化率。

3. 可能不需要 SSR 的情况

SSR 虽然有潜在好处,但也会引入显著的复杂性。以下是一些可能不需要 SSR 的原因:
- 同步服务器和客户端 :需要确保服务器和客户端能够正确同步,包括设置标记、事件处理程序等。认证实现也需要考虑来自服务器或客户端的请求。
- 不同的运行范式 :客户端和服务器在不同的范式下运行,如服务器上没有 DOM 和文件系统。需要协调交接和渲染过程,并正确处理依赖浏览器环境的组件。
- 技术耦合 :React 通常在 Node.js 运行时上运行,这会使客户端和服务器紧密耦合,增加对 JavaScript 语言和平台的依赖。
- 性能调优 :微调 SSR 需要对客户端和服务器进行特殊调整,性能提升通常是通过小的增量改进实现的,并且可能涉及权衡。这可能会导致灵活性降低和维护过程更复杂。

因此,在决定是否使用 SSR 时,应遵循“按需使用”的原则,充分考虑应用的实际需求和权衡。

4. React 服务器端渲染 API

ReactDOMServer 提供了四个重要的方法来生成组件的初始 HTML:
- renderToString :将 React 元素转换为对应的 HTML 标记。该方法根据组件的初始状态和属性生成 HTML。

ReactDOMServer.renderToString(element) // 返回字符串
  • renderToStaticMarkup :与 renderToString 类似,但不会附加 React 在客户端接管时使用的额外 DOM 属性。适用于静态页面生成或模板。
ReactDOMServer.renderToStaticMarkup(element) // 返回字符串
  • renderToNodeStream renderToStaticNodeStream :这两个方法使用 Node 的流 API,是异步的。它们在 React 16 中引入,解决了同步渲染方法在渲染复杂页面时的挑战。
5. 服务器端组件渲染实践

在开始服务器端渲染之前,需要对服务器代码进行一些更改。以下是初始服务器代码的示例:

import { __PRODUCTION__ } from 'environs';
import { resolve } from 'path';
import bodyParser from 'body-parser';
import compression from 'compression';
import cors from 'cors';
import express from 'express';
import helmet from 'helmet';
import favicon from 'serve-favicon';
import hpp from 'hpp';
import logger from 'morgan';
import cookieParser from 'cookie-parser';
import responseTime from 'response-time';
import * as firebase from 'firebase-admin';
import config from 'config';
import DB from '../db/DB';

const app = express();
const backend = DB();

app.use(logger(__PRODUCTION__ ? 'combined' : 'dev'));
app.use(helmet.xssFilter({ setOnOldIE: true }));
app.use(responseTime());
app.use(helmet.frameguard());
app.use(helmet.ieNoOpen());
app.use(helmet.noSniff());
app.use(helmet.hidePoweredBy({ setTo: 'react' }));
app.use(compression());
app.use(cookieParser());
app.use(bodyParser.json());
app.use(hpp());
app.use(cors({ origin: config.get('ORIGINS') }));
app.use('/api', backend);
app.use(favicon(resolve(__dirname, '..', 'static', 'assets', 'meta', 'favicon.ico')));

app.use((req, res, next) => {
    const err = new Error('Not Found');
    err.status = 404;
    next(err);
});

app.use((err, req, res) => {
    console.error(err);
    return res.status(err.status || 500).json({
        message: err.message
    });
});

module.exports = app;

接下来,可以尝试在服务器上渲染一个简单的组件。以下是具体步骤:
1. 引入 React - DOM 并使用 renderToString 方法渲染一个简单的 div 组件。

//...
app.use('/api', backend);
app.use(favicon(resolve(__dirname, '..', 'static', 'assets', 'meta', 'favicon.ico')));
app.use('*', (req, res, next) => {
    const componentResponse = ReactDOMServer.renderToString(
        React.createElement(
            'div',
            null,
            `Rendered on the server at ${new Date()}`
        )
    );
    res.send(componentResponse).end();
});
//...
  1. 运行服务器并使用 cURL 检查响应。
$ npm run server:dev
// ... 在另一个终端会话中
$  curl -v http://localhost:3000

通过以上步骤,就可以开始在服务器上使用 React 进行渲染,并逐步探索服务器端渲染的更多功能和优化方法。

以下是服务器渲染的基本流程图:

graph TD;
    A[浏览器请求] --> B[服务器中间件];
    B --> C[解析请求];
    C --> D[安全处理];
    D --> E[日志记录];
    E --> F[压缩处理];
    F --> G[响应处理];
    G --> H[使用 ReactDOM 生成 HTML];
    H --> I[发送响应到客户端];

通过以上内容,我们对 React 服务器端渲染的原理、适用场景、API 以及实践步骤有了更深入的了解。在实际应用中,需要根据具体需求和场景权衡是否使用 SSR,并进行相应的优化和调整。

6. 服务器端渲染与路由集成

在实际应用中,服务器端渲染通常需要与路由集成,以根据不同的 URL 路径渲染不同的组件。以下是一些关于集成 React Router 进行服务器端渲染的关键点:

  • 路由配置 :首先,需要定义应用的路由配置。可以使用 React Router 的 BrowserRouter StaticRouter 来处理路由。在服务器端,通常使用 StaticRouter ,因为它不依赖于浏览器的历史记录。
import { StaticRouter } from 'react-router-dom';
import React from 'react';
import { renderToString } from 'react-dom/server';
import App from './App';

const handleRender = (req, res) => {
    const context = {};
    const html = renderToString(
        <StaticRouter location={req.url} context={context}>
            <App />
        </StaticRouter>
    );

    if (context.url) {
        res.redirect(301, context.url);
    } else {
        res.send(`
            <!doctype html>
            <html>
                <head>
                    <title>My App</title>
                </head>
                <body>
                    <div id="root">${html}</div>
                    <script src="/bundle.js"></script>
                </body>
            </html>
        `);
    }
};

export default handleRender;
  • 服务器端路由处理 :在服务器端代码中,需要根据请求的 URL 调用相应的路由处理函数。以下是一个简单的 Express 服务器示例:
import express from 'express';
import handleRender from './handleRender';

const app = express();

app.use(express.static('public'));

app.get('*', handleRender);

const port = 3000;
app.listen(port, () => {
    console.log(`Server is running on port ${port}`);
});
7. 数据获取与服务器端渲染

在服务器端渲染时,通常需要在渲染组件之前获取数据。以下是一些常见的数据获取方法:

  • 在服务器端获取数据 :可以在服务器端使用 fetch 或其他数据获取库来获取数据,然后将数据传递给组件。
import fetch from 'node-fetch';

const handleRender = async (req, res) => {
    const data = await fetch('https://api.example.com/data')
      .then(response => response.json());

    const context = {};
    const html = renderToString(
        <StaticRouter location={req.url} context={context}>
            <App data={data} />
        </StaticRouter>
    );

    if (context.url) {
        res.redirect(301, context.url);
    } else {
        res.send(`
            <!doctype html>
            <html>
                <head>
                    <title>My App</title>
                </head>
                <body>
                    <div id="root">${html}</div>
                    <script>
                        window.__INITIAL_DATA__ = ${JSON.stringify(data)};
                    </script>
                    <script src="/bundle.js"></script>
                </body>
            </html>
        `);
    }
};

export default handleRender;
  • 客户端数据复用 :在客户端渲染时,可以复用服务器端获取的数据,避免重复请求。
const initialData = window.__INITIAL_DATA__;

ReactDOM.hydrate(
    <BrowserRouter>
        <App data={initialData} />
    </BrowserRouter>,
    document.getElementById('root')
);
8. 服务器端渲染的性能优化

为了提高服务器端渲染的性能,可以采取以下一些优化措施:

优化措施 说明
代码分割 使用 Webpack 等工具进行代码分割,减少初始加载的 JavaScript 体积。
缓存机制 对渲染结果进行缓存,避免重复渲染相同的内容。可以使用内存缓存或分布式缓存。
异步渲染 使用 renderToNodeStream renderToStaticNodeStream 进行异步渲染,提高服务器的并发处理能力。
数据预取 在服务器端提前获取数据,减少客户端等待时间。
9. 服务器端渲染的安全考虑

在进行服务器端渲染时,需要注意以下安全问题:

  • XSS 攻击 :确保在渲染 HTML 时对用户输入进行正确的转义,防止跨站脚本攻击。
  • 数据泄露 :避免在服务器端渲染的 HTML 中包含敏感信息,如用户密码、令牌等。
  • 代码注入 :对传入的参数和数据进行严格的验证和过滤,防止代码注入攻击。
10. 总结与展望

服务器端渲染为 React 应用带来了许多优势,如更好的 SEO、更快的首次绘制时间等。但同时也引入了一定的复杂性,需要在实际应用中进行权衡和优化。

在未来的发展中,服务器端渲染技术可能会不断完善,性能和安全性将得到进一步提升。同时,随着前端框架和工具的不断发展,服务器端渲染与其他技术的集成也将更加紧密。

以下是一个总结服务器端渲染流程的 mermaid 流程图:

graph LR;
    A[用户请求 URL] --> B[服务器接收请求];
    B --> C[解析 URL 路径];
    C --> D{是否匹配路由};
    D -- 是 --> E[根据路由获取数据];
    D -- 否 --> F[返回 404 页面];
    E --> G[使用 ReactDOM 渲染组件];
    G --> H[生成 HTML 字符串];
    H --> I[发送 HTML 到客户端];
    I --> J[客户端加载 JavaScript];
    J --> K[React 进行水合操作];

通过以上内容,我们全面了解了 React 服务器端渲染的各个方面,包括与路由的集成、数据获取、性能优化和安全考虑等。在实际项目中,需要根据具体需求和场景,灵活运用这些技术,以实现高效、安全的服务器端渲染应用。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值