react-route中renderProps內部結構與服務端渲染總結

最新內容請在github閱讀,我會定時更新這部分的內容。給您帶來不便,請見諒~~~~
最近在學習服務端渲染,之前一直不明白match方法中的renderProps,所以現在打印了log,查看了內部的結構

1.match方法

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

知道服務器端渲染的都知道這個match方法,我這裏不再班門弄斧

2.renderProps屬性

首先我給出我自己配置的React-router:

{
   path:"/",
   component : App,
   IndexRoute:{
     component : Home
   },
   childRoutes:[
     {
       component : Home,
       path:"home"
     }
    ]
 }

下面是完整的renderProps屬性,其中renderProps包含routes,params,location,components,router,matchContext屬性。

 { 
    //renderProps包含routes對象
    routes:
    [ { path: '/',
        component: [Function: App],
        IndexRoute: [Object],
        childRoutes: [Object]
     },
      { component: [Function: Home], path: 'home' } 
    ],
   //renderProps包含params對象
   params: {},
   // renderProps包含location對象
   location:
    { pathname: '/home',
      search: '',
      hash: '',
      state: undefined,
      action: 'POP',
      key: '34hg49',
      query: {}
     },
    // renderProps包含components對象
   components: [ [Function: App], [Function: Home] ],
   // renderProps包含router對象
   router:
    { getCurrentLocation: [Function: getCurrentLocation],
      listenBefore: [Function: listenBefore],
      listen: [Function: listen],
      transitionTo: [Function: transitionTo],
      push: [Function: push],
      replace: [Function: replace],
      go: [Function: go],
      goBack: [Function: goBack],
      goForward: [Function: goForward],
      createKey: [Function: createKey],
      createPath: [Function: createPath],
      createHref: [Function: createHref],
      createLocation: [Function: createLocation],
      canGo: [Function: canGo],
      unsubscribe: [Function: unsubscribe],
      setRouteLeaveHook: [Function: listenBeforeLeavingRoute],
      isActive: [Function: isActive],
      location:
       { pathname: '/home',
         search: '',
         hash: '',
         state: undefined,
         action: 'POP',
         key: '34hg49',
         query: {} 
     },
      params: {},
      routes: [ [Object], [Object] ] 
      },
   // renderProps包含matchContext對象
   matchContext:
    { transitionManager:
       { isActive: [Function: isActive],
         match: [Function: match],
         listenBeforeLeavingRoute: [Function: listenBeforeLeavingRoute],
         listen: [Function: listen] },
      router:
       { getCurrentLocation: [Function: getCurrentLocation],
         listenBefore: [Function: listenBefore],
         listen: [Function: listen],
         transitionTo: [Function: transitionTo],
         push: [Function: push],
         replace: [Function: replace],
         go: [Function: go],
         goBack: [Function: goBack],
         goForward: [Function: goForward],
         createKey: [Function: createKey],
         createPath: [Function: createPath],
         createHref: [Function: createHref],
         createLocation: [Function: createLocation],
         canGo: [Function: canGo],
         unsubscribe: [Function: unsubscribe],
         setRouteLeaveHook: [Function: listenBeforeLeavingRoute],
         isActive: [Function: isActive],
         location: [Object],
         params: {},
         routes: [Object] 
     }
   } 
 }

我相信,當你瞭解了renderProps的內部結構以後,你能夠更好的理解網上的關於react-router服務器渲染的例子了。如果覺得有用別忘了star哦~~~~

3.renderToString返回的一個內容

<!-- 1.HTML元素最頂級具有data-react-checksum屬性計算hash
 res.send('<!doctype html>\n' +
          renderToString(<Html assets={webpackIsomorphicTools.assets()} component={component} store={store}/>));
      });
 -->
