玩轉 React 伺服器端渲染

來源:互聯網
上載者:User

標籤:

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

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

  • 前後端可以共用代碼
  • 前後端路由可以統一處理

React 生態提供了很多選擇方案,這裡我們選用 Redux 和 react-router 來做說明。

Redux

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

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

關於 Store:

  • 整個應用只有一個唯一的 Store
  • Store 對應的狀態樹(State),由調用一個 reducer 函數(root reducer)產生
  • 狀態樹上的每個欄位都可以進一步由不同的 reducer 函數產生
  • Store 包含了幾個方法比如 dispatchgetState 來處理資料流
  • Store 的狀態樹只能由 dispatch(action) 來觸發更改

Redux 的資料流:

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

所以對於整個應用來說,一個 Store 就對應一個 UI 快照,伺服器端渲染就簡化成了在伺服器端初始化 Store,將 Store 傳入應用的根組件,針對根組件調用 renderToString 就將整個應用輸出成包含了初始化資料的 HTML。

react-router

react-router 通過一種聲明式的方式匹配不同路由決定在頁面上展示不同的組件,並且通過 props 將路由資訊傳遞給組件使用,所以只要路由變更,props 就會變化,觸發組件 re-render。

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

可以這樣定義路由,./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)) 擷取資料,顯示詳情頁內容。

更多參考
  • Universal (Isomorphic)
  • isomorphic-redux-app

玩轉 React 伺服器端渲染

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.