1. 程式人生 > >Flux、Redux到react-redux發展衍變三部曲之react-redux解讀

Flux、Redux到react-redux發展衍變三部曲之react-redux解讀

首先要知道,Redux只是一種MVC的機制,它可以執行在任何的前端框架中。react-redux則是對Redux的核心模組(如store)進行封裝,並加入了一些有利於react開發的模組,這樣就可以更便捷的在react中運用redux。

本篇將詳細介紹react-redux開發中的常用模組。在閱讀本文前,假設您已經基本理解了Redux的執行機制,若對其機制還不是很清楚,請參考上一篇《Flux、Redux到react-redux發展衍變之Redux解讀》

下面是兩個關於react-redux應用的demo,可以作為本篇知識的參考案例:

TODOS:https://github.com/smallH/react-redux-todomvc-demo.git

購物車:https://github.com/smallH/react-redux-shoppingcart-demo.git

結合上篇,執行一下上面兩個例子,就可以學會 'react-redux' 用法,本篇不再累贅,這裡主要介紹其一些注意的細節和外掛。

 

mapDispatchToProps的多種入參方式

mapDispatchToProps的作用是:告訴元件所需的actions函式並把它們作為入參props傳遞到元件裡,供元件使用。

大多數情況下使用標準的引用方法如下:

// actions.js
export const add = () => ({ type: 'ADD' })

// Link.js
class Link extends React.Component {
	render() {
		const {addAction} = this.props;
		return(
		    <div onClick={addAction}>增加一條</div>
		)
	}
}
export default Link

// ContainersLink.js 給Link元件定義一個addAction入參事件:增加一條記錄
import { connect } from 'react-redux'
import { add } from '../actions'
import Link from '../components/Link'

const mapDispatchToProps = (dispatch, ownProps) => ({
	addAction: () => {
	    dispatch(add())
	}
})
export default connect(null, mapDispatchToProps)(Link)

有沒有發現程式碼有點多,於是,我們可以優化為:

// Link.js
class Link extends React.Component {
	render() {
		const {add} = this.props;
		return(
		    <div onClick={add}>增加一條</div>
		)
	}
}
export default Link

// ContainersLink.js
import { connect } from 'react-redux'
import { add } from '../actions'

export default connect(null, {add})(Link)

兩種用法效果是一樣的。另外,當actions.js裡需要引用的函式較多時,可以寫成:

// Link
class Link extends React.Component {
	render() {
		const { add } = this.props;
		return(
		    <div onClick={ add }>增加</div>
		)
	}
}
export default Link

// ContainersLink.js
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import * as AllActions from '../actions'

export default connect(null, AllActions)(Link)

或者如果不想通過connect,則可以使用bindActionCreators(actions, dispatch)。

 

classnames 實現樣式管理

react中樣式管理的方式有很多,有很多專案習慣直接在每個元件最底下定義常量樣式檔案style,但這種方法有一定侷限性。本篇中介紹通過classnames實現了對樣式的條件引用,對於一些現實/隱藏元件的操作非常方便。

模組安裝:

npm install classnames -S

用法有以下幾種:

classNames('foo', 'bar'); // => 'foo bar'
classNames('foo', { bar: true }); // => 'foo bar'
classNames({ 'foo-bar': true }); // => 'foo-bar'
classNames({ 'foo-bar': false }); // => ''
classNames({ foo: true }, { bar: true }); // => 'foo bar'
classNames({ foo: true, bar: true }); // => 'foo bar'

如給div增加active樣式,程式碼可以是:

import classnames from 'classnames'

// 以下兩種寫法效果是一樣的
let bool = true;
<div className={classnames({ "active": bool})}></div>
<div className={classnames({ "active": true })}></div>

上例中,可以用過計算bool值實現動態顯示/隱藏元件。

 

combineReducers 合併多個reduce

隨著應用變得複雜,需要對reducers函式進行拆分,拆分後的每一塊獨立負責管理state的一部分。combineReducers函式的作用是:把由多個不同reducer函式作為value,合併成一個最終的reducers函式,然後就可以對這個reducers呼叫createStore。合併後的reducers把各個子reduer返回的結果合併成一個state。state物件的結構由傳入的多個reducer的key決定。如state 物件的結構會是這樣:

{
  reducer1: ...
  reducer2: ...
}

如多個reducer合併實現:

// todos.js
const todos = (state = {}, action) => {
	switch(action.type) {
		case ActionTypes.UPDATE:
			console.log("todo update!");
			return state
		default:
			return state
	}
}

// visibilityFilter.js
const visibilityFilter = (state = {}, action) => {
	switch(action.type) {
		case ActionTypes.UPDATE:
			console.log("visibilityFilter update!");
			return state
		default:
			return state
	}
}

// 合併reducer
import { combineReducers } from 'redux'
import todos from './todos'
import visibilityFilter from './visibilityFilter'

const rootReducer = combineReducers({
	todos,
	visibilityFilter
})

export default rootReducer

這裡要注意,合併後reducers裡的子reducer依然會遍歷執行所有觸發的actions函式。如當觸發ActionTypes.UPDATE時,上例中會列印todo update! 和 visibilityFilter update! 

除此之外,在reducers裡的子reducer也允許二次合併,如把todos修改如下:

// todos
const todo_child1 = (state = [], action) => {}
const todo_child2 = (state = [], action) => {}

const todos = combineReducers({
	todo_child1,
	todo_child2
})

export default todos

