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 的 Electrode 或 Next.js。 或者,如果您想在 Ruby 中渲染 React,请查看 AirBnB 的 Hypernova。
只是想指出,从技术上讲,用户仍然会看到“空白页”。 当他们访问您的域名时,用户仍然必须等待向您的服务器发出请求,然后等待您的服务器向 API 发出请求,然后数据被发送回用户。 在此期间,用户在屏幕上什么也看不到。 如果向第三方 API 的请求很慢,我认为实际上更好的体验是从 CDN 返回您的应用程序代码,以便您的空 UI 尽快显示,然后您至少可以在等待第三方 API 时向用户显示一个旋转器。
我同意,顺便说一下,如果相关的 API 在同一个服务器上,请求可能会非常快,这对于服务器渲染来说是一个很好的理由 :)
您的解决方案的问题在于空 UI,这对 SEO 来说非常糟糕。
我认为对于具有许多 API 请求(具有不同的响应时间)的应用,我们必须混合使用服务器端请求和客户端请求,优先考虑对 SEO 和/或 UI 的顶层至关重要的请求,以进行服务器端渲染。
如果您担心服务器从第三方服务返回数据的额外往返行程,您可以始终先在客户端渲染 UI,不包括第三方组件(可能带有一个临时的旋转器)。 延迟对 DOMContentLoaded 或 onload 事件的客户端请求以请求服务器执行事务。 没有办法避免网络延迟(除非您不使用网络,这将是我的答案,适用于这种情况;),但我看不出为什么您不能将它们混在一起,以便它们在您喜欢的任何时间点发生。
Front End Center(付费内容)的第 15 集有一个名为“客户端渲染的隐藏成本”的视频,深入探讨了这些内容,并使用不同的基于 React 的库进行静态 HTML 渲染。这是一个引人入胜的话题,我很高兴人们认真对待它并从不同的角度着手研究它。
SEO 始终是执行服务器端渲染的主要原因。但是,如果加载时间是一个问题,那么对您的应用程序数据采用某种形式的客户端缓存是否有益?Service Workers 解决此问题,并且通过不要求在每个初始页面加载时加载所有应用程序数据,从而节省数据带宽。
尽管 iOS 在 Service Workers 上进展缓慢,但服务器端渲染仍会为您提供一定的加速,而 Android 和 PC 用户会看到响应时间的显著提升。此外,Service Workers 将为应用程序的离线使用以及增强的原生移动功能打开大门。
只是一个提示,您不应该在 componentWillMount 中调用 API… 如果您的调用返回错误,您将玩得很开心 :)
嗨,各位,把所有这些工作都抛开,使用一个为您完成所有工作的服务吧
https://www.roast.io
预渲染
托管
微笑
我是 beta 测试的一部分,我知道如果社区接受并使用它,这个人会非常高兴!
SPA 对 Web 来说是暴行。如果您喜欢 SSR,那么您只是在向您的代码库中添加复杂性,而您应该从一开始就使用 PJAX。呵呵。