使用React轻松实现服务器端渲染SSR

这篇文章讨论了以下主题:

  • 服务器端渲染如何改善Web应用程序的性能和用户体验。
  • React对服务器端渲染的内置支持。
  • 与框架。

服务器端渲染是指Web服务器在HTTP响应上返回动态HTML的技术。 动态是指响应上的HTML根据请求中的某些变量而有所不同。 通常,变量是URL。

Web服务器在HTTP响应上返回动态HTML的想法根本不是什么新鲜事; 自上世纪创建以来,Web服务器就已经进行了SSR 。 上世纪和现在之间的区别在于,由于当时Web浏览器的功能有限,因此没有其他选择可以替代SSR。 在过去,服务器是完成大部分工作的服务器。 客户端上JavaScript的使用非常有限。

单页应用

在2000年代初期,一种新方法开始流行,即单页面应用程序-也称为SPA。

单页应用程序的想法是,我们将充分利用现代浏览器功能,尝试在浏览器上完成尽可能多的工作。 通过这种方法,JavaScript成为构建Web应用程序时使用的主要语言。

如果您使用创建React App(CRA)创建React应用,那么您正在创建SPA。

SPA的优势

与以前的范例相比,单页应用程序具有许多优势:

  • 通过加快用户交互速度,避免了服务器往返网络的往返,从而改善了用户体验。
  • 通过启用新的和更复杂的用户交互,可以改善用户体验。
  • 它可以降低成本并提高可伸缩性。 考虑一下直接连接到Firebase数据库的SPA。
  • 无需维护前后两个不同的代码库。

SPA问题

SPA带来了一些新问题。 您是否看过以下屏幕:

我们在使用SPA时可能会遇到的一些问题是:

  • 如果网络速度慢并且需要5秒钟来下载初始JavaScript捆绑包怎么办? 在那之前,用户将看不到任何东西。
  • 社会元。 诸如Facebook metas之类的内容不适用于SPA。
  • SEO? 这不再是问题,如您在下图中可以看到的,Google可以读取SPA:

如果禁用JS并导航到React Router站点,您将看到一个空白的空白页。 因此,Google可以执行和读取React SPA。

服务器端渲染

使用SSR,我们的大多数代码都可以在服务器和客户端上运行 。 显然,我们无法在服务器和客户端上同时运行100%的代码,因为存在仅与服务器有关的代码和仅与客户端有关的代码。

由于没有DOM,因此在服务器上运行以下代码没有任何意义:

ReactDOM.render(, document.getElementById("root"));

而且您无法在浏览器上运行服务器:

 const server = express() 
server.listen(8889, () => {
console.log(`Running an Express server`)
})

使用SSR改善性能

在服务器上呈现页面的主要优点之一是,我们可以缩短应用程序的呈现时间,从而改善用户体验。

 1 -> HTML Request 
2 <- HTML Response
3 -> JS Request
4 <- JS Response
5 - JS executed and UI rendered without data
6 -> API request
7 <- JSON response
8 - UI rendered with data

完成#1,2,3和4所需的时间取决于网络和服务器。 对于CDN,这将非常快,尽管在SSR中,仅#3和4将从CDN中受益。

#5完成所需的时间取决于客户端设备。

完成#6和7所需的时间取决于网络和服务器(在CDN中比#1到4更不可能)

#8完成所需的时间取决于客户端设备。

长话短说,看看前面的序列,在SPA中,用户需要完成#5才能看到UI,并需要完成#8才能看到填充数据的UI。 在SSR中,用户可以看到在#2末尾填充了数据的UI

在React中实现SSR

这是容易的部分,因为React内置了对SSR的支持(Suspense除外,即将推出!)。 您唯一需要做的就是import { renderToString } from "react-dom/server"并使用应用程序的Root组件调用renderToString函数。

  const React = require(“反应”) 
const {renderToString} = require(“ react-dom / server”)
const Root = React.createElement(
“ div”,
空值,
“你好SSR”

console.log(renderToString(Root))

您可以在此处运行之前的代码

具有SSR支持的React库

这是具有强大SSR支持的库的列表,您可能希望将它们与react-dom / server结合使用:

  • React Router用于路由客户端和服务器端。
  • 用于设计应用程序样式的样式化组件。
  • 反应头盔以将metas添加到头部。
  • 用于数据获取的Apollo客户端。

组态

在上一节中,我们看到了在服务器上渲染React是多么容易。 现代现实世界的Web应用程序比我们运行的“ Hello SSR”示例要复杂得多,我们需要进行大量配置才能开发和构建它们。

Webpack是当今开发和捆绑Web应用程序的行业标准。 Webpack在帮助我们捆绑Web应用程序方面做了很多有益的事情,尽管配置Webpack对于许多开发人员而言可能是一项繁琐且不费力的任务。 幸运的是,有许多库和框架可以帮助我们进行设置。 不幸的是,并非所有人都支持SSR。

让我概述一下Webpack如何理解不同的SSR替代方案。

发展历程

要使用Webpack开发应用程序,您至少需要:

  • Webpack,显然:
    const webpack = require('webpack');
  • 基于某些Webpack配置的Webpack编译器:
    const webpackConfig = require('./webpack.config.js');
    const compiler = webpack(webpackConfig)
  • 在开发时动态提供HTML,JS和CSS的服务器:
    const serverConfig = require('./webpackDevServer.config.js');
    const WebpackDevServer = require('webpack-dev-server');
    const devServer = new WebpackDevServer( compiler, serverConfigs );

创建React应用程序最流行的工具是Create React App(CRA)。 不幸的是,CRA不支持SSR。 让我们看看如何将SSR支持添加到CRA。

首先,我们应该了解如何在CRA中设置Webpack。 我们需要了解两部分:i)配置和ii)脚本:

