React 16 服务端渲染的新特性

时间:2022-06-19
本文章向大家介绍React 16 服务端渲染的新特性,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

React 16 终于来了!???

React 16 中有许多令人激动的新特性(最著名的是Fiber的重写),但是对我个人而言,最兴奋的还是React 16 对服务器端渲染所做的许多改进。

让我们深入了解一下在React 16 中使用新的、不同的SSR,我希望你能像我一样兴奋!

如何在React 15 中运行SSR

首先,让我们复习一下如何在React 15 中使用SSR。为了实现SSR,通常需要运行一个基于Node的web服务器,例如Express、Hapi或Koa,可以调用 renderToString方法将根组件渲染为字符串,然后写入响应:

// using Express
import { **renderToString** } from "react-dom/server"
import MyPage from "./MyPage"
app.get("/", (req, res) => {
  res.write("<!DOCTYPE html><html><head><title>My Page</title></head><body>");
  res.write("<div id='content'>");  
  **res.write(renderToString(<MyPage/>));**
  res.write("</div></body></html>");
  res.end();
});

然后,在客户端启动代码中,通知客户端使用 render()渲染在服务端生成的HTML,这与客户端渲染应用程序的方法一致:

import { render } from "react-dom"
import MyPage from "./MyPage"
render(<MyPage/>, document.getElementById("content"));

如果你的做法正确,客户端渲染器可以使用现有的服务器生成的HTML进行渲染,不需要更新DOM。

那么,在React 16 中,如何实现SSR呢?

React 16 向后兼容

React小组深刻承诺向后兼容,所以如果你的代码在React 15 中运行没有任何问题,那么,在React 16 仍然可正常运行。上一小节中的示例代码在React 15 和 React 16 中都可以正常运行。

万一在你的应用程序中使用React 16 却发现问题,请提交issue!将有助于核心团队清除React 16 版本的缺陷。

render() 变成 hydrate()

如果你将SSR从React 15 升级到React 16,在浏览器中将会看见如下警告:

这是一个有益的React警告。render() 已变成 hydrate()。

在React 16中,有两种不同的方法实现客户端渲染: render()仅用于渲染客户端内容, hydrate用于渲染服务器端标记。由于React是向下兼容的,在React 16中使用 render()渲染服务端生成的标记仍旧有效,但是需要使用 hydrate()方法来消除警告,为React 17做好准备。前文的代码片段应改写为:

import { **hydrate** } from "react-dom"
import MyPage from "./MyPage"
**hydrate**(<MyPage/>, document.getElementById("content"));

React 16 可以处理数组、字符串和数字

在React 15中,组件的 render方法必须返回一个简单的React元素。而在React 16中,客户端渲染的 render方法允许组件返回字符串、数字或一组元素组成的数组。显然,React 16服务端渲染方法 hydrate方法也支持该特性。

因此,可以使用如下方式编写组件:

class MyArrayComponent extends React.Component {
  render() {
    return [
      <div key="1">first element</div>, 
      <div key="2">second element</div>
    ];
  }
}
class MyStringComponent extends React.Component {
  render() {
    return "hey there";
  }
}
class MyNumberComponent extends React.Component {
  render() {
    return 2;
  }
}

甚至可以在顶层 renderToString方法中传入字符串、数字、组件数组:

res.write(renderToString([
      <div key="1">first element</div>, 
      <div key="2">second element</div>
    ]));
// it’s not entirely clear why you would do this, but it works!
res.write(renderToString("hey there"));
res.write(renderToString(2));

这种方式有助于消除用于包裹React组件的 divspan,有助于减小HTML文件体积。

React 16 生成更有效的HTML

说到减小HTML文件体积,React 16也从根本上减小SSR在创建HTML上的开销。在React 15中,SSR文件中的每个HTML元素都有一个 data-reactid属性,其值即是简单的递增的ID,text节点也含有 react-text和ID。查看一下代码片段看效果:

renderToString(
  <div>
    This is some <span>server-generated</span> <span>HTML.</span>
  </div>
);

在React 15中,该片段生成的HTML如下(注释便于阅读):

