紙上得來終覺淺,咱們來實現一個簡易的服務端渲染流程,意在體會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
React 16renderToNodeStream()/renderToStaticNodeStream()
與 renderToString()/renderToStaticMarkup()
React 16以後都不存在data-react-*
了 雙方還有什麼區別?ReactDom.hydrate()
與renderToString()
結合判斷.. 一臉懵逼