实现一个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()结合判断.. 一脸懵逼
相关文章
相关标签/搜索