服务器端 React 渲染

Avatar of Roger Jin
Roger Jin

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 $200 免费积分!

React 最为人所知的是一个客户端 JavaScript 框架,但您知道您可以在(也许应该)服务器端渲染 React 吗?

假设您为客户构建了一个全新的、快速的事件列表 React 应用。 该应用连接到您使用自己喜欢的服务器端工具构建的 API。 几周后,客户告诉您他们的页面没有显示在 Google 上,并且在发布到 Facebook 时看起来也不好。 看起来可以解决,对吧?

您发现要解决这个问题,您需要在初始加载时从服务器渲染您的 React 页面,以便来自搜索引擎和社交媒体网站的爬虫可以读取您的标记。 有证据表明,Google 有时会执行 javascript 并且可以索引生成的內容,但不总是这样。 因此,如果您想确保良好的 SEO 以及与 Facebook、Twitter 等其他服务的兼容性,始终建议进行服务器端渲染。

在本教程中,我们将带您逐步完成服务器端渲染示例,包括解决与与 API 交互的 React 应用相关的常见障碍。

服务器端渲染的优点

SEO 可能是让您的团队开始讨论服务器端渲染的对话,但它并不是唯一的潜在好处。

这是最大的一个:服务器端渲染可以更快地显示页面。 使用服务器端渲染,您的服务器对浏览器的响应是已准备好呈现的页面 HTML,因此浏览器可以开始渲染,而无需等待所有 JavaScript 下载和执行。 在浏览器下载和执行渲染页面所需的 JavaScript 和其他资产时,没有“空白页”,这在完全客户端渲染的 React 站点中可能会发生。

入门

让我们了解如何使用 Babel 和 webpack 将服务器端渲染添加到基本的客户端渲染 React 应用中。 我们的应用将具有从第三方 API 获取数据的额外复杂性。

编者注: 这篇文章来自一家 CMS 公司,我收到了他们的一些垃圾邮件,我认为这非常不酷,所以我删除了本文中所有对他们的引用,并用通用的“CMS”术语替换了它们。

import React from 'react';
import cms from 'cms';

const content = cms('b60a008584313ed21803780bc9208557b3b49fbb');

var Hello = React.createClass({
  getInitialState: function() {
    return {loaded: false};
  },
  componentWillMount: function() {
    content.post.list().then((resp) => {
      this.setState({
        loaded: true,
        resp: resp.data
      })
    });
  },
  render: function() {
    if (this.state.loaded) {
      return (
        <div>
          {this.state.resp.data.map((post) => {
            return (
              <div key={post.slug}>{post.title}</div>
            )
          })}
        </div>
      );
    } else {
      return <div>Loading...</div>;
    }
  }
});

export default Hello;

以下是入门代码中还包含的内容

  • `package.json` – 用于依赖项
  • Webpack 和 Babel 配置
  • `index.html` – 应用的 HTML
  • `index.js` – 加载 React 并渲染 `Hello` 组件

要运行该应用,请先克隆存储库

git clone ...
cd ..

安装依赖项

npm install

然后启动开发服务器

npm run start

浏览至 `http://localhost:3000` 以查看应用

如果您查看渲染页面的源代码,您会看到发送到浏览器的标记只是一个指向 JavaScript 文件的链接。 这意味着页面的內容不能保证被搜索引擎和社交媒体平台爬取

添加服务器端渲染

接下来,我们将实现服务器端渲染,以便将完全生成的 HTML 发送到浏览器。

要开始,我们将安装 Express,这是一个 Node.js 服务器端应用程序框架

npm install express --save

我们想要创建一个渲染 React 组件的服务器

import express from 'express';
import fs from 'fs';
import path from 'path';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import Hello from './Hello.js';

function handleRender(req, res) {
  // Renders our Hello component into an HTML string
  const html = ReactDOMServer.renderToString(<Hello />);

  // Load contents of index.html
  fs.readFile('./index.html', 'utf8', function (err, data) {
    if (err) throw err;

    // Inserts the rendered React HTML into our main div
    const document = data.replace(/<div id="app"><\/div>/, `<div id="app">${html}</div>`);

    // Sends the response back to the client
    res.send(document);
  });
}

const app = express();

// Serve built files with static files middleware
app.use('/build', express.static(path.join(__dirname, 'build')));

// Serve requests with our handleRender function
app.get('*', handleRender);

// Start server
app.listen(3000);

让我们分解一下正在发生的事情…

