这⼀次彻底弄懂:React 服务端渲染
优采云 发布时间: 2022-07-01 13:17这⼀次彻底弄懂:React 服务端渲染
本文字数:21279字
预计阅读时间:54分钟
1、前言
在前端项目需要首屏渲染速度优化或SEO的场景下,大家或多或少都听过到过服务端渲染( SSR ),但大多数人对服务端渲染具体实现和底层原理还是比较陌生的。本文基于公司官网开发时做了服务端渲染改造基础上,系统理解和梳理这套体系的模式和逻辑,并写了一些笔记和Demo(文后链接)便于深入理解。这次我们来以React为例,把服务端渲染彻底讲弄明白。本文主要有以下内容:
1.1 什么是服务端渲染?
服务端渲染, SSR (Server-side Rendering) ,顾名思义,就是在浏览器发起页面请求后由服务端完成页面的HTML结构拼接,返回给浏览器解析后能直接构建出有内容的页面。
用 node 实现一个简单的 SSR
我们使用Koa框架来创建node服务:
// demo1<br />var Koa = require("koa");<br />var app = new Koa();<br /><br />// 对于任何请求,app将调用该函数处理请求:<br />app.use(async (ctx) => {<br /> // 将HTML字符串直接返回 <br /> ctx.body = `<br /> <br /> <br /> ssr<br /> <br /> <br /> <br /> hello server<br /> <p>word<br /> <br /> <br /> `;<br />});<br />//*敏*感*词*<br />app.listen(3001, () => {<br /> console.log("listen on 3001 port!");<br />});<br /></p>
启动服务后访问页面,查看网页源代码是这样:
npx create-react-app my-app<br />
上面的例子就是一个简单的服务端渲染,其服务侧直接输出了有内容的HTML,浏览器解析之后就能渲染出页面。与服务端渲染对应的是客户端渲染 ,CSR(Client Side Rendering),通俗的讲就是由客户端完成页面的渲染。其大致渲染流程是这样:在浏览器请求页面时,服务端先返回一个无具体内容的HTML,浏览器还需要再加载并执行JS,动态地将内容和数据渲染到页面中,才能完成页*敏*感*词*体内容的显示。目前主流的React ,Vue, Angular 等SPA页面未经特殊处理均采用客户端渲染。最常见脚手架create-react-app 生成的项目就是客户端渲染:
上面采用客户端渲染的HTML页面中中无内容,需在浏览器端加载并执行bundle.js后才能构建出有内容页面。
1.2 为什么用服务端渲染?1.2.1 服务端渲染的优势
相比于客户端渲染,服务端渲染有什么优势?我们可以从下图对比一下这两种不同的渲染模式。
首屏时间更短
采用客户端渲染的页面,要进行JS文件拉取和JS代码执行,动态创建 DOM 结构,客户端逻辑越重,初始化需要执行的 JS 越多,首屏性能就越慢;客户端渲染前置的第三方类库/框架、polyfill 等都会在一定程度上拖慢首屏性能。Code splitting、lazy-load等优化措施能够缓解一部分,但优化空间相对有限。相比而言,服务端渲染的页面直接拉取HTMl就能显示内容,更短的首屏时间创造更多的可能性。
利于SEO
在别人使用搜索引擎搜索相关的内容时,你的网页排行能靠得更前,这样你的流量就有越高,这就是SEO的意义所在。那为什么服务端渲染更利于爬虫爬你的页面呢?因为对于很多搜索引擎爬虫(非google)HTML返回是什么内容就爬什么内容,而不会动态执行JS代码内容。对客户端渲染的页面来说,简直无能为力,因为返回的HTML是一个空壳。而服务端渲染返回的HTML是有内容的。
SSR的出现,就是为了解决这些CSR的弊端。
1.2.2 权衡使用服务端渲染
并不是所有的WEB应用都必须使用SSR,这需要开发者来权衡,因为服务端渲染会带来以下问题:
代码复杂度增加。为了实现服务端渲染,应用代码中需要兼容服务端和客户端两种运行情况,部分代码只能在客户端运行,需要对其进行特殊处理,才能在服务器渲染应用程序中运行。
需要更多的服务器资源。由于服务器增加了渲染HTML的需求,使得原本只需要输出静态资源文件的nodejs服务,新增了数据获取的IO和渲染HTML的CPU占用,如果流量突然暴增,有可能导致服务器宕机,因此需要使用响应的缓存策略和准备相应的服务器负载。
涉及构建设置和部署的更多要求。与可以部署在任何静态文件服务器上的完全静态单页面应用程序 (SPA) 不同,服务器渲染应用程序,需要处于 Node.js server 运行环境。
因此,在使用服务端渲染SSR之前,需要考虑投入产出比:是否真的需要SEO,是否需要将首屏时间提升到极致。如果都没有,使用SSR反而小题大做了。
1.3 服务端渲染的发展史
其实服务端渲染并不是什么新奇的概念,前后端分层之前很长的一段时间里都是以服务端渲染为主(JSP、PHP),那时后端一把*敏*感*词*,在服务端生成完整的 HTML 页面。但那时的服务端渲染和现在还是有本质的区别,存在比较多的弊端,每一个请求都要动态生成HTML,存在大量的重复,服务器机器成本也相对比较高,前后端代码完全掺杂在一起,开发维护难。
随着业务不断发展变化,后端要写的JS逻辑也越发复杂,而且JS有很多潜在的坑使后端越发觉得这是块烫手山芋,于是逐渐出现了前后端分层。伴随AJAX的兴起,浏览器可以做到了不再重现请求页面就可更新局部视图。还可以利用客户端免费的计算资源,后端侧逐渐演变成了提供数据支持。jquery的兴起,良好的客户端兼容性使JS不再受困于各种版本浏览器兼容问题,一统了前端天下。
此后伴随node的兴起,前后端分离越演越烈。前端能摆脱后端的依赖单独起服务,三大框架vue,react,angular也迅势崛起,以操作数据就能更新视图,前端开发人员逐渐摆脱了与烦人的Dom操作打交道,能够专心的关注业务和数据逻辑。前端同时探索出了功能插件,UI库,组件等多种代码复用方案,形成了繁荣的前端生态。
但是三大框架采用客户端渲染模式,随着代码逻辑的加重,首屏时间成了一个很大的问题,同时开发人员也发现SEO也出了问题,大多搜索引擎根本不会去执行JS代码。但是也不可能再回头走老路,于是前端又探索出了一套服务端渲染的框架来解决掉这些问题。此时的服务端渲染是建立在成熟的组组件,模块生态之上,基于Node.js的同构方案成为最佳实践。
2、React服务端渲染的原理
2.1基本思路
React服务端渲染流程
React服务端渲染的基本思路,简单理解就是将组件或页面通过服务器生成html字符串,再发送到浏览器,最后将静态标记"混合"为客户端上完全交互的应用程序。因为要考虑React在服务端的运行情况,故相比之前讲的多了在浏览器端绑定状态与事件的过程。
我们可以结合下面的流程图来一览完整的 React服务端渲染的全貌:当浏览器去请求一个页面,前端服务器端接收到请求并执行 React组件代码,此时React代码中可能包含向后端服务器发起请求,待请求完成返回的数据后,前端服务器组装好有内容的HTML里返给浏览器,浏览器解析HTML后已具备展示内容,但页面并不具备交互能力。
下一阶段,在返回的HTMl中还有script链接,浏览器再拉取JS并执行其包含的React 代码,其能在浏览器端执行完整的生命周期,并通过相关API实现复用此前返回 HTML节点并添加事件的绑定,此时页面才就具备完全交互能力。总的来说,react服务端渲染包含有两个过程:服务端渲染 + 客户端 hydrate 渲染。服务端渲染在服务端渲染出了首屏内容;客户端 hydrate 渲染复用服务端返回的节点,进行一次类似于 render 的 hydrate 渲染过程,把交互事件绑上去(此时页面可交互),并接管页面。
服务端处理后返回的
客户端“浸泡”还原后的
核心思想(同构)
从上面的流程中可以看到,客户端和服务端都要执行React代码完成渲染,那是不是就要写两份代码,供双端使用? 当然不需要,也完全不合理。所谓同构,就是让一份React代码,既可以在服务端中执行,也可以在客户端中执行。
SSR技术栈
我们这里简单理了一下服务端渲染涉及到的技术栈:
知道了服务端渲染、同构的大概思路之后,下面从头开始,一步一步完成具体实践,深入理解其原理。
2.2 服务端如何渲染React组件?
按照之前流程的大概思路,我们首先需要将React组件在服务端转换成HTML字符串,那怎么做呢?React 提供的面向服务端的API(react-dom/server),提供了相关方法能够将 React 组件渲染成静态的(HTML)标签。下面我们简单了解下react-dom/server。
react-dom/server
react-dom/server有renderToString、renderToStaticMarkup,renderToNodeStream、renderToStaticNodeStream四个方法能够将 React 组件渲染成静态的(HTML)标签,前两者能在客户端和服务端运行,后两者只能在服务端运行。
renderToStaticMarkup VS renderToString:renderToString 方法会在 React 内部创建的额外 DOM 属性,例如 data-reactroot, 在相邻文本节点之间生成,这些属性是客户端执行hydrate复用节点的关键所在,data-reactroot属性是服务端渲染的标志之一。如果你希望把 React 当作静态页面*敏*感*词*来使用,renderToStaticMarkup方法会非常有用,因为去除额外的属性可以节省一些字节。
// Home.jsx<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" />import React from "react";<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" />const Home = () => {<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> return (<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> <br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> console.log("hello")}>This is Home Page<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> <p>Home is the page ..... more discribe<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> <br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> );<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" />};<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" />export default Home;</p>
我们使用React-dom/server下提供的renderToString方法,在服务端将其转换为html字符串:
// server.js<br />import Koa from "koa";<br />import React from "react";<br />import { renderToString } from "react-dom/server";<br />import Home from "./containers/Home";<br /><br />const app = new Koa();<br />app.use(async (ctx) => {<br /> // 核心api renderToString 将react组件转化成html字符串<br /> const content = renderToString();<br /> ctx.body = `<br /> <br /> <br /> ssr<br /> <br /> <br /> ${content}<br /> <br /> <br /> `;<br />});<br />app.listen(3002, () => {<br /> console.log("listen:3002");<br />});<br />
可以看到上面代码里有ES6的import 和jsx语法,不能直接运行在node环境,需要借助webpack打包, 构建目标是commonjs。新建webpack.server.js具体配置如下:
// webpack.server.js<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" />const path = require("path");<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" />const nodeExternals = require("webpack-node-externals");<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" />module.exports = {<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> mode: "development",<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> target: "node",<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> entry: "./server.js",<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> resolve: {<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> extensions: [".jsx", ".js", ".tsx", ".ts"],<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> },<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> module: {<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> rules: [<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> {<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> test: /\.jsx?$/,<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> loader: "babel-loader",<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> exclude: /node_modules/,<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> options: {<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> presets: ["@babel/preset-react", "@babel/preset-env"],<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> plugins: [<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> [<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> "@babel/plugin-transform-runtime",<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> {<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> absoluteRuntime: false,<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> corejs: false,<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> helpers: true,<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> regenerator: true,<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> version: "7.0.0-beta.0",<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> },<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> ],<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> ],<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> },<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> },<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> ],<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> },<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> output: {<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> filename: "bundle.js",<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> path: path.resolve(__dirname, "build"),<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> },<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> externals: [nodeExternals()],<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" />};
在webpack构建完成后,可在Node环境运行build/bundle.js,访问页面后查看网页源代码,可以看到,React组件中的内容已经完整地包含在服务端返回到html里面。我们成功迈出了服务端渲染第一步。此时,我们也有必要再深入了解renderToString 到底做了什么,提前踩坑!
renderToString
除了将React组件转换成html字符串外,renderToString还有做了下面这些:
1. 会执行传入的React组件的代码,但是其只执行到React生命周期初始化过程的render及之前,即下面红框的部分,其余大部分生命周期函数在服务端都不执行;这也是服务端渲染的坑点之一。
2.renderToString 生成的产物中会包含一些额外生成特殊标记,代码体积会有所增大,其中属性data-reactroot是服务端渲染的标志,便于后续客户端通过hydrate复用HTML节点。在React16前后其产物也有差距:在React 16 之前,服务端渲染采用的是基于字符串校验和(string checksum)的 HTML 节点复用方式, 会额外生成生成data-reactid、data-react-checksum等属性;React 16 改用单节点校验来复用(服务端返回的)HTML 节点,不再生成data-reactid、data-react-checksum等体积占用大户,只在空白节点间多了 这样的标记。
renderToString react16前<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /><br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> Welcome to React SSR! <br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> Hello There! <br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /><br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /><br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" />// renderToString react16<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" />Welcome to React SSR! Hello There!
3.会被故意忽略掉的on开头的的属性,也就忽略掉了react代码中事件处理,这是也是坑点之一。服务端返回的html里没有处理事件点击,需要靠后续客户端js执行绑定事件。
function shouldIgnoreAttribute(<br /> name: string,<br /> propertyInfo: PropertyInfo | null,<br /> isCustomComponentTag: boolean,<br />): boolean {<br /> if (propertyInfo !== null) {<br /> return propertyInfo.type === RESERVED;<br /> }<br /> if (isCustomComponentTag) {<br /> return false;<br /> }<br /> if (<br /> name.length > 2 &&<br /> (name[0] === 'o' || name[0] === 'O') &&<br /> (name[1] === 'n' || name[1] === 'N')<br /> ) {<br /> return true;<br /> }<br /> return false;<br />}<br />
上面的例子我们可以看到React的代码里有点击事件,但点击后没有反应。需要靠后续客户端js执行绑定事件。如何实现?这就需要同构了。
2.3 实现基础的同构
前文已经大概讲了同构的概念,那为什么需要同构?之前的服务端代码在处理点击事件时故意忽略掉了这类属性,在服务端执行的生命周期也是不完整的,此时的页面是不具备交互能力的。同构,正是解决这些问题的关键,React代码在服务器上执行一遍之后,浏览器再去加载JS后又运行了一遍React代码,完成事件绑定和完整生命周期的执行,从而才能成为完全可交互页面。
react-dom:hydrate
实现同构的另一个核心API是React-dom下的hydrate,该方法能在客户端初次渲染的时候去复用服务端返回的原本已经存在的 DOM 节点,于渲染过程中为其附加交互行为(事件*敏*感*词*等),而不是重新创建 DOM 节点。需要注意是,服务端返回的 HTML 与客户端渲染结果不一致时,出于性能考虑,hydrate可以弥补文本内容的差异,但并不能保证修补属性的差异,而是将错就错;只在development模式下对这些不一致的问题报 Warning,因此必须重视 SSR HydrationWarning,要当 Error 逐个解决。
那具体实现同构?
上面这里我们提供了一个基本的架构图,可以看到,服务端运行React生成html代码我们已经基本实现,目前需要做的就是生产出客户端执行的index.js,那么这个index.js我们如何生产出来呢?
具体实践
首先新建客户端代码client.js,引入React组件,通过ReactDom.hydrate处理挂载到Dom节点, hydrate是实现复用的关键。
// client.js<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" />import React from "react";<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" />import ReactDom from "react-dom";<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" />import Home from "./containers/Home";<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /><br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" />const App = () => {<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> return ;<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" />};<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /><br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" />ReactDom.hydrate(, document.getElementById("root"));
客户端代码也需要webpack打包处理,新建webpack.client.js具体配置如下,需要注意打包输出在public目录下,后续的静态资源服务也起在了这个目录下。
// webpack.client.js<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" />const path = require("path");<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" />const resolve = (dir) => path.resolve(__dirname, "./src", dir);<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" />module.exports = {<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> mode: "development",<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> entry: "./client.js",<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> output: {<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> filename: "index.js",<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> path: path.resolve(__dirname, "public"),<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> },<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;" /> module: {<br style="white-space:pre-wrap;margin: 0px;padding: 0px;max-width: 100%;overflow-wrap: break-word !important;box-sizing: border-box !important;color: rgb(171, 178, 191);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 12px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: n