Redux這個npm包,提供若干API讓我們使用reducer創建store,并能更新store中的數據或獲取store中最新的狀態。“Redux應用”指使用redux結合視圖層實現(React)及其他前端應用必備組件(路由庫、Ajax請求庫)組成的完成類Flux思想的前端應用。
Redux 三大原則
單一數據源
Redux思想中,一個應用永遠只有唯一的數據源。combineReducers化解了數據源對象過于龐大的問題。狀態是只讀的
Redux中,我們并不會自己用代碼來定義一個store。取而代之的是,我們定義一個rducer,它的功能是根據當前觸發的action對當前應用的狀態(state)進行迭代,這里我們斌沒有直接修改應用的狀態,而是返回一份全新的狀態。
Redux提供的createStore方法會根據reducer生成store。最后,我們利用 store.dispatch 方法來達到修改狀態的目的。狀態修改均有純函數完成
在Redux中,我們通過定義reducer來確定狀態的修改。
Redux核心API
Redux的核心是一個store,這個store由Redux提供的 createStore(reducers, [,initialState])方法生成。
Redux里,負責響應action并修改數據的角色是reducer。其函數簽名為reducer(previousState, action ) => newState
。所以,reducer的職責就是根據 previousState 和 action 計算出新的 newState。
// MyReducer.js
const initialState = {
todos: [],
}
// 我們定義的todos這個reducer在第一次執行的時候,會返回 { todos: [] }作為初始化狀態
function todos(previousState = initalState, action) {
switch(action.type) {
case 'xx': {
// 具體的業務邏輯
}
default:
return previousState;
}
}
Redux = Reducer + Flux
通過createStore方法創建的store是一個對象,它本身又包含4個方法
- getState():獲取store中當前的狀態
- dispatch(action): 分發一個action,并返回這個action,這是唯一能改變 store 中數據的方式
- subscribe(listener):注冊一個監聽者,它在store發生變化時被調用
- replaceReducer(nextReducer):更新當前store里的reducer,一般只會在開發模式中調用該方法
與React綁定
react-redux提供了一個組件和一個API幫助Redux和React進行綁定,一個是React組件<Provider />,一個是 connect(). <Provider />接受一個store作為props,是整個Redux應用的頂層組件,connect()提供整個React應用的任意組件中獲取store中數據的功能。
Redux middleware
它提供了一個分類處理action的機會。
面對多樣的業務場景,單純地修改 dispatch 或 reducer 的代碼顯然不具有普適性,我們需要的是可以組合的、自由插拔的插件機制,這一點 Redux借鑒了 Koa (它是用于構建 Web 應用的Node.js 框架)里 middleware 的思想。
理解middleware機制
Redux提供了applyMiddleware方法來加載middleware。
Redux中的applyMiddleware源碼
import compose from './compose'
export default function applyMiddleware(...middlewares) {
return (next) => (reducer, initialState) => {
let store = next(reducer, initialState)
let dispatch = store.dispatch
let chain = []
// 把store的getState方法和dispatch方法分別直接或間接賦值給middlewareAPI
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
// 讓每個middleware帶著middlewareAPI這個參數分別執行一遍
// 執行完后,獲得數組 [f1,f2,f3,f4,...,fn]
// middlewareAPI第二個箭頭函數返回的匿名函數,因為閉包,每個匿名函數都可以訪問相同的store,即middlewareAPI
/*
middlewareAPI中的dispatch為什么要用匿名函數包裹呢?
我們用 applyMiddleware 是為了改造 dispatch,所以 applyMiddleware 執行完后,dispatch 是 變化了的,而 middlewareAPI 是 applyMiddleware 執行中分發到各個 middleware 的,所以 必須用匿名函數包裹 dispatch,這樣只要 dispatch 更新了,middlewareAPI 中的 dispatch 應 用也會發生變化。
*/
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
- 函數式編程思想設計
middleware設計的是一層層包裹的匿名函數,這其實是函數式編程中的currying,他是一種私用匿名單參數函數來實現多參數函數的方法。
currying的middleware結構的好處有以下兩點:
- 易串聯:currying函數具有延遲執行的特性,通過不斷currying形成的middleware可以累積參數,再配合組合(compose)的方式,很容易形成pipeline來處理數據流
- 共享store:在applyMiddleware執行的過程中,store還是舊的,但是因為閉包的存在,applyMiddleware完成后,所有的middleware內部拿到的store是最新且相同的。
import { createStore, applyMiddleware, compose } from 'Redux';
import rootReducer from '../reducers';
const finalCreateStore = compose(
// 在開發環境中使用 middleware
applyMiddleware(d1, d2, d3),
DevTools.instrument()
)
給middleware分發store
let newStore = applyMiddleware(mid1,mid2,mid3,...)(createStore)(reducer, null)
看注釋。組合串聯middleware
dispatch = compose(...chain)(store.dispatch);
// compose 實現方式
function compose(...funs) {
return arg => func.reduceRight((composed, f) => f(composed), arg)
}
compose(...funcs)返回一個匿名函數,其中funcs就是chain數組。當調用reduceRight時,依次從funcs數組的右端取一個函數fx拿來執行,fx的參數composed就是前一次fx+1執行的結果,而第一次執行的fn(n代表chain的長度)的參數arg就是 store.dispath.假設n=3
dispatch = f1(f2(f3(store.dispatch)))
這時調用新dispatch時,每一個middleware都依次執行了。
- 在middleware中調用dispatch會發聲什么
compose之后,所有的middleware算是串聯起來了。可是還有一個問題,在分發store時,我們提到過每個middleware都可以訪問store,即middlewareAPI這個變量,也可以拿到store的dispatch屬性。那么,在middleware中調用store.dispatch()會發生什么,和調用next()有什么區別?
const logger = store => next => action => {
console.log('dispatch:', action);
next(action);
console.log('finish:', action);
};
const logger = store => next => action => { c
onsole.log('dispatch:', action);
store.dispatch(action);
console.log('finish:', action);
};
在分發 store 時我們解釋過,middleware 中 store 的 dispatch 通過匿名函數的方式和最終 compose 結束后的新 dispatch 保持一致,所以,在 middleware 中調用 store.dispatch() 和在其他 任何地方調用的效果一樣。而在 middleware 中調用 next(),效果是進入下一個 middleware。
這就是一個洋蔥模型
next代表下一個執行的中間件,每次return回去的都是一個未執行的函數,只有最后調用才能執行。
// 實現之后的效果
// 這時候返回的是一個經過層層中間件封裝的dispatch,新的dispatch函數
newDispatch = M1(M2(M3(dispatch)))
// M1中的next 就是M2
// M2中的next就是M3
// M3中的next就是dispatch,執行dispatch
// 調用
newDispatch(action)
正常情況下,如上圖左,我們分發一個action時,middleware通過next(action)一層層傳遞和處理action直到Redux原生的dispathc。當某個middleware使用store.dispatch(action)分發action,會發聲右圖的情況,就會形成無限循環。那么store.dispatch(action)的用武之地在哪里呢?
異步請求的時候,使用到Redux Thunk
const thunk = store => next => action => {
typeof action === 'function'?
action(store.dispatch, store.getState) : next(action)
}
Redux Thunk會判斷action是否是函數。如果是,執行action,否則繼續傳遞action到下一個middleware。
const getThenShow = (dispatch, getState) => {
const url = 'http://xxx.json'
fetch(url)
.then((res) => {
dispatch({
type: 'SHOW_MESSAGE_FOR_ME',
message: res.json(),
})
}).catch( ()=> {
dispatch({
type: 'FETCH_DATA_FAIL',
message: 'error'
})
} )
}
// 再應用中調用 store.dispatch(getThenShow)
Redux異步流
使用middleware簡化異步請求
- redux-thunk
Thunk函數實現上就是針對多參數的currying以實現對函數的惰性求值。任何函數,只要參數有回調函數,就能寫成Thunk函數的形式。
redux-thunk的源代碼:
function createThunkMiddleware(extraArg) {
return ({dispath, getState} => next => action => {
if ( typeof action === 'function' ) {
return action(dispatch, getState, extraArg)
}
return next(action)
})
}
- redux-promise
抽象promise來解決異步流問題。
redux-promise 兼容了 FSA 標準,也就是說將返回的結果保存在 payload 中。實現過程非常容易理解,即判斷 action 或action.payload是否為 promise,如果是,就執行 then,返回的結果再發送一次 dispatch。
使用ES7的async和await語法,簡化異步過程
const fetchData = (url, params) => fetch(url, params);
async function getWeather(url, params) {
const result = await fetchData(url, params);
if( result.error ) {
return {
type: 'GET_WEATHER_ERROR',
error: result.error
}
}
return {
type: 'GET_WEATHER_SUCCESS',
payload: result
}
}
- redux-composable-fetch
實際請求中,加上loading狀態
這時候異步請求的action
{
url: '/api/weather.json',
params: {
city: encodeURI(city),
},
types: ['GET_WEATHER', 'GET_WEATHER_SUCESS', 'GET_WEATHER_ERROR'],
}
和FSA不一樣了,沒有types,有了url和type代表請求狀態
const fetchMiddleware = store => next => action => {
if (!action.url || !Array.isArray(action.types)) {
return next(action);
}
const [LOADING, SUCCESS, ERROR] = action.types;
next({
type: LOADING,
loading: true,
...action,
});
fetch(action.url, { params: action.params })
.then(result => {
next({
type: SUCCESS,
loading: false,
payload: result,
});
})
.catch(err => {
next({
type: ERROR,
loading: false,
error: err,
});
});
}
使用middleware處理復雜異步流
1.輪詢
多異步串聯
使用Promiseredux-saga
最優雅通用的解決方法,有靈活而強大的協程機制,可以解決任何復雜的異步交互。
Redux和路由
我們需要一個這樣的路由系統,它既能利用React Router 的聲明式特性,又能將路由信息整合進 Redux store 中。
React Router
1.基本原理
-
React Router特性
React Router與React對比
- 聲明式的路由
// 實例
import { Router, Route, browserHistory } from 'react-router';
const routes = (
<Router history={browserHistory}>
<Route path='/' component{App} />
</Router>
)
- 嵌套路由及路徑匹配
import { Router, Route, IndexRoute, browserHistory } from 'react-router';
const routes = (
<Router history={browserHistory}>
<Route path="/" component={App}>
<IndexRoute component={MailList} />
<Route path="/mail/:mailId" component={Mail} />
</Route>
</Router>
);
App 組件承載了顯示頂欄和側邊欄的功能,而 React Router 會根據當前的 url 自動判斷該顯示郵件列表頁還是詳情頁:
. 當 url 為 / 時,顯示列表頁;
. 當 url 為 /mail/123 時,顯示詳情頁。
- 支持多種路由切換方式
hashChange 或是 history.pushState。hashChange 的方式擁有良好的瀏覽器兼容性,但是 url 中卻多了丑陋的 /#/ 部分;而 history.pushState 方法則能給我們提供優雅的 url,卻需要額外的服務端配置解決任意路徑刷新的問題。
React Router Redux
當我們采用 Redux 架構時,所有的應用狀態必須放在一個單一的 store 中管理,路由狀態也不例外。而這就是 React Router Redux 為我們實現的主要功能。
- React Router與Redux store綁定
React Router Redux 提供了簡單直白的 API——syncHistoryWithStore 來完成與 Redux store的綁定工作。
import { browserHistory } from 'react-router'
import { syncHistoryWithStore } from 'react-router-redux'
import reducers from '<project-path>/reducers'
const store = createStore(reducers);
const history = syncHistoryWithStore(browserHistory, store);
- 用Redux的方式改變路由
對Redux的store進行增強,以便分發的action能被正確識別
import { browserHistory } from 'react-router';
import { routerMiddleware } from 'react-router-redux';
const middleware = routerMiddleware(browserHistory);
const store = createStore(
reducers,
applyMiddleware(middleware)
);
使用
import { push } from 'react-router-redux';
// 切換路由到 /home
store.dispatch(push('/home'));
Redux 與 組件
Redux中,強調了3中不同類型的布局組件:Layouts、Views和Components。它常常是無狀態函數,傳入主體內容的children屬性。
const Layout = ({ children } => {
<div className = 'container'>
<Header />
<div className="contaier">
{ children }
</div>
</div>
})
- Views
子路由入口組件,描述子路由入口的基本結構,包含此路由下所有的展示型組件。
@connect((state) => {
//...
})
class HomeView extends Component {
render() {
const { sth, changeType } = this.props;
const cardProps = { sth, changeType };
return (
<div className="page page-home">
<Card {...cardProps} />
</div>
);
}
}
3、Components
末級渲染組件,描述了從路由以下的子組件。包含具體的業務邏輯和交互,但所有的數據和action都是油Views傳下來。
class Card extends Components {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(opts) {
const { type } = opts;
this.props.changeType(type);
}
render() {
const { sth } = this.props;
return (
<div className="mod-card">
<Switch onChange={this.handleChange}>
// ...
</Switch>
{sth}
</div>
);
}
}