实现一个 SSR 同构应用
纸上得来终觉浅,我们来实现一个简易的服务端渲染流程,意在体会 SSR 带来的红利
页面源码来自 React 状态管理与同构实战
几个重要的概念
实现 SSR
是依靠 React
提供的 ReactDomServer
对象
它主要提供了只能在服务端使用的 renderToString()
与 renderToStaticMarkup()
方法
renderToString()/renderToStaticMarkup()
使用方法: ReactDomServer.renderTostring(element)
/ReactDomServer.renderToStaticMarkup(element)
共同点:
- 都接收一个 React Element 并将此 Element 转化为 HTML 字符串,通过浏览器返回,实现了在服务端将页面拼接字符串插入 HTML 文档中并返回给浏览器 完成初步服务端渲染的目的
不同点
- renderToString(注:React 15) 生成的 HTML 字符串的每个 Dom 节点都有
data-react-id
属性,根节点会有一个data-react-checkSum
属性 - renderToStaticMarkup 不带
data-react-checkSum
属性 浏览器渲染时必会重新渲染组件
关于 data-react-checkSum:
1 | 如果两个组件有相同的props和Dom结构,这个值是一样的 |
这里有一张草图能大概描述这个过程嘤嘤嘤.
ReactDom.hydrate()
React 16 以后通过renderToString
渲染的组件不再带有 data-react-*
属性,因此浏览器端的渲染方式无法简单通过 data-react-checksum
来判断是否需要重新渲染
基于这样儿的背景下 ReactDom
提供了一个新的 API ReactDom.hydrate()
用法同 render()
在浏览器端渲染组件
当然,react 是向下兼容的,浏览器端在渲染组件时使用 render () 仍然没有问题,但不论是面向未来,还是基于性能的考虑,都应该采用更好的模式
renderToNodeStream()/renderToStaticNodeStream()
React 16 为了优化页面的初始加载速度缩短 TTFB 时间,提供了这两个方法
概念
该方法持续产生子节流 返回 Readable stream
最终通过流形式返回的 HTML 字符串
这样 服务端处理内容时是实时向浏览器端传输数据而不是一次性处理完成后才开始返回结果的
renderToStaticNodeStream
之于 renderToNodeStream
也是不会产生 data-react-*
属性,对于静态页面 可以采用此方法。
实际开发中可能存在的问题
- 服务端不存在支持组件挂载的浏览器环境,所以 react 组件只有
componentDidMount
之前的生命周期方法有效,所以在其之前的生命周期方法中不能用到浏览器的特性,比如window、localStorage
. - 双端可能都有拉取数据的需求,所以为了实现代码的复用,一种典型的做法就是把请求数据的逻辑放到 React 组件的静态方法中 然后双端共用,双端请求方法不一致的问题可以通过服务端与浏览器端的判断来封装一下 比如根据 window 是浏览器特有对象
React 16 在服务端渲染上的惊喜
前面也有混杂说过,在此总结一下
- 在浏览器渲染组件需要配合服务端使用
hydrate
方法 - 提供了
stream
方式的接口 - 与浏览器的新特性相似,除了能处理
React Element
也能处理别的类型,比如string number
- 因为在返回结果 Dom 中废除了
data-react-checksum
等属性,所以服务端生成 HTML 更加高效 - 允许在渲染 Dom 中加入非标准 Dom 属性
好了 测试一下,基于 Node.js 实现一个小 demo
Express4.15.3 进行服务端处理
browser: 浏览器端渲染
server:服务端逻辑
share:同构的部分
运行效果:
share/app.js
1 | import React, { Component } from "react"; |
browser/index.js
1 | import React from "react"; |
server/index.js
1 | import express from "express"; |
server 端:使用 renderToString
生成的字符串,使用 res.send
发送给浏览器
client 端: id 为 root 的 Dom 节点就来自服务端返回的结果,用了 React.hydrate
完成了浏览器端的逻辑处理部分
假设一 client 端渲染仍然使用 render ()
测试
1 | import React from "react"; |
结果
由于实现了向下兼容,所以是可以的,但是会给如下警告⚠️
结论 尽量使用新特性
假设二 完全依赖服务端渲染会发生什么
测试
将 browser/index.js
代码注释掉
结果
页面正常显示,但是点击按钮没有不会弹窗
结论 需要双端一起完成页面的展示与交互
假设三 使用 React 16 renderToNodeStream 渲染
测试 更改 server/index.js
1 |
|
说明: 为了配合返回一个流,使用 res.write
方法代替先前的 res.end
好处
使用 renderToString
页面 TTFB 时间
使用 renderToNodeStream
页面 TTFB 时间
结论
采用渐进式流渲染可以最大限度的缩短服务器响应水间,从而使浏览器可以更快的接收到信息
假设三 同构应用与浏览器渲染优势对比
浏览器渲染:
同构应用:
假设三 react16 比 react15 渲染更加高效
React 15
React 16
遗留问题
- 鉴于
renderToNodeStream()/renderToStaticNodeStream()
与renderToString()/renderToStaticMarkup()
React 16 之后都不存在data-react-*
了 双方还有什么区别? - react 16 之后 如何做双端对比? 官方说是根据
ReactDom.hydrate()
与renderToString()
结合判断.. 一脸懵逼