React 同构

282 阅读4分钟

这是我参与更文挑战的第18天,活动详情查看: 更文挑战

前言

React 同构,需要实现以下功能:

  • 在服务端根据 React 组件生成 html
  • 数据脱水和注水
  • 服务器端管理Redux Store
  • 支持服务器和浏览器获取共同数据源

React服务器端渲染HTML

React在服务器端渲染使用函数和浏览器端不一样:

import ReactDOMServer from "react-dom/server';

const appHtml = ReactDOMServer.renderToString(<RootComponent />)

renderToString 函数的返回结果就是一个HTML字符串,至于这个字符串如何处理,要由开发者来决定。

当然,只是把 renderToString 返回的字符串在浏览器中渲染出来,用户看到的只是纯静态的 HTML 而已,并不具有任何动态的交互功能。要让渲染的HTML“活”起来,还需要浏览器端执行 JavaScript 代码。

服务器端渲染产生的React组件HTML被下载到浏览器网页之中,浏览器网页需要使用render函数重新渲染一遍React组件。

如果服务器端渲染和浏览器端渲染产生的内容不一样,用户会先看到服务器端渲染的内容,随后浏览器端渲染会重新渲染内容,用户就会看到一次闪烁,这样给用户的体验很不好。

为了让两端数据一致,就要涉及 脱水注水 的概念。

脱水和注水

服务器端渲染产出了HTML,但是在交给浏览器的网页中不光要有HTML,还需要有 脱水数据,也就是在服务器渲染过程中给React组件的输入数据,这样,当浏览器端渲染时,可以直接根据“脱水数据”来渲染React组件,这个过程叫做 注水

脱水数据的传递方式一般是在网页中内嵌一段 JavaScript,内容就是把传递给React组件的数据赋值给某个变量,这样浏览器就可以直接通过这个变量获取脱水数据。

需要注意的是,使用脱水数据要防止跨站脚本攻击(XSSAttack),因为脱水数据有可能包含用户输入的成分,而用户的输入谁也保不准包含什么。

服务器端Redux Store

在服务器端使用Redux,必须要对每个请求都创造一个新的Store,这是和浏览器渲染的最大区别。

支持服务器和浏览器获取共同数据源

很明显,最简单的方法,就是有一个API服务器提供接口让服务器和浏览器都能够访问,这样无论是什么样的场景,服务器和浏览器获得数据都是一致的。

服务器端路由

因为浏览器端使用了 React-Router 作为路由,没有理由不在服务器端使用一致的方法,不过在服务器端使用React-Router 的方式和浏览器端不一样,在浏览器端,整个 Router 作为一个 React 组件传递一个 ReactDOM的 render 函数,Router 可以自动和URL同步,但是对于服务器端的过程,URL对应到路由规则的过程需要用 match 函数:

import { match, RouterContext } from "react-router";

match({routes: routes, location: requestUrl }, function(err, redirect, renderProps){

    if (err) {
        return res.status(500).send(err.message);
    }
    if (redirect) {
        return res.redirect(redirect.pathname + redirect.search);
    }
    if (!renderProps) {
        return res.status(404).send("Not Found");
    }
    
    const appHtml = ReactDOMServer.renderToString(<RouterContext {...renderProps} />);
});

match 函数接受一个对象和一个回调函数作为参数,对象参数中的 routes 就是 Route 构成的路由规则树,这里根本用不上 Router 类,所以也用不上 Router 的 history 属性,这就是和浏览器端渲染的最大区别。match是通过对象参数中的 location 字段来确定路径的,不是靠和浏览器地址栏关联的 history。

当 match 函数根据 location 和 routes 匹配完成之后,就会调用第二个回调函数参数,根据回调函数第一个参数err和第二个参数 redirect 可以判断匹配是否错误或者是一个重定向。一切顺利的话,第一第二个参数都是空,有用的就是第三个参数 renderProps,这个 renderProps 包含路由的所有信息,把它用扩展操作符展开作为属性传递给 RouterContext 组件,渲染的结果就是服务器端渲染产生的HTML字符串。