<html lang="en-us" data-reactroot="" data-reactid="1" data-react-checksum="350097657">
 <head data-reactid="2"> 
  <title data-react-helmet="true" data-reactid="3">Widgets</title> 
  <link rel="shortcut icon" href="/favicon.ico" data-reactid="4" /> 
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css" data-reactid="5" /> 
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Work+Sans:400,500" data-reactid="6" /> 
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/violet/0.0.1/violet.min.css" data-reactid="7" /> 
  <meta name="viewport" content="width=device-width, initial-scale=1" data-reactid="8" /> 
 </head> 
 <body data-reactid="9"> 
  <div id="content" data-reactid="10"> 
  <!--2.我們的Header+Footer+Content的容器具有data-react-checksum的hash值。其渲染的部分爲如下的內容:
   <div id="content" dangerouslySetInnerHTML={{__html: content}}/>
  -->
   <div class="container" data-reactroot="" data-reactid="1" data-react-checksum="1912308161"> 
    <div class="header" data-reactid="2"> 
     <nav class="navbar navbar-inverse" data-reactid="3"> 
      <div class="container" data-reactid="4"> 
       <div class="navbar-header" data-reactid="5"> 
        <a href="#" class="navbar-brand" data-reactid="6">React全家桶實例</a> 
        <button type="button" class="navbar-toggle collapsed" data-reactid="7"> <span class="sr-only" data-reactid="8">Toggle navigation</span> <span class="icon-bar" data-reactid="9"></span> <span class="icon-bar" data-reactid="10"></span> <span class="icon-bar" data-reactid="11"></span> </button> 
       </div> 
       <div class="navbar-collapse collapse" data-reactid="12"> 
        <ul class="nav navbar-nav" data-reactid="13"> 
         <li role="presentation" class="" data-reactid="14"> <a href="#" role="button" data-reactid="15">聊天</a> </li> 
         <li role="presentation" class="active" data-reactid="16"> <a href="/widget" action="push" data-reactid="17">Widget</a> </li> 
         <li role="presentation" class="" data-reactid="18"> <a href="#" role="button" data-reactid="19">Survey</a> </li> 
         <li role="presentation" class="" data-reactid="20"> <a href="#" role="button" data-reactid="21">關於我們</a> </li> 
         <li role="presentation" class="" data-reactid="22"> <a href="/login" action="push" data-reactid="23">登錄</a> </li> 
         <li role="presentation" class="" data-reactid="24"> <a href="/pagination" action="push" data-reactid="25">分頁</a> </li> 
         <li class="dropdown" data-reactid="26"> <a id="basic-nav-dropdown" role="button" class="dropdown-toggle" aria-haspopup="true" aria-expanded="false" href="#" data-reactid="27">
           <!-- react-text: 28 -->更多
           <!-- /react-text -->
           <!-- react-text: 29 --> 
           <!-- /react-text --><span class="caret" data-reactid="30"></span> </a> 
          <ul role="menu" class="dropdown-menu" aria-labelledby="basic-nav-dropdown" data-reactid="31"> 
           <li role="presentation" class="" data-reactid="32"><a role="menuitem" tabindex="-1" href="#" data-reactid="33">Action</a> </li> 
           <li role="presentation" class="" data-reactid="34"><a role="menuitem" tabindex="-1" href="#" data-reactid="35">Another action</a> </li> 
           <li role="presentation" class="" data-reactid="36"> <a role="menuitem" tabindex="-1" href="#" data-reactid="37">Something else here</a> </li> 
           <li role="separator" class="divider" data-reactid="38"></li>
           <li role="presentation" class="" data-reactid="39"><a role="menuitem" tabindex="-1" href="#" data-reactid="40">Separated link</a></li> 
          </ul> </li> 
        </ul> 
        <ul class="nav navbar-nav navbar-right" data-reactid="41"> 
         <li role="presentation" class="" data-reactid="42"> <a href="#" role="button" data-reactid="43">Link Right</a> </li> 
         <li role="presentation" class="" data-reactid="44"> <a href="#" role="button" data-reactid="45">Link Right</a> </li> 
        </ul> 
       </div> 
      </div> 
     </nav> 
    </div> 
    <div data-reactid="46">
     <!-- react-empty: 47 --> 
     <table class="table table-striped" data-reactid="48"> 
      <thead data-reactid="49"> 
       <tr data-reactid="50"> 
        <th data-reactid="51">ID</th> 
        <th data-reactid="52">顏色</th> 
        <th data-reactid="53">Sprockets</th> 
        <th data-reactid="54">所有者</th> 
        <th data-reactid="55"></th> 
       </tr> 
      </thead> 
      <tbody data-reactid="56"> 
       <tr data-reactid="57"> 
        <td data-reactid="58">1</td> 
        <td data-reactid="59">Red</td> 
        <td data-reactid="60">7</td> 
        <td data-reactid="61">John</td> 
        <td data-reactid="62"> <button class="btn btn-primary" data-reactid="63"> <i class="fa fa-pencil" data-reactid="64"></i>
          <!-- react-text: 65 --> 編輯
          <!-- /react-text --> </button> 
         <div data-reactid="66"> 
          <div style="color:rgba(0, 0, 0, 0.87);background-color:#ffffff;transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;box-sizing:border-box;font-family:Roboto, sans-serif;-webkit-tap-highlight-color:rgba(0,0,0,0);box-shadow:0 1px 6px rgba(0, 0, 0, 0.12),