<div data-reactroot="" data-reactid="1" 
    data-react-checksum="122239856">
  <!-- react-text: 2 -->This is some <!-- /react-text -->
  <span data-reactid="3">server-generated</span>
  <!-- react-text: 4--> <!-- /react-text -->
  <span data-reactid="5">HTML.</span>
</div>

而在React 16中,所有的ID都从节点中移除了,HTML看起来简单很多:

<div data-reactroot="">
  This is some <span>server-generated</span> <span>HTML.</span>
</div>

不仅简洁便于阅读,而且显著地减小HTML文件体积。棒!

React 16 允许使用非标准DOM属性

在React 15中,DOM渲染严格限制HTML元素,并且移除非标准HTML属性。而在React 16中,客户端和服务端渲染均允许在HTML元素上使用非标准属性。了解更多该特性的内容,请查阅 Dan Abramov’s post on the React blog

React 16 SSR不支持的错误边界和Portal

React 16 客户端渲染有两个新特性是服务端不支持的:错误边界和Portal。了解更多内容请查询Dan Abramovd 的一篇文章 excellent post on the React blog,但是至少必须了解的是服务端不会捕获错误边界。关于Portal我目前没有查到相应的解释性的文章,但是Portal 的 API依赖DOM节点,因此无法在服务端使用。

React 16 执行不太严格的客户端检查

在React 15中,当重新渲染节点时, ReactDOM.render()方法执行与服务端生成的字符挨个比对。如果一旦有不匹配的,不论什么原因,React在开发模式下会发出警告,替换整个服务端的节点数。

在React 16中,客户端渲染使用差异算法检查服务端生成的节点的准确性。相比于React 15更宽松;例如,不要求服务端生成的节点属性与客户端顺序完全一致。当React 16的客户端渲染器检测到节点不匹配,仅仅是尝试修改不匹配的HTML子树,而不是修改整个HTML树。

通常,这种变化对用户不会有影响,调用 **ReactDOM.render()/hydrate()**React 16不会修改SSR生成的不匹配HTML。这一项性能优化意味着你需要额外确保修复在 开发模式下的所有警告。

React 16 不需要通过编译获得最佳性能

在React 15中,如果直接使用SSR,即使在 生产模式下性能也不是最优的。这是因为有大量的开发人员警告和提示,并且每个警告都类似于:

if (process.env.NODE_ENV !== "production") {
  // check some stuff and output great developer
  // warnings here.
}

不幸的是,process.env不是一个普通的JavaScript对象,从中取值非常消耗性能。因此即使 NODE_ENV被设置为 production,仅仅检测环境变量常常增加了大量的服务器渲染时间。

为了解决React 15中的这种问题,你需要编译SSR代码移除 process.env,可以使用Webpack Environment Plugin或Babeltransform-inline-environment-variables 插件。从经验来看,许多开发同学未编译服务端代码,结果SSR性能明显下降。

在React 16中,该问题已解。在React 16中只会在开始时调用一次 process.env.NODE_ENV,因此不需要编译SSR代码就可以获得最佳性能。

React 16更快

说到性能,使用React做服务端渲染的同学经常抱怨说即使使用最佳实践,大文件渲染依旧缓慢。

因此,我非常高兴地公布性能测试报告,不同Node版本上React 16性能均有大幅提升:

React 16服务端渲染比React 15快。

与React 15相比, process.env编译后,在Node 4上大约提升2.4倍,Node 6中提升3倍,Node8.4 release版本提升3.8倍。对比未编译的情况,React 16大幅提升性能。

为什么React 16服务端渲染比React 15快这么多?在React 15中,服务端和客户端渲染基本是相同的代码。意味着数据结构需要维持一个虚拟DOM,尽管调用 renderToString后vDOM很快被废弃。也就是说服务端渲染非常浪费。

在React 16,核心团队重新编写服务端渲染引擎,不会创建vDOM,因此会快很多。

警告:我的测试是通过生成巨大的DOM树,使用一个非常简单的递归响应组件。这意味着它是一个非常综合的基准,几乎肯定不能反映真实的使用情况。如果你的组件中有一大堆复杂的“渲染”方法占用了大量的CPU周期,那么React 16可能没那么快。所以,我绝对希望看到React 16 SSR得到明显改善,真实的应用可能改进不到3倍。据传,我听过一些早期采用者的看法关于 1.3x 性能提升。在你的应用程序中测试实验并找出最好的方法!

