React 本身不是框架,因為功能單一不全面,但加上其他輔助套件可以讓整體作為框架來使用。
本篇會介紹兩個輔助套件 – React Router 和 Redux。
這兩個套件將實現 Route 管理以及資料流的統一,讓 React 不只是 React,讓 View 不只是 View,而是具有 MVCR 的完整前端框架。


目錄

  1. React Router
    1. 概念 - Front-End Routing
    2. 原理
      1. 假網址
      2. history
    3. Components of React-Router
    4. 使用範例
      1. main.jsx (程式進入點)
      2. routes.jsx
      3. App.jsx
  2. Redux
    1. 概念 - Flux
      1. Actions & Action Creators
      2. Dispatcher
      3. Stores
      4. Controller Views
      5. 使用 Flux 的好處
    2. Redux 概述
    3. Redux 三大原則
    4. Flux 和 Redux 的異同
      1. Action Creator & Actions
      2. Reducer
      3. Store
    5. 範例
      1. Action
      2. Action Creator
      3. Reducer
      4. Store
      5. View

React Router

概念 - Front-End Routing

對於 React 來說,SPA (Single Page Application,單頁面應用) 是很常見的一種網站形式 (例如 FB),因為這樣才能把 React 的特色發揮的淋漓盡致,而 SPA 一般來說不會 Reload,所有頁面更新都用 ajax 和重繪來完成。
所以正常來說 SPA 會有多個 API 當作 ajax 的接口,還有一個 view 作為顯示的頁面,view 頁面的切換就必須靠 Front-End Routing (前端轉址) 來完成。
但一般的 Routing 都是 Back-End Routing,不支援 Front-End Routing,但
React Router 可以做到 Front-End Routing。

原理

假網址

React Router 會用 js 幫你讀 URL,依照 URL 替換 Component,進行前端轉址。

history

轉址方式主要分為 browserHistory、hashHistory 和 memoryHistory 三種。

  • browserHistory
    介紹:官方範例使用的轉址方式,利用 html5 的 History API (history.pushState() & replaceState() & popstate()) 來進行網址的假修改。
    原理:詳細可以看這篇
    優點:網址簡潔好看。
    缺點:History API 是 html5 的特性,因此一些較老舊的瀏覽器就 GG 了 (例如萬惡的 IE6),這種情況下頁面還是可以顯示,但是就免不了 reload 了。另外,需要有後端轉址框架 (例如前一篇提到的 Express) 配合而不能單純以靜態頁面顯示,所以後端轉址框架要設定 * (萬用字元) 指到 view,不然一旦 reload 就會跟我的女友一樣 404。
    PS:browserHistory 有三種常見的情況會觸發 reload:瀏覽器不支援 History API、使用者在子頁面按 F5 的時候和直接用子頁面的假網址連進來的時候。

  • hashHistory
    介紹:React Router 預設使用的轉址方式,利用瀏覽器不會將錨點變化視做頁面變化的特性來轉址。
    原理:設定及讀取 location.hash,並使用 location.replace() 來轉址,onhashchange 來偵測網址變更。
    優點:所有瀏覽器都適用 (錨點早期的瀏覽器就會吃了),且不需要依靠後端轉址框架。(利用錨點的特性,瀏覽器不會 reload)
    缺點:網址中有 #_k 會影響網址美觀。

  • memoryHistory
    介紹:將 state 存入 memory 中,URL 不會變化,reload 時會回到 最初的頁面。
    優點:
    缺點:

詳細原理可以看這篇