[1]          0 1px 4px rgba(0, 0, 0, 0.12);border-radius:2px;display:inline-block;min-width:88px;mui-prepared:;-webkit-transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;-moz-transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;-moz-box-sizing:border-box;" data-reactid="67"> 
           <button style="border:10px;box-sizing:border-box;display:inline-block;font-family:Roboto, sans-serif;-webkit-tap-highlight-color:rgba(0, 0, 0, 0);cursor:pointer;text-decoration:none;margin:0;padding:0;outline:none;font-size:inherit;font-weight:inherit;position:relative;z-index:1;height:36px;line-height:36px;width:100%;border-radius:2px;transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;background-color:#ffffff;text-align:center;mui-prepared:;-moz-box-sizing:border-box;-webkit-transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;-moz-transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;" tabindex="0" type="button" data-reactid="68"> 
            <div data-reactid="69"> 
             <div style="height:36px;border-radius:2px;background-color:;transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;top:0;mui-prepared:;-webkit-transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;-moz-transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;" data-reactid="70">
              <span style="position:relative;opacity:1;font-size:14px;letter-spacing:0;text-transform:uppercase;font-weight:500;margin:0;user-select:none;padding-left:16px;padding-right:16px;color:rgba(0, 0, 0, 0.87);mui-prepared:;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;" data-reactid="71">Modal Dialog</span>
             </div>
            </div></button>
          </div>
          <!-- react-empty: 72 -->
         </div></td>
       </tr>
       <tr data-reactid="73">
        <td data-reactid="74">2</td>
        <td data-reactid="75">Taupe</td>
        <td data-reactid="76">1</td>
        <td data-reactid="77">George</td>
        <td data-reactid="78"><button class="btn btn-primary" data-reactid="79"><i class="fa fa-pencil" data-reactid="80"></i>
          <!-- react-text: 81 --> 編輯
          <!-- /react-text --></button>
         <div data-reactid="82">
          <div style="color:rgba(0, 0, 0, 0.87);background-color:#ffffff;transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;box-sizing:border-box;font-family:Roboto, sans-serif;-webkit-tap-highlight-color:rgba(0,0,0,0);box-shadow:0 1px 6px rgba(0, 0, 0, 0.12),
