把redux封裝起來,更加簡單方便的使用

昨夜星辰不想說話發表於2018-02-07

Redux

Redux 是 JavaScript 狀態容器,提供可預測化的狀態管理

內容目錄

  1. 動機
  2. 核心概念
  3. 三大原則
  4. 先前技術
  5. 基礎
  6. 示例

Redux?

  • JavaScript單頁應用開發日趨複雜 --> state more --> state在什麼時候,由於什麼原因,如何變化已然不受控制
  • 分開變化和非同步 --> React試圖在檢視層禁止非同步和直接操作DOM來解決這個問題 --> React依舊把處理state中資料的問題留給了我們
  • Redux由Flux演變而來,但受Elm的啟發,避開了Flux的複雜性
  • 你可能並不需要Redux

核心概念

image

三大原則

  • 單一資料來源:整個應用的 state 被儲存在一棵 object tree 中,並且這個 object tree 只存在於唯一一個 store 中。
  • State 是隻讀的:唯一改變 state 的方法就是觸發 action,action 是一個用於描述已發生事件的普通物件。
  • 使用純函式來執行修改:為了描述 action 如何改變 state tree ,你需要編寫 reducers。

先前技術

  • Redux 可以被看作 Flux 的一種實現嗎? 是,也可以說 不是。
  1. 和 Flux 一樣,Redux 規定,將模型的更新邏輯全部集中於一個特定的層(Flux 裡的 store,Redux 裡的 reducer)
  2. Flux 和 Redux 都不允許程式直接修改資料,而是用一個叫作 “action” 的普通物件來對更改進行描述。
  3. 而不同於 Flux ,Redux 並沒有 dispatcher 的概念(原因是它依賴純函式來替代事件處理器)。
  4. 和 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,進行二次封裝

內容目錄

  1. redux存在的問題
  2. 設計思想
  3. api
  4. 使用注意點
  5. 示例

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 };
複製程式碼

使用注意點

  1. 由於我會繼承你的ReactDom並重寫componentWillUnmout生命週期所以在你的ReactDom想實現componentWillUnmount生命週期必須加上靜態屬性並且上下文還是ReactDom如下:
static componentWillUnmount (){
         	this._cons();
       	}
    
_cons(){
            console.log("生命週期銷燬");
        }
複製程式碼
  1. 為了方便使用,Store中提供getAllInitData方法,獲取storeName下所有初始資料,減少想手動初始化資料時的重複性定義。
  2. (dispatch, _this),action中第二個的系統級入參,提供_this,方便action內部函式互相呼叫。
  3. 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])]
	};
}
複製程式碼

相關文章