handleRender 函数处理所有请求。 文件顶部导入的 ReactDOMServer 类 提供了 `renderToString()` 方法,该方法将 React 元素渲染到其初始 HTML。

ReactDOMServer.renderToString(<Hello />);

这将返回 `Hello` 组件的 HTML,我们将其注入到 `index.html` 的 HTML 中,以在服务器上生成页面的完整 HTML。

const document = data.replace(/<div id="app"><\/div>/, `<div id="app">${html}</div>`);

要启动服务器,请更新 `package.json` 中的启动脚本,然后运行 `npm run start`

"scripts": {
  "start": "webpack && babel-node server.js"
},

浏览至 `http://localhost:3000` 以查看应用。 瞧! 您的页面现在正在从服务器渲染。 但是有一个问题。 如果您在浏览器中查看页面源代码,您会注意到博客文章仍然未包含在响应中。 这是怎么回事? 如果我们打开 Chrome 中的网络选项卡,我们会看到 API 请求正在客户端发生。

尽管我们正在服务器上渲染 React 组件,但 API 请求异步地在 `componentWillMount` 中发生,并且组件在请求完成之前被渲染。 因此,即使我们在服务器上进行渲染,我们也只进行了部分渲染。 事实证明,React 存储库中存在一个 问题,有 100 多条评论讨论了这个问题和各种解决方法。

在渲染之前获取数据

要解决此问题,我们需要确保 API 请求在渲染 `Hello` 组件之前完成。 这意味着在 React 的组件渲染周期之外发出 API 请求,并在渲染组件之前获取数据。

要将数据获取移动到渲染之前,我们将安装 react-transmit

npm install react-transmit --save

React Transmit 为我们提供了优雅的包装组件(通常称为“高阶组件”),用于获取在客户端和服务器上都起作用的数据。

以下是使用 React Transmit 实现的组件

import React from 'react';
import cms from 'cms'
import Transmit from 'react-transmit';

const content = cms('b60a008584313ed21803780bc9208557b3b49fbb');

var Hello = React.createClass({
  render: function() {
    if (this.props.posts) {
      return (
        <div>
          {this.props.posts.data.map((post) => {
            return (
              <div key={post.slug}>{post.title}</div>
            )
          })}
        </div>
      );
    } else {
      return <div>Loading...</div>;
    }
  }
});

export default Transmit.createContainer(Hello, {
  // These must be set or else it would fail to render
  initialVariables: {},
  // Each fragment will be resolved into a prop
  fragments: {
    posts() {
      return content.post.list().then((resp) => resp.data);
    }
  }
});

我们已经将我们的组件包装在一个高阶组件中,该组件使用 `Transmit.createContainer` 获取数据。 我们已经从 React 组件中删除了生命周期方法,因为没有必要两次获取数据。 并且我们已经更改了 `render` 方法以使用 `props` 引用而不是 `state`,因为 React Transmit 将数据作为 props 传递给组件。

为了确保服务器在渲染之前获取数据,我们导入 Transmit 并使用 `Transmit.renderToString` 而不是 `ReactDOM.renderToString` 方法。

import express from 'express';
import fs from 'fs';
import path from 'path';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import Hello from './Hello.js';
import Transmit from 'react-transmit';

function handleRender(req, res) {
  Transmit.renderToString(Hello).then(({reactString, reactData}) => {
    fs.readFile('./index.html', 'utf8', function (err, data) {
      if (err) throw err;

      const document = data.replace(/<div id="app"><\/div>/, `<div id="app">${reactString}</div>`);
      const output = Transmit.injectIntoMarkup(document, reactData, ['/build/client.js']);

      res.send(document);
    });
  });
}

const app = express();

// Serve built files with static files middleware
app.use('/build', express.static(path.join(__dirname, 'build')));

// Serve requests with our handleRender function
app.get('*', handleRender);

// Start server
app.listen(3000);

重新启动服务器,浏览至 `http://localhost:3000`。 查看页面源代码,您会看到页面现在正在服务器上完全渲染!

更进一步

我们做到了! 在服务器上使用 React 可能很棘手,尤其是在从 API 获取数据时。 幸运的是,React 社区蓬勃发展,创建了许多有用的工具。 如果您有兴趣了解用于构建在客户端和服务器上渲染的大型 React 应用的框架,请查看 Walmart Labs 的 ElectrodeNext.js。 或者,如果您想在 Ruby 中渲染 React,请查看 AirBnB 的 Hypernova