[1]          0 1px 4px rgba(0, 0, 0, 0.12);border-radius:2px;display:inline-block;min-width:88px;mui-prepared:;-webkit-transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;-moz-transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;-moz-box-sizing:border-box;" data-reactid="83">
           <button style="border:10px;box-sizing:border-box;display:inline-block;font-family:Roboto, sans-serif;-webkit-tap-highlight-color:rgba(0, 0, 0, 0);cursor:pointer;text-decoration:none;margin:0;padding:0;outline:none;font-size:inherit;font-weight:inherit;position:relative;z-index:1;height:36px;line-height:36px;width:100%;border-radius:2px;transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;background-color:#ffffff;text-align:center;mui-prepared:;-moz-box-sizing:border-box;-webkit-transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;-moz-transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;" tabindex="0" type="button" data-reactid="84">
            <div data-reactid="85">
             <div style="height:36px;border-radius:2px;background-color:;transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;top:0;mui-prepared:;-webkit-transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;-moz-transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;" data-reactid="86">
              <span style="position:relative;opacity:1;font-size:14px;letter-spacing:0;text-transform:uppercase;font-weight:500;margin:0;user-select:none;padding-left:16px;padding-right:16px;color:rgba(0, 0, 0, 0.87);mui-prepared:;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;" data-reactid="87">Modal Dialog</span>
             </div>
            </div></button>
          </div>
          <!-- react-empty: 88 -->
         </div></td>
       </tr>
       <tr data-reactid="89">
        <td data-reactid="90">3</td>
        <td data-reactid="91">Green</td>
        <td data-reactid="92">8</td>
        <td data-reactid="93">Ringo</td>
        <td data-reactid="94"><button class="btn btn-primary" data-reactid="95"><i class="fa fa-pencil" data-reactid="96"></i>
          <!-- react-text: 97 --> 編輯
          <!-- /react-text --></button>
         <div data-reactid="98">
          <div style="color:rgba(0, 0, 0, 0.87);background-color:#ffffff;transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;box-sizing:border-box;font-family:Roboto, sans-serif;-webkit-tap-highlight-color:rgba(0,0,0,0);box-shadow:0 1px 6px rgba(0, 0, 0, 0.12),
[1]          0 1px 4px rgba(0, 0, 0, 0.12);border-radius:2px;display:inline-block;min-width:88px;mui-prepared:;-webkit-transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;-moz-transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;-moz-box-sizing:border-box;" data-reactid="99">
           <button style="border:10px;box-sizing:border-box;display:inline-block;font-family:Roboto, sans-serif;-webkit-tap-highlight-color:rgba(0, 0, 0, 0);cursor:pointer;text-decoration:none;margin:0;padding:0;outline:none;font-size:inherit;font-weight:inherit;position:relative;z-index:1;height:36px;line-height:36px;width:100%;border-radius:2px;transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;background-color:#ffffff;text-align:center;mui-prepared:;-moz-box-sizing:border-box;-webkit-transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;-moz-transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;" tabindex="0" type="button" data-reactid="100">
            <div data-reactid="101">
             <div style="height:36px;border-radius:2px;background-color:;transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;top:0;mui-prepared:;-webkit-transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;-moz-transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;" data-reactid="102">
              <span style="position:relative;opacity:1;font-size:14px;letter-spacing:0;text-transform:uppercase;font-weight:500;margin:0;user-select:none;padding-left:16px;padding-right:16px;color:rgba(0, 0, 0, 0.87);mui-prepared:;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;" data-reactid="103">Modal Dialog</span>
             </div>
            </div></button>
          </div>
          <!-- react-empty: 104 -->
         </div></td>
       </tr>
       <tr data-reactid="105">
        <td data-reactid="106">4</td>
        <td data-reactid="107">Blue</td>
        <td data-reactid="108">2</td>
        <td data-reactid="109">Paul</td>
        <td data-reactid="110"><button class="btn btn-primary" data-reactid="111"><i class="fa fa-pencil" data-reactid="112"></i>
          <!-- react-text: 113 --> 編輯
          <!-- /react-text --></button>
         <div data-reactid="114">
          <div style="color:rgba(0, 0, 0, 0.87);background-color:#ffffff;transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;box-sizing:border-box;font-family:Roboto, sans-serif;-webkit-tap-highlight-color:rgba(0,0,0,0);box-shadow:0 1px 6px rgba(0, 0, 0, 0.12),