组态

  • webpack.config.js
  • webpackDevServer.config.js

脚本:

  • start.js

当我们运行yarn start ,script / start.js在我们的机器上启动Webpack开发服务器。 之后,当我们导航到localhost:300X时,Webpack开发服务器将返回静态HTML和包含应用程序JS的bundle.js。 在本文中,我将端口300X称为可用于运行WebpackDevServer的端口CRA。

Webpack正在监视应用程序的源代码,因此,每次编辑和保存任何代码时, compiler编译该应用程序。 发生这种情况时,Webpack开发服务器会通过websocket向浏览器发送一条消息,说“存在UI的新版本”,以便可以对其进行更新。 这样,CRA可以带来良好的开发经验。

生产建立

在将应用程序部署到生产环境之前,您需要运行build过程。 CRA中的构建会执行script / build.js,该脚本使用webpack.config.js(都是react-scripts的一部分)将应用程序的源代码打包为可以分发到Web的最佳形式。

组态

  • webpack.config.js

脚本:

  • build.js

一旦在本地或理想情况下在CI(连续集成)中运行构建脚本,就可以部署Web应用程序。

部署CRA非常简单,因为该应用程序由HTML,JS和CSS等静态资产组成 。 我们不需要服务器即可在生产环境中运行该应用程序,因为我们发送给所有浏览器的应用程序对于所有浏览器都是相同的。 您可以从CDN提供静态应用程序。

反应脚本

如果您像大多数人一样,使用Create React App创建一个React应用,现在您想为其添加SSR支持,则您必须进行一些重大更改。 我看到进行这些更改的两条路径。 一种是弹出您的应用程序(这是一种单向操作,不建议操作),然后将WebpackDevServer替换为可用于开发和生产的生产就绪型服务器。 我看到的另一条路径不是弹出您的应用程序,而是在图片上添加另一个“框”,您可以随时轻松将其删除。

我认为优质软件的设计方式使我们可以立即做出决定,并在将来轻松改变主意。 换句话说,好的软件是可组合的软件。

服务器

让我们看看如何用SSR编写CRA,以及在图片上添加另一个“框”的意思:

首先,我们添加另一台服务器,在这种情况下运行在端口8888上的Express服务器。 这个新服务器将负责动态生成HTML,而不是始终返回相同的index.html。

当我们导航到localhost:8888时,新服务器将使用React应用程序的根组件调用renderToString并在HTTP响应中发送该字符串。 只有在请求没有尝试从React应用程序访问CSS或JS资产时,服务器才应该这样做。

如果请求尝试从React应用访问CSS或JS资产,则服务器会将请求代理到在端口300X上运行的WebpackDevServer。

我们使用这种方法所做的就是让新服务器处理它所关心的(HTML),并让CRA继续处理其余的事情。 通过添加以下中间件,可以使用Express来实现该过程非常简单:

 const port = process.env.REACT_APP_WEBPACK_DEV_SERVER_PORT 
router.use(
["/static", "/sockjs-node"],
proxy({
target: `http://localhost:${port}`,
ws: true
})
);

关于该代码段的一些注意事项:

  • 环境变量process.env.REACT_APP_WEBPACK_DEV_SERVER_PORT不属于CRA。 我们需要修改scripts / start.js以添加该变量。
  • CRA将所有静态资源放在localhost:300X / static下,因此很容易代理它们。
  • 我们使用的代理是http-proxy-middleware。 我们仅在开发环境中use代理。
  • Webpack HMR使用websockets通知浏览器该应用程序已更改。 它使用的路径是localhost:300X / sockjs-node。

在这种方法中,您将在要添加SSR的每个CRA中添加此代理。 因此,您可以将其提取到Express中间件中,以在不同的应用程序之间复用。

index.html

CRA具有index.html模板,该模板用于:

  • 构建时注入一些数据,例如页面标题。
  • 运行时

    中注入React应用的HTML。

我们的服务器还需要一个HTML模板,以便从renderToString();注入HTML renderToString(); 。 为了继续将CRA与SSR功能组合在一起,我们将使用相同的模板。

