all articles

try React SSR the simplest way

2018-12-09 @sunderls

js react ssr

最近的项目终于要用到了 React SSR 了。

为了尽量不要前置假设而用了一些不必要的库,先从最基本的实现一步一步学一下。

基本 SSR

React 提供了将 element 输出为 html 的方法,理论上用这个就够了。

一个问题是在服务器端渲染的话,没有 lifecycle,数据的处理上需要好好考虑一下。

ReactDOMServer

ReactDOMServer 用来输出 String

const ReactDOMServer = require("react-dom/server");
const React = require("react");

// 这段代码输出一个红色的p标签
ReactDOMServer.renderToString(
  React.createElement(
    "p",
    {
      style: {
        color: "red"
      }
    },
    "hello world"
  )
);

输出的 html 如下

<p style="color:red" data-reactroot="">hello world</p>

类似的用renderToStaticMarkup可以输出纯净的 html

<p style="color:red">hello world</p>

也就是说,如果不需要前端的配合的话,用 markup,否则还是用 renderToString 比较好。

如何用 jsx

嗯,这个只能提前 build 了把,比如

/tsx
   componentA.tsx
   componentB.tsx
/js
   componentA.js
   compoenntB.js

把/tsx 的 component 转换为/js,然后在 node.js require js 的内容即可

如果不想 build,而是动态 build,那就用@babel/register,这个插件可以 hook 到 node.js 的 require。

具体可以看这里:https://babeljs.io/docs/en/babel-register

require("@babel/register")({
  presets: ["@babel/preset-env", "@babel/preset-react"]
});

但是 on the fly 的性能太差了吧,还是提前 build 好比较好

SSR 的 html 和前端的交互如何处理?

ReacDOM.hydrate可以解决这个问题。

ReactDOM.hydrate(element, container[, callback])

记得 ReactDOM.render(), 参数是一样的,只不过 hydrate 不会从 0 替换 dom,而相信这个 dom,重复利用他。 这样 SSR 得到的 html 可以立马显示给用户,提供更快的 fisrt render。

lifecyle 和数据处理

SPA 的话,针对数据显示大概是这个流程:

  1. component 初始化
  2. componentDidMount 触发
  3. fetch api
  4. setState 触发数据更新
  5. dom 完成更新

如果加入 SSR,只需要统一 componentDidMount 中的数据获取就行了。

我们可以参考一下next.js得到一下解决方案

  1. component 都加一个 static getInitialData 方法
  2. constructor 中调用 getInitialData 然后初始化
  3. SSR 的时候也调用 getInitialData,然后把 data 传递给 component
  4. 输出 string,前端 js 中 hydrate,完毕。

someComponent.js

const React = require("react");
const { Component } = require("react");

export default class SomeComponent extends Component {
  constructor(props) {
    super(props);
    // 如果有props的data,就用之
    // 否则调用init方法获取
    const data = props.initialData || SomeComponent.getInitialData();
    this.state = data;
  }

  // 一个统一的init方法,为啥static是避免在node.js创建instance
  static getInitialData() {
    return {
      color: "red"
    };
  }

  render() {
    return <p style={this.state}>{this.props.children}</p>;
  }
}

node.js 里面就这样写

const SomeComponent = require("./component.jsx").default;
ReactDOMServer.renderToStaticMarkup(
      React.createElement(
        SomeComponent,
        {
          initialData: SomeComponent.getInitialData()
        },
        "hello world"
      )
    )
  )

client js 里面这么写

import React from "react";
import ReactDOM from "react-dom";
import SomeComponent from "./someComponent.js";

ReactDOM.hydrate(
  <SomeComponent>hello world</SomeComponent>,
  document.querySelector("#root")
);

简单易懂。

有一个问题是 SomeComponent 的 constructor 里面的话,就的是 sync 的,如果在 getInitialData 中进行了 API 调用,就会出现问题。

async 问题

如何解决呢?client 这边肯定是不能做到 sync 的,server 这边肯定需要做到 sync 来提高相应速度。

所以 component 中要做一定的 convention: initialData 的初始化放在 constructor 和 componentDidMount 中。

component

const React = require("react");
const { Component } = require("react");

export default class SomeComponent extends Component {
  constructor(props) {
    super(props);
    this.state = props.initialData || {};
  }

  static async getInitialData() {
    return await Promise.resolve({ color: "red" });
  }

  async componentDidMount() {
    // 如果props中没有data,在这里初始化
    if (!this.props.initialData) {
      this.setState(await SomeComponent.getInitialData());
    }
  }

  render() {
    return <p style={this.state}>{this.props.children}</p>;
  }
}

node.js

// express相关代码省略
const SomeComponent = require("./src/someComponent.js").default;

app.get("/", async (req, res) => {
  // 根据请求await 数据
  const initialData = await SomeComponent.getInitialData();
  // 结束后返回
  res.send(`
    <div id="root">${ReactDOMServer.renderToStaticMarkup(
      React.createElement(
        SomeComponent,
        {
          initialData
        },
        "hello world"
      )
    )}</div>
    <script src="/main.js"></script>
    `);
});

总结

简单的 ssr 还是非常简单的,自己就可以实现。 当然实际开发过程中还会遇到问题,这个我后面再总结分享了