[1]          0 1px 4px rgba(0, 0, 0, 0.12);border-radius:2px;display:inline-block;min-width:88px;mui-prepared:;-webkit-transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;-moz-transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;-moz-box-sizing:border-box;" data-reactid="115">
           <button style="border:10px;box-sizing:border-box;display:inline-block;font-family:Roboto, sans-serif;-webkit-tap-highlight-color:rgba(0, 0, 0, 0);cursor:pointer;text-decoration:none;margin:0;padding:0;outline:none;font-size:inherit;font-weight:inherit;position:relative;z-index:1;height:36px;line-height:36px;width:100%;border-radius:2px;transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;background-color:#ffffff;text-align:center;mui-prepared:;-moz-box-sizing:border-box;-webkit-transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;-moz-transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;" tabindex="0" type="button" data-reactid="116">
            <div data-reactid="117">
             <div style="height:36px;border-radius:2px;background-color:;transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;top:0;mui-prepared:;-webkit-transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;-moz-transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;" data-reactid="118">
              <span style="position:relative;opacity:1;font-size:14px;letter-spacing:0;text-transform:uppercase;font-weight:500;margin:0;user-select:none;padding-left:16px;padding-right:16px;color:rgba(0, 0, 0, 0.87);mui-prepared:;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;" data-reactid="119">Modal Dialog</span>
             </div>
            </div></button>
          </div>
          <!-- react-empty: 120 -->
         </div></td>
       </tr>
      </tbody>
     </table>
     <button data-reactid="121">加載widget</button>
    </div>
    <div class="footer" data-reactid="122">
     Footer
    </div>
   </div>
  </div>
  <script charset="UTF-8" data-reactid="11">
   window.__data = {
    //routing來自於combineReducer的react-router
    "routing": {
        "locationBeforeTransitions": {
            "pathname": "\u002Fwidget",
            "search": "",
            "hash": "",
            "action": "POP",
            "key": null,
            "query": {}
        }
    },
     //reduxAsyncConnect來自於combineReducer
    "reduxAsyncConnect": {
        "loaded": true
    },
    //auth來自於combineReducer
    "auth": {
        "loaded": true,
        "loading": false,
        "user": null
    },
    //form來自於combineReducer中的redux-form
    "form": {},
    //multireducer來自於combineReducer
    "multireducer": {
        "counter1": {
            "count": 0
        },
        "counter2": {
            "count": 0
        },
        "counter3": {
            "count": 0
        }
    },
   //info來自於combineReducer
    "info": {
        "loaded": true,
        "loading": false,
        "data": {
            "message": "This came from the api server",
            "time": 1496386316261
        }
    },
     //connect來自於combineReducer
    "connect": {
        "connected": false
    },
    //recipeGrid來自於combineReducer的violet-paginator
    "recipeGrid": {
        "removing": [],
        "loadError": null,
        "sortReverse": false,
        "updating": [],
        "requestId": null,
        "totalCount": 0,
        "page": 1,
        "pageSize": 15,
        "isLoading": false,
        "initialized": false,
        "results": [],
        "stale": false,
        "massUpdating": [],
        "filters": {},
        "sort": ""
    },
     //widgets來自於combineReducer
    "widgets": {
        "loaded": true,
        "editing": {},
        "saveError": {},
        "loading": false,
        "data": [{
            "id": 1,
            "color": "Red",
            "sprocketCount": 7,
            "owner": "John"
        },
        {
            "id": 2,
            "color": "Taupe",
            "sprocketCount": 1,
            "owner": "George"
        },
        {
            "id": 3,
            "color": "Green",
            "sprocketCount": 8,
            "owner": "Ringo"
        },
        {
            "id": 4,
            "color": "Blue",
            "sprocketCount": 2,
            "owner": "Paul"
        }],
        "error": null
    }
};
  </script>
  <script src="http://localhost:8091/dist/main-3dbb874c116423a8ee43.js" charset="UTF-8" data-reactid="12"></script> 
 </body>
</html>

你可以查看react-universal-bucket這個例子。上面是renderToString輸出的內容,你要學會關注data-react-checksumdata-reactidwindow.__data

4.renderToString的真實作用

4.1 renderToString本身是靜態的

之所以說renderToString是靜態的,是因爲其如果僅僅是調用它來渲染組件樹那麼其只是一個模版語言而已。React提供了一個API用於將虛擬DOM樹在服務端環境下進行渲染,這個API是ReactDom/server中的renderToString。這個方法接受一個虛擬DOM樹,並返回一個渲染後的HTML字符串,例如我有一個根級組件Root,我便可以使用下列語句得到結果:

import {renderToString} from 'ReactDom/server';
const markup = renderToString(
    <Root />
);