React 16 支持流

最后但并非最不重要的是,React 16现在支持直接渲染节点流。

渲染流可以减小第一个字节(TTFB)渲染时间,在文档的下一个部分生成之前,将文档的开头向下发送到浏览器。所有主流浏览器都会在服务器以这种方式流出内容时开始解析和呈现文档。

从呈现流中获得的另一个很棒的东西是响应backpressure的能力。这意味着,在实践中如果网络支持,不能接受更多的字节,渲染得到的信号与停顿渲染到堵塞清理。这意味着服务器使用更少的内存,对I/O条件更敏感,这两种情况都可以帮助服务器在充满挑战的条件下保持正常工作。

使用React 16渲染流,需要调用两个方法: renderToNodeStreamrenderToStaticNodeStream,与 renderToStringrenderToStaticMarkup相对应。新方法返回Readable。

当收到 renderTo(Static)NodeStream方法返回 Readable流,它处于暂停模式,并且还没有渲染。当调用read或pipeWritable时开始渲染,大部分Node web框架从 Writable继承响应对象,因此,一般来说,只要将 Readable发送到响应。

举个例子,从上面的例子可以用流式重写:

// using Express
import { **renderToNodeStream** } from "react-dom/server"
import MyPage from "./MyPage"
app.get("/", (req, res) => {
  res.write("<!DOCTYPE html><html><head><title>My Page</title></head><body>");
  res.write("<div id='content'>"); **const stream = renderToNodeStream(<MyPage/>);
  stream.pipe(res, { end: false });
  stream.on('end', () => {**
    res.write("</div></body></html>");
    res.end();
  **});**
});

注意,当我们管的响应对象,我们必须包括可选参数 {end:false}告诉流不自动结束响应当渲染完成。这允许我们完成HTML主体,并在流完全写入响应后结束响应。

流有一些陷阱

虽然在大多数场景中,对流的渲染应该是一种升级,但目前有一些流媒体模式不能很好地工作。

一般来说,任何使用服务器呈现模式的模式都会产生标记,需要将这些标记添加到文档中,然后才可以与流媒体基本上不兼容。其中一些示例是动态决定在前面添加到页面中的CSS的框架 向文档添加元素的标记或框架。如果你使用这些类型的框架,可能不得不使用字符串渲染。

另一种尚未在React 16中发挥作用的模式是嵌入调用 renderToNodeStream。在React 15是相当典型的使用 rendertostaticmarkup生成的页面模板和嵌入调用 rendertostring产生动态的内容,如:

res.write("<!DOCTYPE html>");
res.write(renderToStaticMarkup(
  <html>
    <head>
      <title>My Page</title>
    </head>
    <body>
      <div id="content">
        { renderToString(<MyPage/>) }
      </div>
    </body>
  </html>);

但是,如果用流式对等体替换这些呈现调用,该代码将停止工作,因为它是不可能的一 Readable流( rendertonodestream返回的)是嵌入在一个组件的元件。我希望在后续会增加!

小结

以上这些是React 16中主要的SSR变化,我希望你们和我一样兴奋。

在结束之前,我要向反应核心团队的所有成员表示衷心的感谢,感谢他们致力于使服务器端成为反应生态系统的第一部分。该团队包括(但绝不限于)Jim Sproch, Sophie Alpert, Tom Occhino, Sebastian Markbåge, Dan Abramov, Dominic Gannaway。谢谢,谢谢,谢谢!

现在让我们开始使用服务器渲染一些HTML!

感谢Sunil Pai, Dan Abramov, Alec Flett, Swarup Karavadi, Helen Weng, _ Dan Fabulich_ 检阅这篇文章。


往期精选文章

使用虚拟dom和JavaScript构建完全响应式的UI框架

扩展 Vue 组件

使用Three.js制作酷炫无比的无穷隧道特效

一个治愈JavaScript疲劳的学习计划

全栈工程师技能大全

WEB前端性能优化常见方法

一小时内搭建一个全栈Web应用框架

干货:CSS 专业技巧

四步实现React页面过渡动画效果

让你分分钟理解 JavaScript 闭包



小手一抖,资料全有。长按二维码关注京程一灯,阅读更多技术文章和业界动态。