Components of React-Router

  • Router
    React Router 中必用的元件,能保持 UI 和 URL 的同步。
    屬性 (props):
    • children / routes (必填)
      跟一般元件的 children 一樣是用來傳入子元件的,同樣地,也可以將子元件用兩個 tag 括住。
      子元件:
      • Route
        聲明 URL 和 Component 的映射關係。
        屬性 (props):
        • path
          設定 URL 路徑。
        • component
          URL 映射到的 Component。當 URL 和 path 匹配時,此 Component 將會被渲染。
          並且,父子 Route 的 Component 也具有父子關係。
          因此父 Route 的 Component 中使用 this.props.children 可以存取子 Route 的 Component。
        • components
          同 component,只是它的參數是 JSON,且可一次設定多個 Component。
          父 Route 的 Component 中使用 this.props.[key] 可以存取子 Route 的對應 key 值 Component。
        • getComponent
          參數:location, callback
          同 component,但是是非同步函數。
        • children
          跟一般元件的 children 一樣是用來傳入子元件的,同樣地,也可以將子元件用兩個 tag 括住。
        • onEnter
          route 進入前調用。
        • onLeave
          route 離開前調用。
      • IndexRoute
        當 URL 和父 route 的 path 一致時,會進入該父 route 的 IndexRoute。
        屬性 (props):除了沒 path 之外,與 Route 的 props 一樣。
      • PlainRoute
        Route 的 JS 版本 (Route 是 JSX)。
        屬性 (props):除了列在下面的之外,與 Route 的 props 一樣。
        • childRoutes
          同 Route 的 children。
        • getChildRoutes
          參數:location, callback
          同 childRoutes,但是是非同步函數。
        • indexRoute
          當 URL 和 PlainRoute 的 path 一致時,會進入該 PlainRoute 的 indexRoute。
        • getIndexRoute
          參數:location, callback
          同 indexRoutes,但是是非同步函數。
      • Redirect
        不改變舊的 URL,重定向到其他 route。
        屬性 (props):
        • from
          重定向的來源路徑。
        • to
          重定向的目標路徑。
        • query
          用 JSON 指定目標路徑的 query。
      • IndexRedirect
        將 Redirect 套用在父 route 時使用。
        屬性 (props):除了沒 from 之外,與 Redirect 的 props 一樣。
    • history
      之前有提到 browserHistory 和 hashHistory,在這可以設定要使用哪一種。
      EX:
      history={browserHistory}
    • createElement
      參數:Component, props
      Route 渲染傳入的 Component 時會觸發。預設是傳入一個叫 createElement 的函數來處理。
      1
      2
      3
      function createElement(Component, props) {
      return <Component {...props}/>
      }