markup即爲渲染後的結果。renderToString這個方法是服務端渲染的基礎,但如果只是單純這樣使用,那麼基本等於將React作爲一個複雜很多的模板語言來寫而已,因爲這個渲染並不會理會任何的ajax請求(沒有請求也就是說在渲染我們組件之前無法自定義組件需要的數據,這是硬傷!!可以使用下文的redux的Provider將store中的數據傳遞給我們的組件來解決),同時也不會根據url來做任何路由,它只會在第一次render方法調用後結束。這也就是說render方法之後的所有生命週期函數都不會被觸發,在一次服務端渲染中,只有constructor、componentWillMount和render會被各觸發一次,並且在期間使用setState也是沒有意義的

這顯然不是我們期望的,爲了愉快得滿足我們的須有,這裏有兩個問題需要解決:

(1)路由(使用react-router來監聽location變化從而重新渲染組件,此處不做講解)。
(2)ajax請求(而不是將React作爲一個模板語言,先發送ajax請求獲取到store狀態再渲染組件樹,redux-async-connect完成或者中間件自己處理)。

4.2 renderToString結合redux處理ajax請求

(1)通過redux-async-connect來處理ajax請求並自動渲染

由於服務端渲染只會走一遍生命週期,並且在第一次render後便會停止,所以想要真正渲染出最終的頁面,我們必須在第一次渲染前就將狀態準備好。這也就是說,我們必須要有一次超前的ajax請求實現獲取狀態,然後來根據這個狀態渲染我們最終的組件樹:

function hydrateOnClient() {
    res.send('<!doctype html>\n' +
      renderToString(<Html assets={webpackIsomorphicTools.assets()} store={store}\/>));
}
//這裏的match方法是在express的中間件中處理
match({ history, routes: getRoutes(store), location: req.originalUrl }, (error, redirectLocation, renderProps) => {
    if (redirectLocation) {
      res.redirect(redirectLocation.pathname + redirectLocation.search);
      //重定向要添加pathname+search
    } else if (error) {
      console.error('ROUTER ERROR:', pretty.render(error));
      res.status(500);
      hydrateOnClient();
    } else if (renderProps) {
      //loadOnServer和ReduxAsyncConnect來自於redux-async-connect庫
      loadOnServer({...renderProps, store, helpers: {client}}).then(() => {
        const component = (
          <Provider store={store} key="provider">
            <ReduxAsyncConnect {...renderProps} />
            //這裏不是 <RouterContext {...renderProps} />
            //https://zhuanlan.zhihu.com/p/22875338
          <\/Provider>
        );
        res.status(200);
        global.navigator = {userAgent: req.headers['user-agent']};
        res.send('<!doctype html>\n' +
          renderToString(<Html assets={webpackIsomorphicTools.assets()} component={component} store={store}\/>));
         //component表示要渲染爲html字符串的組件樹,store來自於redux
      });
    } else {
      res.status(404).send('Not found');
    }
  });
}

在Provider中我們提供了一個store屬性,我們看看Html組件的處理邏輯你就明白了:

import React, {Component, PropTypes} from 'react';
import {renderToString} from 'react-dom/server';
import serialize from 'serialize-javascript';
// https://github.com/liangklfang/serialize-javascript
import Helmet from 'react-helmet';
export default class Html extends Component {
  render() {
    const {assets, component, store} = this.props;
    const content = component ? renderToString(component) : '';
    //我們的component屬性表示最外層組件爲Provider,其會接收redux的store作爲參數
    const head = Helmet.rewind();
    return (
      <html lang="en-us">
        <head>
          {Object.keys(assets.styles).map((style, key) =>
            <link href={assets.styles[style]} key={key} media="screen, projection"
                  rel="stylesheet" type="text/css" charSet="UTF-8"/>
          )}
        </head>
        <body>
          <div id="content" dangerouslySetInnerHTML={{__html: content}}/>
          <script dangerouslySetInnerHTML={{__html: `window.__data=${serialize(store.getState())};`}} charSet="UTF-8"/>
          //返回服務端store的狀態
          <script src={assets.javascript.main} charSet="UTF-8"/>
        </body>
      </html>
    );
  }
}

