鍍金池/ 教程/ HTML/ 服務(wù)器端渲染
React 組件
Redux 的基礎(chǔ)概念
JSX
DOM 操作
在 React 應(yīng)用中使用 Redux
進(jìn)化 Flux
Webpack 配置 React 開發(fā)環(huán)境
服務(wù)器端渲染
組合組件
表單
屬性擴(kuò)散
開發(fā)環(huán)境配置
組件生命周期
Data Flow
JSX 與 HTML 的差異
組件間通信
使用 JSX
事件處理
Flux
React 概覽
Mixins
Redux

服務(wù)器端渲染

React 提供了兩個(gè)方法 renderToStringrenderToStaticMarkup 用來將組件(Virtual DOM)輸出成 HTML 字符串,這是 React 服務(wù)器端渲染的基礎(chǔ),它移除了服務(wù)器端對(duì)于瀏覽器環(huán)境的依賴,所以讓服務(wù)器端渲染變成了一件有吸引力的事情。

服務(wù)器端渲染除了要解決對(duì)瀏覽器環(huán)境的依賴,還要解決兩個(gè)問題:

  • 前后端可以共享代碼
  • 前后端路由可以統(tǒng)一處理

React 生態(tài)提供了很多選擇方案,這里我們選用 Reduxreact-router 來做說明。

Redux

Redux 提供了一套類似 Flux 的單向數(shù)據(jù)流,整個(gè)應(yīng)用只維護(hù)一個(gè) Store,以及面向函數(shù)式的特性讓它對(duì)服務(wù)器端渲染支持很友好。

2 分鐘了解 Redux 是如何運(yùn)作的

關(guān)于 Store:

  • 整個(gè)應(yīng)用只有一個(gè)唯一的 Store
  • Store 對(duì)應(yīng)的狀態(tài)樹(State),由調(diào)用一個(gè) reducer 函數(shù)(root reducer)生成
  • 狀態(tài)樹上的每個(gè)字段都可以進(jìn)一步由不同的 reducer 函數(shù)生成
  • Store 包含了幾個(gè)方法比如 dispatch, getState 來處理數(shù)據(jù)流
  • Store 的狀態(tài)樹只能由 dispatch(action) 來觸發(fā)更改

Redux 的數(shù)據(jù)流:

  • action 是一個(gè)包含 { type, payload } 的對(duì)象
  • reducer 函數(shù)通過 store.dispatch(action) 觸發(fā)
  • reducer 函數(shù)接受 (state, action) 兩個(gè)參數(shù),返回一個(gè)新的 state
  • reducer 函數(shù)判斷 action.type 然后處理對(duì)應(yīng)的 action.payload 數(shù)據(jù)來更新狀態(tài)樹

所以對(duì)于整個(gè)應(yīng)用來說,一個(gè) Store 就對(duì)應(yīng)一個(gè) UI 快照,服務(wù)器端渲染就簡(jiǎn)化成了在服務(wù)器端初始化 Store,將 Store 傳入應(yīng)用的根組件,針對(duì)根組件調(diào)用 renderToString 就將整個(gè)應(yīng)用輸出成包含了初始化數(shù)據(jù)的 HTML。

react-router

react-router 通過一種聲明式的方式匹配不同路由決定在頁(yè)面上展示不同的組件,并且通過 props 將路由信息傳遞給組件使用,所以只要路由變更,props 就會(huì)變化,觸發(fā)組件 re-render。

假設(shè)有一個(gè)很簡(jiǎn)單的應(yīng)用,只有兩個(gè)頁(yè)面,一個(gè)列表頁(yè) /list 和一個(gè)詳情頁(yè) /item/:id,點(diǎn)擊列表上的條目進(jìn)入詳情頁(yè)。

可以這樣定義路由,./routes.js

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

// 無狀態(tài)(stateless)組件,一個(gè)簡(jiǎn)單的容器,react-router 會(huì)根據(jù) route
// 規(guī)則匹配到的組件作為 `props.children` 傳入
const Container = (props) => {
  return (
    <div>{props.children}</div>
  );
};

// route 規(guī)則:
// - `/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;

