纸上得来终觉浅,咱们来实现一个简易的服务端渲染流程,意在体会SSR带来的红利css
页面源码来自React状态管理与同构实战html
实现SSR
是依靠React
提供的ReactDomServer
对象react
它主要提供了只能在服务端使用的renderToString()
与renderToStaticMarkup()
方法express
使用方法: ReactDomServer.renderTostring(element)
/ ReactDomServer.renderToStaticMarkup(element)
浏览器
data-react-id
属性,根节点会有一个data-react-checkSum
属性data-react-checkSum
属性 浏览器渲染时必会从新渲染组件关于data-react-checkSum:bash
若是两个组件有相同的props和Dom结构,这个值是同样的
咱们知道 服务端渲染完页面内容难过以后,浏览器端也会渲染以完成组件的交互等能力,浏览器端会生成组件的data-react-checkSum值 而后跟服务端渲染组件的值作对比,若是相等,则再也不重复渲染
复制代码
这里有一张草图能大概描述这个过程嘤嘤嘤.服务器
React 16之后经过 renderToString
渲染的组件再也不带有data-react-*
属性,所以浏览器端的渲染方式没法简单经过data-react-checksum
来判断是否须要从新渲染app
基于这样儿的背景下ReactDom
提供了一个新的API ReactDom.hydrate()
用法同render()
在浏览器端渲染组件dom
固然,react是向下兼容的,浏览器端在渲染组件时使用render()仍然没有问题,但不管是面向将来,仍是基于性能的考虑,都应该采用更好的模式svg
React 16 为了优化页面的初始加载速度缩短TTFB时间,提供了这两个方法
该方法持续产生子节流 返回 Readable stream
最终经过流形式返回的HTML字符串 这样 服务端处理内容时是实时向浏览器端传输数据而不是一次性处理完成后才开始返回结果的
renderToStaticNodeStream
之于 renderToNodeStream
也是不会产生data-react-*
属性,对于静态页面 能够采用此方法。
componentDidMount
以前的生命周期方法有效,因此在其以前的生命周期方法中不能用到浏览器的特性,好比 window、localStorage
.前面也有混杂说过,在此总结一下
hydrate
方法stream
方式的接口React Element
也能处理别的类型,好比string number
data-react-checksum
等属性,因此服务端生成HTML更加高效好了 测试一下,基于Node.js实现一个小栗子
Express4.15.3 进行服务端处理
运行效果:
share/app.js
import React, { Component } from "react";
import logo from "./logo.svg";
import "./App.css";
class App extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
alert('我被触发辣')
}
render() {
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React in the Server</h2>
</div>
<p className="App-intro">点击按钮体验</p>
<button onClick={e => this.handleClick()}> 我是按钮 </button>
</div>
);
}
}
export default App;
复制代码
browser/index.js
import React from "react";
import { hydrate } from "react-dom";
import App from "../shared/App";
hydrate(<App />, document.getElementById("root"));
复制代码
server/index.js
import express from "express";
import React from "react";
import { renderToString } from "react-dom/server";
import App from "../shared/App";
const app = express();
app.use(express.static("public"));
app.get("*", (req, res) => {
const htmlMarkup = renderToString(<App />);
res.send(`
<!DOCTYPE html>
<head>
<title>Universal Reacl</title>
<link rel="stylesheet" href="/css/main.css">
<script src="/bundle.js" defer></script>
</head>
<body>
<div id="root">${htmlMarkup}</div>
</body>
</html>
`);
});
app.listen(process.env.PORT || 3000, () => {
console.log("Server is listening");
});
复制代码
server端:
使用 renderToString
生成的字符串,使用res.send
发送给浏览器
client端:
id为root的Dom节点就来自服务端返回的结果,用了React.hydrate
完成了浏览器端的逻辑处理部分
测试
import React from "react";
import {render } from "react-dom";
import App from "../shared/App";
render(<App />, document.getElementById("root"));
复制代码
结果 因为实现了向下兼容,因此是能够的,可是会给以下警告⚠️
结论 尽可能使用新特性
测试 将browser/index.js
代码注释掉 结果 页面正常显示,可是点击按钮没有不会弹窗 结论 须要双端一块儿完成页面的展现与交互
测试 更改 server/index.js
import express from "express";
import React from "react";
import { renderToNodeStream } from "react-dom/server";
import App from "../shared/App";
const app = express();
app.use(express.static("public"));
app.get("*", (req, res) => {
res.write(`
<!DOCTYPE html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Universal Reacl</title>
<link rel="stylesheet" href="/css/main.css">
<script src="/bundle.js" defer></script>
</head>`
);
res.write("<div id='root'>");
const stream = renderToNodeStream(<App/>);
stream.pipe(res, { end: false });
stream.on('end', () => {
res.write("</div></body></html>");
res.end();
});
});
复制代码
说明: 为了配合返回一个流,使用res.write
方法代替先前的res.end
好处 使用renderToString
页面TTFB时间
使用renderToNodeStream
页面TTFB时间
结论 采用渐进式流渲染能够最大限度的缩短服务器响应水间,从而使浏览器能够更快的接收到信息
浏览器渲染:
同构应用:
React 15
renderToNodeStream()/renderToStaticNodeStream()
与 renderToString()/renderToStaticMarkup()
React 16以后都不存在data-react-*
了 双方还有什么区别?ReactDom.hydrate()
与renderToString()
结合判断.. 一脸懵逼