我們的組件接收的component屬性爲組件樹,該組件樹會被渲染爲html字符串。而我們的store屬性來自於我們的redux中的store,我們會調用store.getState()將當前的store的狀態返回給客戶端。此時客戶端就有了渲染頁面的所有數據了,通過window.__data就可以獲取到服務端渲染的store的狀態,從而服務端和客戶端的store狀態就同步了。同時我們要注意上面的這段代碼:

   const content = component ? renderToString(component) : '';

下面是我們的要渲染的component組件樹內容:

 const component = (
          <Provider store={store} key="provider">
            <ReduxAsyncConnect {...renderProps} />
            //這裏不是 <RouterContext {...renderProps} />
            //https://zhuanlan.zhihu.com/p/22875338
          <\/Provider>
    );

很顯然,我們的Provider將store放到了context中,從而所有的下級組件都能獲取到該context中的store。文章一開頭就說了renderProps的簽名了,該屬性傳入了ReduxAsyncConnect,其包含了routes,params,location,components,router,matchContext等,這樣我們就可以根據我們自己的數據來渲染組件了(ReduxAsyncConnect本身就是處理ajax異步請求,保證數據加載完成再進行渲染)。上面給出的例子中所有的ajax請求是通過ReduxAsyncConnect自動完成了,如下:

import { asyncConnect } from 'redux-async-connect';
@asyncConnect([{
  deferred: true,
  promise:({store:{dispatch,getState}}) =>{
   //判斷我們的state中是否有widget,同時loaded屬性是否是true,如果爲true表示加載過一次數據
   //否則我們手動加載數據。但是是在頁面發生跳轉之後才加載數據
    if(!isLoaded(getState())){
      return dispatch(loadWidgets());
      //注意這裏必須要返回return,因爲這是對dispatch進行增強的邏輯,所以必須有return纔可以
    }
  }
}])

(2)手動處理ajax請求來設置store的初始狀態

但是我們也可以自己來處理ajax請求,請看下面的例子(來自於參考文獻中文章):

//在中間件中處理就可以了,攔截特定的請求
function serverSideRender(req, res) {
    request.get(url)
    .then(response => normalRender(res, response))
    .catch(err => render500(res, err))
}

在自己定義的normalRender這個方法裏,我們可以通過Redux提供的createStore方法的第二個參數來進行創建帶有初始狀態的store,然後將這個狀態送入根組件,並執行後續的渲染:

function normalRender(res, response) {
    // 假設response的響應體中就包含了所有狀態信息
    const initState = response.body;
    const store = createStore(reducers, initState);
    //發送ajax請求得到的數據(服務器端自動完成ajax請求)
    const markup = renderToString(
        <Provider store={store}>
            <APP />
        <\/Provider>
    );
    // 將markup和最終的state塞到模板內渲染,這個模板取決於使用的模板引擎,也可以直接字符串替換。也就是將我們的服務端store狀態發送到客戶端,保證store的狀態同步
    return res.render(template, {
        markup,
        finalState: store.getState().toJSON()
    });
}

這個方法以響應結果爲初始化狀態渲染DOM,並將渲染後的結果塞入模板,值得注意的是,渲染參數裏面有個finalState,這是初次渲染後、store的最終狀態,我們需要將其序列化後強制寫到返回的HTML的script標籤中,將其賦予一個、例如叫initState的變量中,這樣最終返回的HTML結構如下:

<html>
    <head>
        ......
        <script>window.initState = {{finalState}}</script>
    </head>
    <body>
        <div id="react-container"><div>{{markup}}</div></div>
    </body>
</html>

window.initState便擁有了我們服務端渲染後的狀態,如此,客戶端便有了一個途徑來根據這個狀態來初始化客戶端的store,並接續接下來的操作,這實質上是完成了服務端和客戶端之間狀態的對接。

參考資料:

【React/Redux】深入理解React服務端渲染

【React/Redux/Router/Immutable】React最佳實踐的正確食用姿勢

server-rendering

發佈了210 篇原創文章 · 獲贊 83 · 訪問量 98萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章