如果要在 Route 以及 Component 間插入 middleware (EX:Relay) 就需要改寫這函數了。

  • onError
    參數: error
    用來處理 Route 拋出的錯誤。
  • onUpdate
    參數:無
    當 URL 改變時會觸發。

    • Link
      類似 <a>,觸發 URL 以及 Component 的切換。

      洨知識:
      URL 分成七部份:協議 (EX:https)、域名 (這個還可以在細分成頂級、次級…)、port、相對路徑、資源、query (?,就是 get 方法的參數)、錨點 (#)。

    <a>href 不同,協議、域名、port 在 Link 中都是不能更改的。
    屬性 (props):

  • to
    <a>href 屬性中相對路徑 + 資源的部份,但須是 Router 有定義的路徑。
  • query
    <a>href 屬性中 query 的部份。
    PS. 可以寫在 to 中。
  • hash
    <a>href 屬性中錨點的部份。
    要補上 #
    PS. React Router 還不能自動滾動頁面,所以錨點不能滾動頁面,但還是可以 hashHistory。
  • params
    設定 Route 的參數。
  • state
  • activeClassName
    設定 active 的 ClassName 以套用 css。
    PS. 若 Link to 的對象 URL 等同於當前 URL 或其前綴則稱為 active 狀態。
  • activeStyle
    設定 active 的 Style。
  • onClick
    參數:e
    點擊時觸發的事件。
  • 其他
    <a> 可以使用的屬性 <link> 也可以使用,並直接套用到 <a> 上去。
    • IndexLink
      若是想要連到 IndexRoute 而使用 Link,那麼在同層 Route 中此 Link 將會是 active 狀態,因為當前 URL 必為同層 Route URL 的前綴。此時可使用 IndexLink,在同層 Route 就不會是 active 的狀態了。
      PS. to IndexRoute 時才有此效果。
    • RouterContext

使用範例

main.jsx (程式進入點)

1
2
3
4
5
6
import routes from './routes.jsx'
import {Router, browserHistory, hashHistory} from 'react-router'
ReactDOM.render((
<Router children={routes} history={browserHistory}/>
), document.getElementById('root'));

routes.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import {Route, IndexRoute} from 'react-router';
import App from './App.jsx';
import APage from 'A/APage.jsx';
import BPage from 'B/BPage.jsx';
import CPage from 'C/CPage.jsx';
export default (
<Route path="/" component={App}>
<IndexRoute component={CounterPage}/>
<Route path="counter" component={CounterPage}/>
<Route path="about/:name" component={AboutPage}/>
<Route path="todo" component={TodoPage}/>
</Route>
);

App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React from 'react'
import {Link} from 'react-router'
require('vendor/vendor.scss');
export class App extends React.Component {
render() {
return (
<div>
React Example
<div>
<Link to='/counter'>
<button> Counter Page</button>
</Link>
<Link to='/about/corn'>
<button> About Page</button>
</Link>
<Link to='/todo'>
<button>Todo Page</button>
</Link>
</div>
{this.props.children}
</div>
)
}
}

Redux

概念 - Flux

Flux 是 Facebook 內部搭配 React 使用的一種概念、架構、設計模式,而不是框架或函式庫。
主要理念是單向資料流,如下圖:

下面就來說明個別的功能。

Actions & Action Creators

Action Creators 是輔助函式,可以在 View 呼叫,進而把 Actions 和 data 發送給 Dispatcher。Actions 實際上是透過 Dispatcher 來分派 data 的。
Actions 是唯一可以改變 Stores 裡面的數據的方法。

Dispatcher

接收 Actions 以及分派 payloads (要傳遞的資料) 到所有被註冊的回呼函式(callback)。
一個專案只能有一個 Dispatcher。

Stores

管理 state 和處理邏輯 (接收資料的方法和被註冊到 Dispatcher 的回呼函式) 的容器。

Controller Views

負責管理 state 的 React Component。從 Stores 接受 state,並把 state 透過 props 往下傳遞到子 Component。

以下這張圖說明的很清楚:

英文好的可以看這張:

使用 Flux 的好處

Component 之間有共用的狀態資料儲存地。
不使用 Flux 的話,state 儲存在各自的 this.state 中。若不同 Component 之間想互相通知 state 的改變的話,需要一層一層傳遞 callback,很繁雜。

Redux 概述

  • 是 JS 的狀態容器,提供可預測的狀態管理
  • 由 Flux 演變而來,但避開了 Flux 的複雜性,單純易用
  • Redux 已納入 Facebook 官方項目,成為前端狀態管理的主流解決方案
  • 跟 React 沒有相依關係,可以單獨使用或搭配其他前端框架使用

Redux 三大原則

  1. 單一資料來源
  2. state 是唯讀的
  3. 使用純函數來執行 state 的修改

Flux 和 Redux 的異同

Action Creator & Actions

  • Flux:經由 Action creator 直接建立 action 並且 dispatch 出去。
  • Redux:Action Creator 只會產生一個純粹的 javascript object,你要自己 dispatch 出去。

Reducer

  • Flux:沒有 Reducer。
  • Redux:給你現在的狀態跟要執行的動作,傳回一個新的狀態。

Store

  • Flux:有多個 Stores。
  • Redux:只有一個 Store。dispatch 這個動作是用 store 來達成,把原本的 Dispatcher 這個東西拿掉了。

範例

Action

1
2
3
4
5
6
export const ADD_TODO = 'ADD_TODO';
action = {
type: ADD_TODO,
text
}

Action Creator

1
2
3
4
5
6
7
8
9
10
// actions/TodoActions
export const ADD_TODO = 'ADD_TODO';
export function addTodo(text)
{
return{
type: ADD_TODO,
text
}
}

Reducer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// reducers/AppReducers
import {ADD_TODO} from 'actions/TodoActions';
export function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
text: action.text,
completed: false
}
];
default:
return state;
}
}

Store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// index.jsx
import { render } from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from 'containers/App'
import AppReducers from 'reducers/AppReducers'
let store = createStore(AppReducers, window.devToolsExtension && window.devToolsExtension());
let rootElement = document.getElementById('root');
render(
<Provider store={store}>
<App />
</Provider>,
rootElement
);

View

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// containers/App
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import { addTodo, toggleTodo } from 'actions/TodoActions'
import AddTodo from 'components/Todo/AddTodo'
import TodoList from 'components/Todo/TodoList'
class App extends Component {
render() {
const { dispatch, todos } = this.props
return (
<div>
<AddTodo onAddClick={text => dispatch(addTodo(text))}></AddTodo>
<TodoList todos={todos}></TodoList>
</div>
)
}
}
export default connect(state => state)(App);

本篇有許多地方尚未寫的很完整,不過最近繁忙,會抽空儘快補上。

這篇就這樣告一段落囉,下篇 講的是 React Native。如果有興趣可以繼續收看~

這篇會不定期更新,如果有打錯或有任何疑問都歡迎留言告知喔~
我們下篇再見~