React Router 按需加载+服务器渲染的闪屏问题

伴随着React协议的『妥协』(v16采用MIT),React为项目的主体,这个在短时间内是不会改变的了,在平时使用过程当中发现了以下这个问题:

在服务器渲染的时候,刷新页面会出现闪屏的现象(白屏一闪而过)前端

做为努力最求极致的我,是不能容忍的,而这一现象是半道出现的,也就是在添加按需加载以后。要说清楚这个问题,得从React的服务器渲染开始提及,(急于寻求问题解决方案的,能够直接去文章后半部分)react

服务器渲染(SSR)基础原理

React的虚拟DOM是其可被用于服务端渲染的关键。其原理简单的来讲就是首先每一个ReactComponent 在虚拟DOM中完成渲染,而后React经过虚拟DOM来更新浏览器DOM中产生变化的那一部分。虚拟DOM做为内存中的DOM表现,为React在Node.js这类非浏览器环境下提供了可能。React能够从虚拟DOM中生成一个字符串,而不是更新真正的DOM,这使得咱们能够在客户端和服务端使用同一个React Component。git

基本设计理念

React 提供了两个可用于服务端渲染组件的函数:ReactDOMServer.renderToStringReactDOMServer.renderToStaticMarkup。 在设计用于服务端渲染的ReactComponent时须要有预见性,考虑如下方面:github

  • 选取最优的渲染函数。算法

  • 如何支持组件的异步状态。redux

  • 如何将应用的初始化状态传递到客户端。api

  • 哪些生命周期函数能够用于服务端的渲染。浏览器

  • 如何为应用提供同构路由支持。服务器

  • 单例、实例以及上下文的用法。react-router

渲染函数

render()

咱们常见的render方法,用于浏览器渲染。

import ReactDOM from 'react-dom'; ReactDOM.render( element, container, [callback] )

renderToString()

ReactDOMServer.renderToString是两个服务端渲染函数中的一个,也是开发主要使用的一个函数,ReactDOM.render不一样,该函数去掉了用于表示渲染位置的参数。取而代之,该函数只返回一个字符串,这是一个快速的同步(阻塞式)函数,很是快。

  • 用法:

const ReactDOMServer = require('react-dom/server'); ReactDOMServer.renderToString(element)
  • 例子:

const ReactDOMServer = require('react-dom/server'); const Hello = React.createClass({ render: function() {   return <div>hello</div>; } }); const helloString = ReactDOMServer.renderToString( React.createElement(Hello) ); /* 输出结果大概为: helloString = ` <div  data-reactid=".xxx"  data-react-checksum="-123456" >  hello </div> ` */

从上面这个例子,很容易发现,React为div注入了一些自定义属性,首先reactid,这是在浏览器环境下,React为了区分DOM节点,在须要更新的时候可以精肯定位的标记。然后checksum这个属性仅仅存在与服务端,这个你可能没见过,或没留意,它的做用是拿服务端返回的String与已建立的DOM作校验,这就准许了React在客户端和服务端在结构上拥有相同的DOM结构,该属性只会添加在根节点元素上。拿到checksum大抵会作一些事情:

  • 检查第一个元素是否有data-react-checksum属性,若是有则经过ReactDOMServer.renderToString拿到前端的,经过adler32算法获得的值和data-react-checksum对比,若是一致则表示,无需渲染,不然从新渲染。

renderToStaticMarkup()

import ReactDOMServer from 'react-dom/server'; ReactDOMServer.renderToStaticMarkup(element) // eg: ReactDOMServer.renderToStaticMarkup( React.createElement(   Provider,   { store },   React.createElement(     RouterContext,     matchedData[1]   ) ) );

这个函数和上面的那个函数大致相同,除却,它不会给节点添加任何额外的属性值,它的返回值是『干净』的,在某种状况下,能够节约空间使用。

选择

每一个渲染函数都有本身的用途,因此你必须明确本身的需求,再去决定使用哪一个渲染函数。当且仅当你不打算在客户端渲染这个React Component时,才应该选择使用ReactDOMServer.renderToStaticMarkup函数。下面有一些示例:

  • 生成HTML电子邮件

  • 经过HTML到PDF的转化来生成PDF

  • 组件测试

  • 等一些须要『纯』DOM的状况下

大多数状况下,咱们都会选择使用ReactDOMServer.renderToString。这将准许React使用data-react-checksum在客户端迅速的初始化同一个React Component,由于React能够重用服务端提供的DOM,因此它能够跳过生成DOM节点以及把他们挂载到文档中这两个昂贵的进程,对于复杂些的站点,这样作就会显著的减小加载时间,用户能够更快的与站点进行交互。确保React Component可以在服务端和客户端准确的渲染出一致的结构是很重要的。若是data-react-checksum不匹配,React会舍弃服务端提供的DOM,而后生成新的DOM节点,而且将它们更新到文档中。此时,也就不具有服务端渲染带来的各类性能上的优点。这个错误会是下面这样的,若是你开了React dev模式:

Warning: React attempted to reuse markup in a container but the checksum was invalid. This generally means that you are using server rendering and the markup generated on the server was not what the client was expecting. React injected new markup to compensate which works but you have lost many of the benefits of server rendering. Instead, figure out why the markup being generated is different on the client or server:
 (client) <noscript data-reacti
 (server) <div data-reactid=".q

而解决这个问题,则是保证在服务器环境下可以渲染出与客户端同样的DOM结构,总结一下通常会是两种状况:

  • 异步/延迟加载

  • 存在随机逻辑

  • 最大的问题是,客户端和服务端的环境差别造就的问题,如document环境下的一些元素没法在服务端渲染等

总结

上面讲了些服务器渲染须要注意到的点,而一开始提到的按需加载刷新页面出现的闪屏问题尚未落实。如下:在异步加载的时候服务器渲染和客户端渲染的结果不一样,即经过checksum校验失败,从新渲染,在这个从新渲染的时候,须要从新匹配路由,以上就会出现闪屏的状况。咱们注意到,在使用react-router的时候,服务器中使用到了一个match函数

match({ history, routes: getRoutes(store), location: req.originalUrl }, (error, redirectLocation, renderProps) => {})

简单的说他匹配分析了客户端的路由,使得渲染过程当中可以吻合到客户端的路由控制器。而若是客户端在首次出现须要从新渲染的时候,若是是动态路由(按需加载使用到的一项技术),就须要从新匹配渲染,这时候会出现短暂的白屏闪过。解决这个问题,只须要在客户端渲染以前先匹配路由,使用match。(关于match的详细介绍,参见官方文档)

// +redux
// 客户端在渲染前加上匹配路由函数match match({ history, routes }, (error, redirectLocation, renderProps) => { if (!error) {   // 渲染   ReactDOM.render(     <Provider store={store}>{routes}</Provider>,     document.getElementById('root')   ); } else {   console.error(error);   // todo: 错误信息收集 } });

以上,即可解决题中问题。

相关文章
相关标签/搜索