從這里開始,我們通過這個(gè)非常簡(jiǎn)單的應(yīng)用來解釋實(shí)現(xiàn)服務(wù)器端渲染前后端涉及的一些細(xì)節(jié)問題。

Reducer

Store 是由 reducer 產(chǎn)生的,所以 reducer 實(shí)際上反映了 Store 的狀態(tài)樹結(jié)構(gòu)

./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)
  };
}

rootReducerstate 參數(shù)就是整個(gè) Store 的狀態(tài)樹,狀態(tài)樹下的每個(gè)字段對(duì)應(yīng)也可以有自己的 reducer,所以這里引入了 listReduceritemReducer,可以看到這兩個(gè) reducer 的 state 參數(shù)就只是整個(gè)狀態(tài)樹上對(duì)應(yīng)的 listitem 字段。

具體到 ./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 就是一個(gè)包含 items 的簡(jiǎn)單數(shù)組,可能類似這種結(jié)構(gòu):[{ id: 0, name: 'first item'}, {id: 1, name: 'second item'}],從 'FETCH_LIST_SUCCESS'action.payload 獲得。

然后是 ./reducers/item.js,處理獲取到的 item 數(shù)據(jù)

const initialState = {};

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

Action

對(duì)應(yīng)的應(yīng)該要有兩個(gè) action 來獲取 list 和 item,觸發(fā) reducer 更改 Store,這里我們定義 fetchListfetchItem 兩個(gè) 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 是一個(gè)前后端通用的 Ajax 實(shí)現(xiàn),前后端要共享代碼這點(diǎn)很重要。

另外因?yàn)樯婕暗疆惒秸?qǐng)求,這里的 action 用到了 thunk,也就是函數(shù),redux 通過 thunk-middleware 來處理這類 action,把函數(shù)當(dāng)作普通的 action dispatch 就好了,比如 dispatch(fetchList())

Store

我們用一個(gè)獨(dú)立的 ./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

接下來就是實(shí)現(xiàn) <List>,<Item> 組件,然后把 Redux 和 React 組件關(guān)聯(lián)起來,具體細(xì)節(jié)參見 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__` 來自服務(wù)器端渲染,下一部分細(xì)說
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'));

至此,客戶端部分結(jié)束。

Server Rendering

接下來的服務(wù)器端就比較簡(jiǎn)單了,獲取數(shù)據(jù)可以調(diào)用 action,routes 在服務(wù)器端的處理參考 react-router server rendering,在服務(wù)器端用一個(gè) match 方法將拿到的 request url 匹配到我們之前定義的 routes,解析成和客戶端一致的 props 對(duì)象傳遞給組件。

./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');
    }
  });
});

服務(wù)器端渲染部分可以直接通過共用客戶端 store.dispatch(action) 來統(tǒng)一獲取 Store 數(shù)據(jù)。另外注意 renderFullPage 生成的頁(yè)面 HTML 在 React 組件 mount 的部分(<div id="root">),前后端的 HTML 結(jié)構(gòu)應(yīng)該是一致的。然后要把 store 的狀態(tài)樹寫入一個(gè)全局變量(__INITIAL_STATE__),這樣客戶端初始化 render 的時(shí)候能夠校驗(yàn)服務(wù)器生成的 HTML 結(jié)構(gòu),并且同步到初始化狀態(tài),然后整個(gè)頁(yè)面被客戶端接管。

最后關(guān)于頁(yè)面內(nèi)鏈接跳轉(zhuǎn)如何處理?

react-router 提供了一個(gè) <Link> 組件用來替代 <a> 標(biāo)簽,它負(fù)責(zé)管理瀏覽器 history,從而不是每次點(diǎn)擊鏈接都去請(qǐng)求服務(wù)器,然后可以通過綁定 onClick 事件來作其他處理。

比如在 /list 頁(yè)面,對(duì)于每一個(gè) item 都會(huì)用 <Link> 綁定一個(gè) route url:/item/:id,并且綁定 onClick 去觸發(fā) dispatch(fetchItem(id)) 獲取數(shù)據(jù),顯示詳情頁(yè)內(nèi)容。

更多參考


[最初發(fā)布于 Coding Blog]