這時state返回的是物件是 {{todo_child1, todo_child2}, visibilityFilter}

 

reselect 快取機制,演算法優化

reselect 模組使用到了快取機制,Selector可以計算衍生的資料,可以讓Redux做到儲存儘可能少的state,在元件互動操作的時候,優化了state發生變化帶來的壓力。簡單來說,它會執行函式時產生的結果儲存至記憶體中,當下一次輸入不變的情況下,直接從記憶體中獲取輸出結果。使用方法:

import { createSelector } from 'reselect'

const getTodos = state => state.todos.present

// 使用 Selector
export const getCompletedTodoCount = createSelector([getTodos], (todos) => (
	todos.reduce((count, todo) => todo.completed ? count + 1 : count, 0)
))
// 不使用 Selector
export const getCompletedTodoCount = state => 
    state.todos.reduce((count, todo) => (todo.completed ? count + 1 : count), 0);

上例中若不使用Selector,每次state發生變化都會重新計算一次。

 

redux-undo 實現撤銷重做

可以實現對每次state狀態變化的監聽和記錄,通過觸發undo和惹到實現撤銷重做的效果。如給todos模組增加undoable監聽:

import { combineReducers } from 'redux'
import todos from './todos'
import visibilityFilter from './visibilityFilter'
import undoable from 'redux-undo'

// todos 任務列表增刪改查
// visibilityFilter設定濾邏條件
const rootReducer = combineReducers({
	todos: undoable(todos),
	visibilityFilter
})

export default rootReducer

撤銷重做的觸發:

import React from 'react'
import { ActionCreators as UndoActionCreators } from 'redux-undo'
import { connect } from 'react-redux'
import classnames from 'classnames'

let UndoRedo = ({
	onUndo,
	onRedo
}) => (
	<div className={classnames({ "undoredu": true })}>
	    <div onClick={onUndo} className={classnames({ "undoredu-btn": true })}>
	     	撤銷
	    </div>
	    <div onClick={onRedo} className={classnames({ "undoredu-btn": true })}>
	      	重做
	    </div>
	 </div>
)

const mapDispatchToProps = ({
	onUndo: UndoActionCreators.undo,
	onRedo: UndoActionCreators.redo
})

UndoRedo = connect(
	null,
	mapDispatchToProps
)(UndoRedo)

export default UndoRedo

這裡要注意,要獲取經過undoable包裝後的todos狀態值時,要由state.todos 改為 state.todos.present。

 

redux-thunk 獲取dispatch控制權,實現非同步處理

redux-thunk屬於中介軟體,它可以讓action建立函式先不返回一個action物件,而是返回一個函式,函式傳遞兩個引數(dispatch,getState),在函式體內進行業務邏輯的封裝。就是在Action傳遞到Store之前再做點事情,常用於非同步處理。先看看thunk原始碼如下:

function createThunkMiddleware(extraArgument) {
  return function (_ref) {
    var dispatch = _ref.dispatch,
        getState = _ref.getState;
    return function (next) {
      return function (action) {
        if (typeof action === 'function') {
          return action(dispatch, getState, extraArgument);
        }

        return next(action);
      };
    };
  };
}

var thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;

使用方法:

import React from 'react'
import { render } from 'react-dom'
import { createStore, applyMiddleware } from 'redux'
import { Provider } from 'react-redux'
import thunk from 'redux-thunk'
import reducer from './reducers'
import App from './containers/App'

const middleware = [thunk];
const store = createStore(reducer, applyMiddleware(...middleware))

render(
	<Provider store={store}>
    	    <App />
  	</Provider>, document.getElementById('root')
)

引入thunk後,這時,原來的Action裡的函式會由:

export const addToCart = productId => {}

變為:

export const addToCart = productId => (dispatch, getState) => {}

可以發現,Action中的addToCart拿到了dispatch的控制權,這樣就可以配合axios和Promise實現非同步請求的實現。如:

// shop.js
const _get = ({
	url,
	query
}) => {
	return axios({
		method: 'get',
		url,
		params: { ...query}
	}).then((res) => {
		if(res.status == 200) {
			return res.data
		}
		return Promise.reject(res);
	}, (err) => {
		return Promise.reject(err.message || err.data);
	});
};

export const getNetProducts = () => {
	const url = "/data/products.json"
	const query = {}
	return _get({
		url,
		query
	}).then((data) => Promise.resolve(data)).catch((e) => Promise.reject(e));
}

export default {
	getNetProducts: getNetProducts
}

// actions.js
import shop from '../api/shop'
import * as types from '../constants/ActionTypes'

const receiveProducts = products => ({
	type: types.RECEIVE_PRODUCTS,
	products
})

export const getAllProducts = () => dispatch => {
	// 請求網路JSON資料
	shop.getNetProducts().then((data) => {
		dispatch(receiveProducts(data)) // 等到請求結束再觸發實現非同步
	}).catch((e) => {
		console.log("請求資料發生錯誤");
	});
}

自定義中介軟體的結構寫法如下:

const myMiddleware = (store) => (next) => (action) => {
	// 對action資料進行操作
	// 返回action物件
	next(action)
}
const middleware = [thunk, myMiddleware];

一般情況下是不需要用到自定義中介軟體的,因為npm上有提供了大部分可用的中介軟體,可自己選擇性查詢使用。

整篇的《Flux、Redux到react-redux衍變發展》三部曲介紹至此總結完畢啦,希望這些知識點的歸納能對大家有用,我會繼續努力分享更多前端知識總結給大家,共同進步。