玩轉 React 服務器端渲染

React 提供了兩個方法 renderToString 和 renderToStaticMarkup 用來將組件(Virtual DOM)輸出成 HTML 字符串,這是 React 服務器端渲染的基礎,它移除了服務器端對於瀏覽器環境的依賴,因此讓服務器端渲染變成了一件有吸引力的事情。php

服務器端渲染除了要解決對瀏覽器環境的依賴,還要解決兩個問題:css

  • 先後端能夠共享代碼
  • 先後端路由能夠統一處理

React 生態提供了不少選擇方案,這裏咱們選用 Redux 和 react-router 來作說明。html

Redux

Redux 提供了一套相似 Flux 的單向數據流,整個應用只維護一個 Store,以及面向函數式的特性讓它對服務器端渲染支持很友好。react

2 分鐘瞭解 Redux 是如何運做的

關於 Store:git

  • 整個應用只有一個惟一的 Store
  • Store 對應的狀態樹(State),由調用一個 reducer 函數(root reducer)生成
  • 狀態樹上的每一個字段均可以進一步由不一樣的 reducer 函數生成
  • Store 包含了幾個方法好比 dispatchgetState 來處理數據流
  • Store 的狀態樹只能由 dispatch(action) 來觸發更改

Redux 的數據流:github

  • action 是一個包含 type, payload } 的對象
  • reducer 函數經過 store.dispatch(action) 觸發
  • reducer 函數接受 (state, action) 兩個參數,返回一個新的 state
  • reducer 函數判斷 action.type 而後處理對應的 action.payload 數據來更新狀態樹

因此對於整個應用來講,一個 Store 就對應一個 UI 快照,服務器端渲染就簡化成了在服務器端初始化 Store,將 Store 傳入應用的根組件,針對根組件調用 renderToString 就將整個應用輸出成包含了初始化數據的 HTML。express

react-router

react-router 經過一種聲明式的方式匹配不一樣路由決定在頁面上展現不一樣的組件,而且經過 props 將路由信息傳遞給組件使用,因此只要路由變動,props 就會變化,觸發組件 re-render。json

假設有一個很簡單的應用,只有兩個頁面,一個列表頁 /list 和一個詳情頁 /item/:id,點擊列表上的條目進入詳情頁。redux

能夠這樣定義路由,./routes.js後端

import React from 'react';
import { Route } from 'react-router';
import { List, Item } from './components';

// 無狀態(stateless)組件,一個簡單的容器,react-router 會根據 route
// 規則匹配到的組件做爲 `props.children` 傳入 const Container = (props) => { return (  <div>{props.children}</div> ); }; // route 規則: // - `/list` 顯示 `List` 組件 // - `/item/:id` 顯示 `Item` 組件 const routes = ( <Route path="/" component={Container} >  <Route path="list" component={List} />  <Route path="item/:id" component={Item} /> </Route> ); export default routes; 

從這裏開始,咱們經過這個很是簡單的應用來解釋實現服務器端渲染先後端涉及的一些細節問題。

Reducer

Store 是由 reducer 產生的,因此 reducer 實際上反映了 Store 的狀態樹結構

./reducers/index.js

import listReducer from './list';
import itemReducer from './item';

export default function rootReducer(state = {}, action) {
  return {
 list: listReducer(state.list, action),  item: itemReducer(state.item, action) }; } 

rootReducer 的 state 參數就是整個 Store 的狀態樹,狀態樹下的每一個字段對應也能夠有本身的
reducer,因此這裏引入了 listReducer 和 itemReducer,能夠看到這兩個 reducer
的 state 參數就只是整個狀態樹上對應的 list 和 item 字段。

具體到 ./reducers/list.js

const initialState = [];

export default function listReducer(state = initialState, action) {
  switch(action.type) {
  case 'FETCH_LIST_SUCCESS': return [...action.payload]; default: return state; } } 

list 就是一個包含 items 的簡單數組,可能相似這種結構:[{ id: 0, name: 'first item'}, {id: 1, name: 'second item'}],從'FETCH_LIST_SUCCESS' 的 action.payload 得到。

而後是 ./reducers/item.js,處理獲取到的 item 數據

const initialState = {};

export default function listReducer(state = initialState, action) {
  switch(action.type) {
  case 'FETCH_ITEM_SUCCESS': return [...action.payload]; default: return state; } } 

Action

對應的應該要有兩個 action 來獲取 list 和 item,觸發 reducer 更改 Store,這裏咱們定義 fetchList 和 fetchItem 兩個 action。

./actions/index.js

