深入淺出: Isomorphic 的核心架構: React-Redux Server Rendering

React Redux Structure of data, store, props, state and component
「找到對的人真的那麼簡單嗎?」我們都渴望有一天遇到對的人,但交往到一半,就分手了。我告訴你, React Isomorphic 的架構成千上萬,和女朋友一樣難找。

React 和 Redux 的出世, 聚焦了世界前端工程師的目光, 到底它可以做什麼? 到底它在解決什麼問題? 我也是被 Isomorphic 深深吸引的一人:

  1. Server Rendering
    隨著 server rendering 的出現, React 的 Isomorphic 哲學隨之出現, 它解決了Single Page SEO 問題, API邏輯曝露在前端和效能提升。
  2. 完美的 Flux 架構
    React 的精華是它的 Component 設計, 靈活在 Component 之間組合和分拆, 獨立的元件就應該要獨立!! 可是, 當系統越來越大, Component 的靈活性也成為它的致命傷。 Flux 的單向的資料流(data flow), 為 React Component 的靈活加上限制, 彌補不足。

既然 Isomorphic 這麼強大, 大家忍不住展開腳步, 自自然然會到 Github 和 Google 開始大量的 Pull 和 Clone 現成的 Repository 下來, 心想: 應該和之前的 jQuery 或 Backbone 類似, 改些設定檔和HTML 應該可以很快上手. 實際上是這樣的, 我決定去沖杯咖啡, 因為看到它的結構複雜, 而且用到的工具多, 學習曲線高到我覺得需要通宵一晚才能看完一遍, 咖啡可以幫我提提神。

這篇文章記錄了我的學習筆記, React Isomorphic, 它的結構分成5個部份, 其中我只會著墨在第2部份, 只有這個部份會在不同架構下共用, 其餘4個會因應不同開發人者的偏好而改變。

  1. Dev-Server &Build Bundle:
    建立開發環境 (dev) 和上線環境 (production) , 主要解決 hot-reload, auto-watch, bundle build 等的問題. 常用工具: webpack, gulp, nodenpm, browserify and grunt。
  2. *React-Redux Components and Data flow
    這是個單純的問題, 主要解決 UI 和 DATA 的互動和變化, Facebook 為 React 的 VirtualDOM 設計了最優化的 UI 和DATA更新機制。
    這是個複雜的問題, 因為UI 分為 server 的 UI 和 client 的 UI, 而且它們是共用 Component (即是同一個媽生), 所以 Data 也分為 server 和 client 的Data。
    為解決上面的UI/DATA問題, Redux 和 Reac 成為一種解決, Facebook 在這個題目上提供幾個關鍵的工具和建議 (Ex: Container/Presentational Component)。
  3. Routing
    Routing 在 express 和 single page web 的使用上很簡單 , 因為 server 要做什麼, Client 要做什麼, 分得清清楚。 當 Isomorphic 的架構出現, 共用 Component 和共用 Reducer 的設計出現, API 的行為到底要放在哪裡? 應該放在 server ? 還是可以暴露在 client 達到 server client 共用?
  4. Share Reducer/Action Creator
  5. 和 Routing 類似而且有關系
  6. Test

這個 repository 是本文章的 Example, 它的設計按照 Facebook 的 React-Redux 通用架構, 理解了它, 就能快速理解其他 boilerplate。

React Redux server rendering structure

上圖是Example的整體架構, 以 Express server 作為 HTTP server, 而且只有一個 GET ’./’ Routing , 通過 webpack-dev-middleware 和 webpack-hot-server-middle 作為開發環境的工具(把 ./static/bundle.js 存到 memory), 當 Component 有所改變時也同時改變 ./static/bundle.js 的內容, 使得瀏覽器即看更新程式的變化。

上圖的藍色方格是Isomorphic的精華部份 React/Redux, 裡面處理了 react component, redux, root component, store 和 reducer 的所有互動。

React Redux Structure of data, store, props, state and component

正如上面提到, 這個 Example的設計按照 Facebook 的 React-Redux 通用架構。程式的進入點(Entry)以 Root Component 作為進入的第一個 Component, 位置在./index.js, 由特別的 <Provider> Component 組成, 成為 Virtual DOM 的第一個節點, 把 Store 往第二個節點傳播就變得簡單和統一。