在本文中,我将不讨论如何使用相同模板的实现细节。 如您所见,每个应用程序的实现都将是相同的,因此我们可以将其抽象为一些可重用的功能。 我已经做了,随时使用。

应用特定的服务器代码

每个应用程序都有一些具体的代码,因此不能抽象为通用中间件

以下应用仅需要React:

 const html = renderToString(); 

下一个应用程序使用GraphQL,React-Router和StyledComponents。 所有这些库都具有SSR支持,并且需要在服务器上进行一些小的设置。

 const sheet = new ServerStyleSheet(); 
const graphqlClient = new ApolloClient({
link: createHttpLink({
uri: `${API_BASE_URL}/api/graphql`,
fetch
}),
cache: new InMemoryCache()
});

let html;
const App = (





);
await getDataFromTree(App);
html = renderToString(App);

剧本

向CRA添加SSR功能的最后两个步骤将我们带入开始和构建脚本:

脚本/ build.js

在生产环境中将应用程序捆绑在客户端上的构建脚本是相同的,因为我们没有更改CRA的运行方式。

脚本/ build-server.js

我们需要创建一个新的构建脚本,以将我们的代码(例如JSX)转换为可以在服务器上运行的JS。 为了保持组成,新的构建将由react-scripts build && react-scripts-ssr server-build组成。 如果您想使用它,我已经实现了服务器构建脚本。

脚本/start.js

我们需要更改CRA启动脚本的工作方式,因为现在它还需要启动呈现HTML的服务器。 是的,它在同一包中实现。

库与框架

现在是经常争论的问题,我应该使用框架还是库?

框架和库都是某些具体解决方案的抽象。 不同之处在于库更专业。 图书馆做的事情更少。 框架是抽象的抽象,换句话说,框架是库的超集。

这是什么意思? 一个框架将解决很多问题,而您对解决方案中涉及的更多具体部分一无所知。 例如,您可以将Next.js用于React中的服务器端渲染,这样做将免费获得代码拆分和预取。

使用框架的成本是多少? 最明显的是,它可能涵盖了您不需要的用例。 我认为真正的成本是学习成本和变革成本。

学习成本

获得属于特定抽象级别(框架)的领域知识会产生一定的成本,这在更具体的实现(库)中可能没有用。

例如,如果框架使用紧密结合解决方案的路由器的特定实现和框架要解决的用例,则您可能会学习如何使路由器的抽象起作用,但您将不知道该如何实现。实际的漫游器工作。 例如,遵循某些命名对流或文件夹结构,您可能会生成应用程序的路由,而无需了解路由器的工作方式。

我并不是在建议不要使用抽象。 抽象非常重要。 它们消除了理解所有涉及的部分的认知开销,从而帮助我们更快地完成了健壮的事情。 您还应该考虑到,如果您非常抽象,则现在可能会移动得很快,但将来会变慢。 如果您不了解它们,则很难优化或改进它们。

变更成本

在决定我们适合多少抽象级别时,我们应该考虑的另一件事是变更的成本。 将来是否很容易将框架的某些具体实现更改为其他内容? 框架可帮助我们更快地完成强大的任务。 未来的稳健性可能会变得僵化,这意味着不灵活。

当React Router v4发布时,它在路由领域带来了一些创新,但是您不能在Next.js中使用它。 然后有人创建了After.js。 After.js自称为“使用React Router 4构建的服务器渲染的React应用的类似Next.js的框架”

现在剩下的问题是,如果我使用After.js,是否可以在将来的React Router 4中更改其他路由器? 还是我需要创建另一个框架? 也许final.js?

框架上的图书馆

我更喜欢停留在库的抽象层次上。 它使我有机会了解可以有效使用和优化的更具体的案例。 同样,由于处于库的抽象级别,因此更容易适应变化,我认为这在繁忙的技术行业中非常重要。

库比框架更多,并且库并不总是具有一致的文档。 我认为处于库的抽象级别需要更多的奉献精神,也许不是每个人都有动机。 因此,这可能是个人决定。

要考虑的另一个重要因素是您和您的团队的经验和知识。 如果您的团队没有足够的时间来掌握正确的库,使用正确的库解决正确的问题可能对您不起作用。 我有偏见,但在这种情况下,我会帮助团队提供高质量的强化培训,例如我们our

结论

  • 服务器端渲染可以提高Web应用程序的性能和用户体验。
  • React具有对服务器端渲染的内置支持。
  • Create React App不支持SSR,但是您可以使用react-scripts-ssr轻松将具有CSR和SSR功能的CRA组合在一起,您可以随时选择退出。
  • 您需要根据要解决的问题和现在拥有的知识,以及将来打算拥有的知识库,来确定应该在哪个抽象层次上进行学习。
  • 您可以使用Next.js或After.js之类的框架来创建SSR React应用,但是您应该考虑学习的成本和变更的成本。