import fetch from 'isomorphic-fetch';

export function fetchList() {
  return (dispatch) => {
 return fetch('/api/list')  .then(res => res.json())  .then(json => dispatch({ type: 'FETCH_LIST_SUCCESS', payload: json })); } } export function fetchItem(id) { return (dispatch) => {  if (!id) return Promise.resolve();  return fetch(`/api/item/${id}`)  .then(res => res.json())  .then(json => dispatch({ type: 'FETCH_ITEM_SUCCESS', payload: json })); } } 

isomorphic-fetch 是一個先後端通用的 Ajax 實現,先後端要共享代碼這點很重要。

另外由於涉及到異步請求,這裏的 action 用到了 thunk,也就是函數,redux 經過 thunk-middleware 來處理這類 action,把函數看成普通的 action dispatch 就行了,好比 dispatch(fetchList())

Store

咱們用一個獨立的 ./store.js,配置(好比 Apply Middleware)生成 Store

import { createStore } from 'redux';
import rootReducer from './reducers';

// Apply middleware here
// ...

export default function configureStore(initialState) {
  const store = createStore(rootReducer, initialState);
  return store;
}

react-redux

接下來實現 <List><Item> 組件,而後把 redux 和 react 組件關聯起來,具體細節參見 react-redux

./app.js

import React from 'react';
import { render } from 'react-dom';
import { Router } from 'react-router';
import createBrowserHistory from 'history/lib/createBrowserHistory';
import { Provider } from 'react-redux';
import routes from './routes';
import configureStore from './store';

// `__INITIAL_STATE__` 來自服務器端渲染,下一部分細說 const initialState = window.__INITIAL_STATE__; const store = configureStore(initialState); const Root = (props) => { return (  <div>  <Provider store={store}>  <Router history={createBrowserHistory()}>  {routes}  </Router>  </Provider>  </div> ); } render(<Root />, document.getElementById('root')); 

至此,客戶端部分結束。

Server Rendering

接下來的服務器端就比較簡單了,獲取數據能夠調用 action,routes 在服務器端的處理參考 react-router server rendering,在服務器端用一個 match 方法將拿到的 request url 匹配到咱們以前定義的 routes,解析成和客戶端一致的 props 對象傳遞給組件。

./server.js

import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { RoutingContext, match } from 'react-router';
import { Provider } from 'react-redux';
import routes from './routes';
import configureStore from './store';

const app = express();

function renderFullPage(html, initialState) {
  return `
 <!DOCTYPE html>  <html lang="en">  <head>  <meta charset="UTF-8">  </head>  <body>  <div id="root">  <div>  ${html}  </div>  </div>  <script>  window.__INITIAL_STATE__ = ${JSON.stringify(initialState)};  </script>  <script src="/static/bundle.js"></script>  </body>  </html> `; } app.use((req, res) => { match({ routes, location: req.url }, (err, redirectLocation, renderProps) => {  if (err) {  res.status(500).end(`Internal Server Error ${err}`);  } else if (redirectLocation) {  res.redirect(redirectLocation.pathname + redirectLocation.search);  } else if (renderProps) {  const store = configureStore();  const state = store.getState();  Promise.all([  store.dispatch(fetchList()),  store.dispatch(fetchItem(renderProps.params.id))  ])  .then(() => {  const html = renderToString(  <Provider store={store}>  <RoutingContext {...renderProps} />  </Provider>  );  res.end(renderFullPage(html, store.getState()));  });  } else {  res.status(404).end('Not found');  } }); }); 

服務器端渲染部分能夠直接經過共用客戶端 store.dispatch(action) 來統一獲取 Store 數據。另外注意 renderFullPage 生成的頁面 HTML 在 React 組件 mount 的部分(<div id="root">),先後端的 HTML 結構應該是一致的。而後要把 store 的狀態樹寫入一個全局變量(__INITIAL_STATE__),這樣客戶端初始化 render 的時候可以校驗服務器生成的 HTML 結構,而且同步到初始化狀態,而後整個頁面被客戶端接管。

最後關於頁面內連接跳轉如何處理?

react-router 提供了一個 <Link> 組件用來替代 <a> 標籤,它負責管理瀏覽器 history,從而不是每次點擊連接都去請求服務器,而後能夠經過綁定 onClick 事件來做其餘處理。

好比在 /list 頁面,對於每個 item 都會用 <Link> 綁定一個 route url:/item/:id,而且綁定 onClick 去觸發 dispatch(fetchItem(id)) 獲取數據,顯示詳情頁內容。

更多參考

相關文章
相關標籤/搜索