實現一個SSR同構應用

紙上得來終覺淺,咱們來實現一個簡易的服務端渲染流程,意在體會SSR帶來的紅利css

頁面源碼來自React狀態管理與同構實戰html

幾個重要的概念

實現SSR是依靠React提供的ReactDomServer對象react

它主要提供了只能在服務端使用的renderToString()renderToStaticMarkup()方法express

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-checkSumbash

若是兩個組件有相同的props和Dom結構,這個值是同樣的

咱們知道 服務端渲染完頁面內容難過以後,瀏覽器端也會渲染以完成組件的交互等能力,瀏覽器端會生成組件的data-react-checkSum值 而後跟服務端渲染組件的值作對比,若是相等,則再也不重複渲染
複製代碼

這裏有一張草圖能大概描述這個過程嚶嚶嚶.服務器

cache_detai

ReactDom.hydrate()

React 16之後經過 renderToString渲染的組件再也不帶有data-react-*屬性,所以瀏覽器端的渲染方式沒法簡單經過data-react-checksum來判斷是否須要從新渲染app

基於這樣兒的背景下ReactDom提供了一個新的API ReactDom.hydrate() 用法同render()在瀏覽器端渲染組件dom

固然,react是向下兼容的,瀏覽器端在渲染組件時使用render()仍然沒有問題,但不管是面向將來,仍是基於性能的考慮,都應該採用更好的模式svg

renderToNodeStream()/renderToStaticNodeStream()

React 16 爲了優化頁面的初始加載速度縮短TTFB時間,提供了這兩個方法

概念

該方法持續產生子節流 返回 Readable stream 最終經過流形式返回的HTML字符串 這樣 服務端處理內容時是實時向瀏覽器端傳輸數據而不是一次性處理完成後纔開始返回結果的

renderToStaticNodeStream 之於 renderToNodeStream 也是不會產生data-react-*屬性,對於靜態頁面 能夠採用此方法。

實際開發中可能存在的問題

  1. 服務端不存在支持組件掛載的瀏覽器環境,因此react組件只有componentDidMount以前的生命週期方法有效,因此在其以前的生命週期方法中不能用到瀏覽器的特性,好比 window、localStorage.
  2. 雙端可能都有拉取數據的需求,因此爲了實現代碼的複用,一種典型的作法就是把請求數據的邏輯放到React組件的靜態方法中 而後雙端共用,雙端請求方法不一致的問題能夠經過服務端與瀏覽器端的判斷來封裝一下 好比根據window是瀏覽器特有對象

React 16 在服務端渲染上的驚喜

前面也有混雜說過,在此總結一下

  • 在瀏覽器渲染組件須要配合服務端使用hydrate方法
  • 提供了stream方式的接口
  • 與瀏覽器的新特性類似,除了能處理React Element 也能處理別的類型,好比string number
  • 由於在返回結果Dom中廢除了data-react-checksum等屬性,因此服務端生成HTML更加高效
  • 容許在渲染Dom中加入非標準Dom屬性

好了 測試一下,基於Node.js實現一個小栗子

Express4.15.3 進行服務端處理

ssrvs1

  • browser: 瀏覽器端渲染
  • server:服務端邏輯
  • share:同構的部分

運行效果:

ssr

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完成了瀏覽器端的邏輯處理部分

假設一 client端渲染仍然使用render()

測試

import React from "react";
import {render } from "react-dom";
import App from "../shared/App";

render(<App />, document.getElementById("root"));

複製代碼

結果 因爲實現了向下兼容,因此是能夠的,可是會給以下警告⚠️

hydrate_to_rende

結論 儘可能使用新特性

假設二 徹底依賴服務端渲染會發生什麼

測試browser/index.js代碼註釋掉 結果 頁面正常顯示,可是點擊按鈕沒有不會彈窗 結論 須要雙端一塊兒完成頁面的展現與交互

假設三 使用React 16 renderToNodeStream渲染

測試 更改 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時間

TTFB2

使用renderToNodeStream頁面TTFB時間

TTFB1

結論 採用漸進式流渲染能夠最大限度的縮短服務器響應水間,從而使瀏覽器能夠更快的接收到信息

假設三 同構應用與瀏覽器渲染優點對比

瀏覽器渲染:

client_rende

同構應用:

ss

假設三 react16比react15渲染更加高效

React 15

react15_rende
React 16
react16

遺留問題

  1. 鑑於renderToNodeStream()/renderToStaticNodeStream()renderToString()/renderToStaticMarkup() React 16以後都不存在data-react-*了 雙方還有什麼區別?
  2. react 16以後 如何作雙端對比? 官方說是根據ReactDom.hydrate()renderToString()結合判斷.. 一臉懵逼
相關文章
相關標籤/搜索