Redux
Redux 是 JavaScript 狀態容器,提供可預測化的狀態管理
內容目錄
Redux?
- JavaScript單頁應用開發日趨複雜 --> state more --> state在什麼時候,由於什麼原因,如何變化已然不受控制
- 分開變化和非同步 --> React試圖在檢視層禁止非同步和直接操作DOM來解決這個問題 --> React依舊把處理state中資料的問題留給了我們
- Redux由Flux演變而來,但受Elm的啟發,避開了Flux的複雜性
- 你可能並不需要Redux
核心概念
三大原則
- 單一資料來源:整個應用的 state 被儲存在一棵 object tree 中,並且這個 object tree 只存在於唯一一個 store 中。
- State 是隻讀的:唯一改變 state 的方法就是觸發 action,action 是一個用於描述已發生事件的普通物件。
- 使用純函式來執行修改:為了描述 action 如何改變 state tree ,你需要編寫 reducers。
先前技術
- Redux 可以被看作 Flux 的一種實現嗎? 是,也可以說 不是。
- 和 Flux 一樣,Redux 規定,將模型的更新邏輯全部集中於一個特定的層(Flux 裡的 store,Redux 裡的 reducer)
- Flux 和 Redux 都不允許程式直接修改資料,而是用一個叫作 “action” 的普通物件來對更改進行描述。
- 而不同於 Flux ,Redux 並沒有 dispatcher 的概念(原因是它依賴純函式來替代事件處理器)。
- 和 Flux 的另一個重要區別,是 Redux 設想你永遠不會變動你的資料。
- Elm 是一種函數語言程式設計語言,更新遵循 (state, action) => state 的規則
基礎
action:事件響應
reducer:純函式,資料改變
store:生成真正的store資料樹
示例
app.js:
import React from 'react';
import ReactDOM from 'react-dom';
import { HashRouter, Route } from 'react-router-dom';
import { asyncComponent } from 'AsyncComponent';
import HeaderMenu from './components/heard';
import SiderMenu from './components/sider';
import thunk from 'redux-thunk';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import reducer from './reducer';
import './css/main.css';
const demo1 = asyncComponent(() => import(/* webpackChunkName: 'demo1' */ './demo1'));
const demo2 = asyncComponent(() => import(/* webpackChunkName: 'demo2' */ './demo2'));
let store = createStore(reducer, applyMiddleware(thunk));
const router = (
<Provider store={store}>
<HashRouter>
<div>
<HeaderMenu />
<div className="ant-layout ant-layout-has-sider layout">
<SiderMenu />
<Route exact path="/" component={demo1} />
<Route exact path="/demo/demo1" component={demo1} />
<Route exact path="/demo/demo2" component={demo2} />
</div>
</div>
</HashRouter>
</Provider>
);
ReactDOM.render(router, document.getElementById('content'));
複製程式碼
index.js:
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Layout, Breadcrumb, Button } from 'antd';
import 'date-util';
import ModalTip from 'modalTip';
import { changeNeedCode } from './action';
const { Content } = Layout;
/**
* demo1
*/
const mapStateToProps = (state, ownProps) => {
return {
demo1Store: state.demo1Store
};
};
const mapDispatchToProps = (dispatch, ownProps) => {
return {
changeNeedCode: bindActionCreators(changeNeedCode, dispatch)
};
};
class demo1 extends React.Component {
componentDidMount() {
let that = this;
let changeNeedCode = that.props.changeNeedCode;
changeNeedCode('zhanghao');
}
render() {
let that = this;
return (
<Layout style={{ padding: '0 24px 24px' }}>
<Breadcrumb style={{ margin: '12px 0' }}>
<Breadcrumb.Item>demo</Breadcrumb.Item>
<Breadcrumb.Item>demo1</Breadcrumb.Item>
</Breadcrumb>
<Content style={{ background: '#fff', padding: 24, margin: 0, minHeight: 280 }}>
<div>
<div>{new Date().format('yyyy-MM-dd hh:mm:ss')}</div>
<div>{that.props.demo1Store.welcomeText}</div>
<div>是否需要驗證碼{that.props.demo1Store.needCode}</div>
<div>
<Button onClick={() => that._showModalTip('info')}>Info</Button>
<Button onClick={() => that._showModalTip('success')}>Success</Button>
<Button onClick={() => that._showModalTip('error')}>Error</Button>
<Button onClick={() => that._showModalTip('warning')}>Warning</Button>
</div>
</div>
</Content>
</Layout>
);
}
_showModalTip(type) {
switch (type) {
case 'info':
ModalTip.infoTip('info');
break;
case 'success':
ModalTip.successTip('success');
break;
case 'error':
ModalTip.errorTip('error');
break;
case 'warning':
ModalTip.warningTip('warning');
break;
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(demo1);
複製程式碼
action.js:
import fetch from 'fetch/fetch';
import ModalTip from 'modalTip';
let checkNeedCode = nickName => {
let params = {};
params.nickName = nickName;
params.t = new Date().getTime();
return fetch
.post('/oauth/checkLogin', params)
.then(res => {
return res.needCode;
})
.catch(e => {
ModalTip.warningTip(e.message);
});
};
exports.changeNeedCode = nickName => async dispatch => {
let needCode = await checkNeedCode(nickName);
dispatch({ type: 'change_needCode', needCode: needCode });
};
複製程式碼
reducer.js:
import { combineReducers } from 'redux';
const demo1Store = (state = { welcomeText: '', needCode: '' }, action = {}) => {
switch (action.type) {
case 'change_welcomeText':
return { ...state, welcomeText: action.welcomeText };
case 'change_needCode':
return { ...state, needCode: action.needCode };
default:
return { ...state };
}
};
const demo2Store = (state = { columnName: '', newsTitle: '', selectText: '選擇器1', dUserCode: '' }, action = {}) => {
switch (action.type) {
case 'change_columnName':
return { ...state, columnName: action.columnName };
case 'change_newsTitle':
return { ...state, newsTitle: action.newsTitle };
case 'change_selectText':
return { ...state, selectText: action.selectText };
case 'change_dUserCode':
return { ...state, dUserCode: action.dUserCode };
default:
return { ...state };
}
};
export default combineReducers({
demo1Store: demo1Store,
demo2Store: demo2Store
});
複製程式碼
Reduxm
為了更好的使用Redux,進行二次封裝
內容目錄
redux存在的問題
- 一份store樹,離開頁面再次進入,資料不會初始化
- reducer拆分造成彙總困難
- action的type管理混亂,重複問題
- 繁雜的使用規則,index頁面action和store引入,純函式reducer大量case僅僅為了改變一個值
設計思想(@修飾器)
- connectstore對ReactDom繼承注入action和store,重寫componentWillUnmount生命週期,離開頁面自動觸發store初始化
- 使用@修飾器、store對reducer提取存入reducerfactory,action對action提取存入actionfactory和actiontypefactory
- action的type跟隨store定義,並隱式新增名稱空間解決type重複問題、以及隱式case定義省略大量case
api
/**
* 資料注入層
* 提供createStore、getDevTools、getActionType、getAllInitData四個方法
*
* createStore方法,繫結資料到整個react路由層
* @params router(react路由), debug(是否開啟除錯工具)
* @return reactRouter
*
* getDevTools方法,獲取除錯工具檢視
* @return DevTools(除錯工具檢視)
*
* getActionType方法,獲取storeName下所有actionType
* @param storeName(資料層名稱)
* @return {}(storeName下所有actionType)
*
* getAllInitData方法,獲取storeName下所有初始資料
* @param storeName(資料層名稱)
* @return {}(storeName下所有初始資料)
*/
import Store from './store/store';
/**
* store修飾器,處理整個store層存入資料工廠
* @params storeName(資料層名稱), allActionType(改變整個資料層的actionType), allStoreLogs(改變整個資料層的列印日誌級別)
* @return true
*/
const store = Store.store;
/**
* storeActionType修飾器,按名稱錄入actionType
* @params actionType(資料改變響應type), level(日誌級別)
* @return target
*/
const storeActionType = Store.storeActionType;
/**
* storeDestroy修飾器,按名稱錄入是否需要銷燬
* @return target
*/
const storeDestroy = Store.storeDestroy;
/**
* connectStore修飾器,連線資料,事件和reactDom
* @params storeList[](頁面所需資料層名稱), destroyStoreList[](離開頁面銷燬資料層名稱)
* @return reactDom
* 由於我會繼承你的ReactDom並重寫componentWillUnmount生命週期
* 所以
* 在你的ReactDom想實現componentWillUnmount生命週期必須加上靜態屬性
* 並且上下文還是ReactDom
* 如下
* static componentWillUnmount (){
this._cons();
}
_cons(){
console.log("生命週期銷燬");
}
*/
import connectStore from './connect/connectstore';
/**
* 事件注入層
*/
import Action from './action/action';
/**
* action修飾器,處理整個action層存入事件工廠
* @param actionName(事件層名稱)
* @return target
*/
const action = Action.action;
/**
* actionProps修飾器,按名稱錄入action
* @params actionFunName(事件名稱), level(日誌級別)
* @return target
*/
const actionProps = Action.actionProps;
/**
* actionInjection修飾器,按名稱反向注入事件到reactDom
* @param actionName(事件名稱)
* @return target
*/
const actionInjection = Action.actionInjection;
export { Store, store, storeActionType, storeDestroy, connectStore, action, actionProps, actionInjection };
複製程式碼
使用注意點
- 由於我會繼承你的ReactDom並重寫componentWillUnmout生命週期所以在你的ReactDom想實現componentWillUnmount生命週期必須加上靜態屬性並且上下文還是ReactDom如下:
static componentWillUnmount (){
this._cons();
}
_cons(){
console.log("生命週期銷燬");
}
複製程式碼
- 為了方便使用,Store中提供getAllInitData方法,獲取storeName下所有初始資料,減少想手動初始化資料時的重複性定義。
- (dispatch, _this),action中第二個的系統級入參,提供_this,方便action內部函式互相呼叫。
- app.js路由檔案中,如果想使用如下方式:
(r => {
r.keys().forEach(r);
})(require.context('./', true, /reducer\.js/));
(r => {
r.keys().forEach(r);
})(require.context('./', true, /action\.js/));
複製程式碼
來省略手動引入reducer和action的話,所有頁面元件必須按如下非同步方式引入
const XXX = asyncComponent(() => import(/* webpackChunkName: 'XXX' */ './XXX'));
複製程式碼
如不想非同步引入頁面元件,則必須在import { Store } from 'reduxm';和import XXX from '.XXX';之前進行如下引入:
import './XXX/reducer';
import './XXX/action';
複製程式碼
示例
app.js:
import React from 'react';
import ReactDOM from 'react-dom';
import { HashRouter, Route } from 'react-router-dom';
import { asyncComponent } from 'AsyncComponent';
import HeaderMenu from './components/heard';
import SiderMenu from './components/sider';
import 'date-util';
import './css/main.css';
(r => {
r.keys().forEach(r);
})(require.context('./', true, /reducer\.js/));
(r => {
r.keys().forEach(r);
})(require.context('./', true, /action\.js/));
import { Store } from 'reduxm';
const demo1 = asyncComponent(() => import(/* webpackChunkName: 'demo1' */ './demo1'));
const demo2 = asyncComponent(() => import(/* webpackChunkName: 'demo2' */ './demo2'));
let debug = true;
const router = Store.createStore(
<HashRouter>
<div>
<HeaderMenu />
<div className="ant-layout ant-layout-has-sider layout">
<SiderMenu />
<Route exact path="/" component={demo1} />
<Route exact path="/demo/demo1" component={demo1} />
<Route exact path="/demo/demo2" component={demo2} />
{debug ? Store.getDevTools() : null}
</div>
</div>
</HashRouter>,
debug
);
ReactDOM.render(router, document.getElementById('content'));
複製程式碼
index.js:
import React from 'react';
import { Layout, Breadcrumb, Button } from 'antd';
import { connectStore, actionInjection } from 'reduxm';
import ModalTip from 'modalTip';
const { Content } = Layout;
/**
* demo1
*/
@connectStore(['demo1Store'], ['demo1Store'])
@actionInjection('demo1Action')
export default class demo1 extends React.Component {
componentDidMount() {
this.props.changeNeedCode('zhanghao');
this.props.changeImmutableList(this.props.demo1Store);
}
render() {
let that = this;
console.log(that.props.demo1Store.immutableList);
console.log(that.props.demo1Store.immutableList.toJS());
console.log(that.props.demo1Store.immutableInList.immutableList[0]);
console.log(that.props.demo1Store.immutableInList.immutableList[0].toJS());
return (
<Layout style={{ padding: '0 24px 24px' }}>
<Breadcrumb style={{ margin: '12px 0' }}>
<Breadcrumb.Item>demo</Breadcrumb.Item>
<Breadcrumb.Item>demo1</Breadcrumb.Item>
</Breadcrumb>
<Content style={{ background: '#fff', padding: 24, margin: 0, minHeight: 280 }}>
<div>
<div>{new Date().format('yyyy-MM-dd hh:mm:ss')}</div>
<div>{that.props.demo1Store.welcomeText}</div>
<div>是否需要驗證碼{that.props.demo1Store.needCode}</div>
<div>
<Button onClick={() => that._showModalTip('info')}>Info</Button>
<Button onClick={() => that._showModalTip('success')}>Success</Button>
<Button onClick={() => that._showModalTip('error')}>Error</Button>
<Button onClick={() => that._showModalTip('warning')}>Warning</Button>
</div>
</div>
</Content>
</Layout>
);
}
_showModalTip(type) {
switch (type) {
case 'info':
ModalTip.infoTip('info');
break;
case 'success':
ModalTip.successTip('success');
break;
case 'error':
ModalTip.errorTip('error');
break;
case 'warning':
ModalTip.warningTip('warning');
break;
}
}
}
複製程式碼
action.js:
import fetch from 'fetch/fetch';
import ModalTip from 'modalTip';
import { Store, action, actionProps } from 'reduxm';
import immutable from 'immutable';
const demo1Type = Store.getActionType('demo1Store');
const demo1AllInitStore = Store.getAllInitData('demo1Store');
let checkNeedCode = nickName => {
let params = {};
params.nickName = nickName;
params.t = new Date().getTime();
return fetch
.post('/oauth/checkLogin', params)
.then(res => {
return res.needCode;
})
.catch(e => {
ModalTip.warningTip(e.message);
});
};
@action('demo1Action')
class demo1Action {
@actionProps('changeNeedCode')
static changeNeedCode = nickName => async (dispatch, _this) => {
let needCode = await checkNeedCode(nickName);
dispatch({ type: demo1Type.change_needCode, needCode: needCode });
};
@actionProps('changeImmutableList', 'error')
static changeImmutableList = demo1Store => async (dispatch, _this) => {
let immutableList = demo1Store.immutableList;
let immutableInList = demo1Store.immutableInList;
immutableList = immutable.set(immutableList, 0, 4);
immutableInList.immutableList[0] = immutable.set(immutableInList.immutableList[0], 0, 10);
dispatch({
type: demo1Type.change_demo1Store,
demo1Store: {
immutableList: immutableList,
immutableInList: immutableInList
}
});
demo1AllInitStore.welcomeText = 'www---www';
console.log(demo1AllInitStore);
};
}
複製程式碼
reducer.js:
import { store, storeActionType, storeDestroy } from 'reduxm';
import immutable from 'immutable';
@store('demo1Store', 'change_demo1Store')
class demo1 {
@storeActionType('change_welcomeText')
@storeDestroy
static welcomeText = 'Welcome to Redux test!';
@storeActionType('change_needCode')
@storeDestroy
static needCode = 1;
@storeActionType('change_immutableList', 'waring')
@storeDestroy
static immutableList = immutable.fromJS([1, 2, 3]);
@storeActionType('change_immutableInList')
@storeDestroy
static immutableInList = {
immutableList: [immutable.fromJS([7, 8, 9])]
};
}
複製程式碼