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: 錯誤信息收集 } });

以上,即可解決題中問題。

相關文章
相關標籤/搜索