在我整理上圖時, 我也發現按照 Facebook Principle 設計的好處, 當我把 Component 按照檔案和資料夾的結構放置, 拆分成 Root Component, Container Component 和 Presentational Component, 通過顏色標籤到各個檔案, 它們各自引用到的 npm module 都是相互獨立而且不重覆 (Root Component 例外). 說到這裡, 覺得 Facebook 為 Front-end developer 做了很多好事。

Different file response for independent work

下面是設計 react-redux components 用到的關鍵 Function和元件:

<Provider>

Provider 是特殊的 React Component,雖然具有一般 Component 的特性,但它的出現只是為了將 Store 作為 props 的參數傳入, 不必重覆性地傳遞 Store。只需要在渲染(Render) Root Component 時即可 (more):

import { Provider } from 'react-redux'
let store = createStore(todoApp)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

connect( … )

它的作用是連結 React component 和 Redux, 自動化地從 Redux 的 State Tree 中讀取數據,並通過 props 來連結數據和要渲染的 Component (more)。

  1. 注入 store 到 Root Component

store 以參數的方法傳到<Provider> ,Root Component :

const HelloWorld = connect(
mapStateToProps,
mapDispatchToProps
)(Hello)

2. 連結 store 到 presentational components

React 和 Redux 是2個獨立的 Module, 雙方可以完全分開地獨立使用和測試. 關系就像蕃茄醬和薯條, 分開使用都不錯, 一起使用可發揮到300%的放大效益。

要連接 React 元件到 資料流模組 Redux 有很多方法, Facebook 建議使用 mapStateToProps mapDispatchToProps 將要使用到的 Function 和 Data 使用 props 的參數形式傳到 Component:

mapStateToProps : Redux store 中的目標 state 傳到 props

mapDispatchToProps: 將 Redux 中的 dispatch function 傳到 props, 之後在 this.props.onClick 中可以修改 store 的值

const mapStateToProps = (state, ownProps) => {
return {
message: state.helloWorld.message
}
}
const mapDispatchToProps = (dispatch, ownProps) => {
return {
onClick: () => {
dispatch({ type: HELLO_WORLD })
}
}
}
const HelloWorld = connect(
mapStateToProps,
mapDispatchToProps
)(Hello)

必須知道的 NPM Module

下面是幾個重要的 Module, 詳細閱讀可以更了解這個 repository 的運作:

react-redux

Redux&React bindings are not included in Redux by default. You need to install npm package react-redux explicitly. 
This assumes that you’re using npm package manager with a module bundler like Webpack or Browserify to consume CommonJS modules.

webpack-dev-middleware

It’s a simple wrapper middleware for webpack. It serves the files emitted from webpack over a connect server. This should be used for development only (more).
* No files are written to disk, it handle the files in memory

webpack-hot-server-middleware

It is designed to be used in conjunction with webpack-dev-middleware, to hot update Webpack bundles on the server (more).

Remark

下面是幾個重要的筆記, 我見過很多次而且常常忘它們的功能:

HMR

Hot Module Replacement’, 或叫作 ‘hot module swapping’。
在程式運行期間, 當更新程式時瀏覽器自動更新而不需按下 Refresh (more)。

combineReducers( ... )

最後以一個 Object 的

It turns an object whose values are different reducing functions into a single reducing function you can pass to createStore.

createStore ( … )

To create a store the createStore(reducer, [initialState], [enhancer]) which will be passed in <Provider>:

import { createStore, combineReducers } from 'redux'
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([ action.text ])
default:
return state
}
}
function prefixTodos(state = [], action) {
switch (action.type) {
case 'PRE_ADD_TODO':
return state.concat([ 'pre_'+action.text ])
default:
return state
}
}
const mixReducers= combineReducers({todos, prefixTodos})
let store = createStore(mixReducers, [ 'Use Redux' ])

subscribe( … ) VS connect(…)

They are doing the same things in Redux, but React officially announces that NOT advise you to use store.subscribe(), for the reasons that React Redux makes many performance optimizations that are hard to do by hand.

Container Components VS Presentational Components

Components could be divided into two categories, I also heard Fat and Skinny, Smart and Dumb, Stateful and Pure, Screens and Components (from):

  • Container Components: Provide the data and behavior to presentational or other container components.
  • Presentational Components: 1) Have no dependencies on the rest of the app, 2) Are concerned with how things look.