繼續上一篇文章,這篇文章主要是介紹一下Redux
和Redux-Saga
的基本使用以及我常在工做中使用的方法,文中若有錯誤,歡迎指正。本文對其中涉及到的一些概念不作具體介紹,你們能夠去Redux官網去查看。文末涉及到的項目,具體代碼請查看 Git倉庫。javascript
Redux
是 JavaScript
狀態容器,提供可預測化的狀態管理。css
npm install redux -S
html
state
是隻讀的一般state
變化會致使View
層出現變化,可是用戶實際上是接觸不到state
的,只能經過View
層進行修改。Action
就是View
層發出的一個通知,通知state
要發生變化。java
Action
是一個對象,type
屬性是必須的,表示此次通知的名稱,其餘屬性能夠自由設置,我通常都是用payload
屬性,你們能夠自由發揮。react
Store
收到 Action
之後,必須給出一個新的 State
,這樣 View
纔會發生變化。這種 State
的計算過程就叫作 Reducer
。git
Reducer
是一個函數,它接受 Action
和當前 State
做爲參數,返回一個新的 State。
github
Reducer
函數最重要的特徵是,它是一個純函數。也就是說,只要是一樣的輸入,一定獲得一樣的輸出。npm
store.dispatch()是 View
發出 Action
的惟一方法。redux
接收一個Action
爲參數,併發送。數組
// 發送
store.dispatch({
type: 'TEST',
payload: 'payload'
});
// 手動觸發
const data = {
name: 'lj'
}
const reducer = (state = data, action) => {
switch (action.type) {
case 'CHANGE_NAME':
return {...state, action.payload};
default:
return state;
}
};
const state = reducer(data, {
type: 'CHANGE_NAME',
payload: {name: 'hd'}
});
複製代碼
爲了不每次手動調用Reducer
函數,store.dispatch
方法會觸發 Reducer
的自動執行。爲此,Store
須要知道 Reducer
函數,作法就是在生成 Store
的時候,將 Reducer
傳入createStore
方法。
import { createStore } from 'redux';
const store = createStore(reducer);
複製代碼
createStore
接受 Reducer
做爲參數,生成一個新的 Store
。之後每當store.dispatch
發送過來一個新的 Action
,就會自動調用 Reducer
,獲得新的 State
。
用於 Reducer
的拆分。你只要定義各個子 Reducer
函數,而後用這個方法,將它們合成一個大的 Reducer
。
import { combineReducers } from 'redux';
const Reducer = combineReducers({
reducer1,
reducer2,
reducer3
})
export default Reducer;
複製代碼
建立的方法其實很簡單,我目前使用 redux-actions
,實際上是一箇中間件,能夠用來簡化生成Action
以及Reducer
。
安裝
npm install --save redux-actions
使用
./actions/index.js
import { createActions } from 'redux-actions';
const actions = createActions({
SET_ACCOUNT: data => data
});
./reducer/index.js
import { handleActions } from 'redux-actions';
import actions from '../actions/index';
import Immutable from "seamless-immutable";
const defaultState = Immutable({
accounts: []
});
const reducer = handleActions(
new Map([
[
actions.setAccount,
(state, {
payload
}) =>
state.set("accounts", payload)
]
]),
defaultState
);
export default reducer;
複製代碼
解釋一下上面的部分代碼
createActions(actionMap)
同來建立action
,actionMap
是一個對象,操做類型做爲鍵,其值必須是一個函數或者一個數組或者是actionMap
。
createActions({
ADD_TODO: todo => ({ todo }), // payload creator
REMOVE_TODO: [
todo => ({ todo }), // payload creator
(todo, warn) => ({ todo, warn }) // meta
]
});
複製代碼
handleActions(reducerMap, defaultState[, options])
用來建立多個reduce
,接收兩個參數,第一個參數是一個映射,第二個參數是初始的state
。
//兩種寫法
1.
const reducer = handleActions(
{
INCREMENT: (state, action) => ({
counter: state.counter + action.payload
}),
DECREMENT: (state, action) => ({
counter: state.counter - action.payload
})
},
{ counter: 0 }
);
2.
const reducer = handleActions(
new Map([
[
INCREMENT,
(state, action) => ({
counter: state.counter + action.payload
})
],
[
DECREMENT,
(state, action) => ({
counter: state.counter - action.payload
})
]
]),
{ counter: 0 }
);
複製代碼
Redux
官方提供的 React
綁定庫。 具備高效且靈活的特性。
npm install react-redux -S
React-Redux
將全部組件分紅兩大類:UI 組件和容器組件。
UI組件
UI
的呈現,不帶有任何業務邏輯。Redux
的 API。
容器組件
UI
的呈現。Redux
的 API。鏈接 React
組件與 Redux store
。
鏈接操做不會改變原來的組件類。
反而返回一個新的已與 Redux store
鏈接的組件類。
connect
方法接受兩個參數:mapStateToProps
和mapDispatchToProps
。它們定義了 UI
組件的業務邏輯。前者負責輸入邏輯,即將state映射到 UI
組件的參數(props),後者負責輸出邏輯,即將用戶對 UI
組件的操做映射成 Action
。
import { connect } from 'react-redux';
const Component = connect(
mapStateToProps,
mapDispatchToProps
)(Con);
複製代碼
組件將會監聽 Redux store
的變化。任什麼時候候,只要 Redux store
發生改變,mapStateToProps
函數就會被調用。該回調函數必須返回一個純對象,這個對象會與組件的 props
合併。
// mapStateToProps是一個函數,它接受state做爲參數,返回一個對象。
// mapStateToProps會訂閱 Store,每當state更新的時候,就會自動執行,從新計算 UI 組件的參數,從而觸發 UI 組件的從新渲染。
const mapStateToProps = state => {
const { test } = state;
return {
test
};
};
複製代碼
用來創建 UI
組件的參數到store.dispatch
方法的映射。它定義了哪些用戶的操做應該看成 Action
,傳給 Store
。它能夠是一個函數,也能夠是一個對象。
const mapDispatchToProps = dispatch => {
return {
onClick: () => {
dispatch({
type: 'TEST',
payload: 'payload'
});
}
};
};
複製代碼
connect
方法生成容器組件之後,須要讓容器組件拿到state
對象,才能生成 UI
組件的參數。 React-Redux
提供Provider
組件,可讓容器組件拿到state
。避免容器組件可能在很深的層級,傳遞state
形成的麻煩。
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import Reducer from './reducers'
import App from './App.jsx'
let store = createStore(Reducer);
render(
<Provider store={store}> // 包裹整個應用 App下的左右子組件均可以使用store裏的數據了 <App /> </Provider>,
document.getElementById('root')
)
複製代碼
redux-saga
是 redux 一箇中間件,用於解決異步問題。
npm install --save redux-saga
createSagaMiddleware
建立一個 Saga middleware
和要運行的 Sagas
。applyMiddleware
將Saga middleware
鏈接至 Redux store
。// .saga.js
export function* saga() {
console.log('hello');
}
// ./index.js
import { createStore, applyMiddleware } from 'redux';// applyMiddleware 將中間件鏈接到store
import createSagaMiddleware from 'redux-saga'; // 用來建立一個saga中間件
import { saga } from './saga';
const store = createStore(
reducer,
applyMiddleware(createSagaMiddleware(helloSaga))
);
複製代碼
上面這段代碼只是簡單模擬了一下saga
使用的基本流程,在實際項目中咱們應該這樣操做。
import { delay } from 'redux-saga';
import { put, takeEvery } from 'redux-saga/effects';
// ...
export function* incrementAsync() {
yield delay(1000); // 延遲 模擬異步
yield put({ type: 'INCREMENT' }); //
}
export function* watchSaga() {
yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}
//...
export default function* rootSaga() {
yield all([
watchSaga()
])
}
複製代碼
建議你們先去了解一下generator,否則可能會看不懂。
saga實際上是基於generator
實現的,它會 yield
對象到 redux-saga
middleware
,被 yield
的對象都是一類指令,好比 delay(1000)、put({ type: 'INCREMENT' }), 指令可被 middleware
解釋執行。當 middleware
取得一個 yield
後的 Promise
,middleware
會暫停 Saga
,直到 Promise
完成。delay
表示1s
後執行resolve()
, put
實際上是redux-saga/effect
提供的一個方法,用來發送一個type
爲type
屬性的Action
,去觸發state
的更新。
咱們建立的watchSaga
,用takeEvery
進行監聽,每當監聽到一個名爲INCREMENT_ASYNC
的Action
,執行所對應的incrementAsync
任務。
當saga
數量比較多的時候,爲了更好地分類,咱們可使用all
進行saga
的合併,更好地進行管理。
看到這裏,咱們能夠把以上代碼進行一個整理
//createStore.js
import { createStore, applyMiddleware, compose } from 'redux';
import createSagaMiddleware from 'redux-saga';
import loggerMiddleware from 'redux-logger'; // 打印
import createRootReducer from '../reducers';
export const sagaMiddleware = createSagaMiddleware();
const middlewares = [sagaMiddleware, loggerMiddleware]; // 處理多個saga
export default function configureStore() {
const store = createStore(
createRootReducer,
applyMiddleware(...middlewares)
);
return store;
}
//index.js
import configureStore, { sagaMiddleware } from './utilities/appStore';
import rootSaga from './sagas';
const store = configureStore();
sagaMiddleware.run(rootSaga);
ReactDOM.render(
<Provider store={store}> <App /> </Provider>,
document.getElementById('root')
);
複製代碼
在發起(dispatch
)到 Store
而且匹配 pattern
的每個 action
上派生一個 saga
。
每當一個 action
被髮起到 Store
,而且匹配 pattern
時,則 takeLatest
將會在後臺啓動一個新的 saga
任務。 若是此前已經有一個 saga
任務啓動了(在當前 action
以前發起的最後一個 action
),而且仍在執行中,那麼這個任務將被取消。
建立一個 Effect
描述信息,用來命令 middleware
向 Store
發起一個 action
。 這個 effect
是非阻塞型的,而且全部向下遊拋出的錯誤(例如在 reducer
中),都不會冒泡回到 saga
當中。
建立一個 Effect
描述信息,用來命令 middleware
以參數 args
調用函數 fn
。(通常用來調用接口)
建立一個 Effect
描述信息,用來命令 middleware
以 非阻塞調用 的形式執行 fn
。
實現按順序調用執行,避免阻塞。
建立一個 Effect
描述信息,用來命令 middleware
取消以前的一個分叉任務。
接下來咱們會根據以上內容,編寫一個關於記帳的小Demo
,方便更好地理解。
目錄結構
create-react-app my-project
cd my-project
npm install redux react-redux redux-actions seamless-immutable redux-saga redux-logger antd lodash moment -S
複製代碼
import { createActions } from 'redux-actions';
const actions = createActions({
SET_LOADING: loading => loading,
SET_ACCOUNT: data => data,
SET_ALL: num => num,
// 接口
GET_ACCOUNT:get=> get, // 獲取帳目
ADD_ACCOUNT: add => add, // 新增帳目
UPDATE_ACCOUNT: update => update, // 修改帳目
DELATE_ACCOUNT: del => del // 刪除帳目
});
export default actions;
複製代碼
//reducer/account.js
import {
handleActions
} from 'redux-actions';
import actions from '../actions/index';
import Immutable from "seamless-immutable";
//默認數據
const defaultState = Immutable({
loading: false,
accounts: [],
all: 0
});
const reducer = handleActions(
new Map([
[
actions.setLoading,
(state, {
payload
}) =>
state.set("loading", payload)
],
[
actions.setAccount,
(state, {
payload
}) =>
state.set("accounts", payload)
],
[
actions.setAll,
(state, {
payload
}) =>
state.set("all", payload)
]
]),
defaultState
);
export default reducer;
//reducer/index.js
import {
combineReducers
} from 'redux';
import account from './account';
const reducer = combineReducers({
account
});
export default reducer;
複製代碼
// saga/index.js
import {
all
} from 'redux-saga/effects';
import account from './account';
export default function* rootSaga() {
yield all([account()]);
}
// saga/account.js
...
下面會有介紹
複製代碼
// createStore.js
import { createStore, applyMiddleware, compose } from 'redux';
import createSagaMiddleware from 'redux-saga';
import logger from 'redux-logger';//打印action的中間件
import reducer from './store/reducer';
export const sagaMiddleware = createSagaMiddleware();// 建立saga中間件
const middlewares = [sagaMiddleware, logger];
export default function configureStore() {
const store = createStore( // 建立store,避免手動調用
reducer,
compose(applyMiddleware(...middlewares)) //saga鏈接store
);
return store;
}
// index.js
...
import * as serviceWorker from './serviceWorker'
import { Provider } from 'react-redux';
import configureStore, { sagaMiddleware } from './createStore';
import rootSaga from './store/saga';
import zhCN from 'antd/lib/locale-provider/zh_CN';
import { ConfigProvider } from 'antd';
import 'antd/dist/antd.css';
const store = configureStore();
sagaMiddleware.run(rootSaga); // 運行saga
ReactDOM.render(
<Provider store={store}> <ConfigProvider locale={zhCN}> <App /> </ConfigProvider> </Provider>,
document.getElementById('root'));
serviceWorker.unregister();
複製代碼
// App.js
import React, { useState, useEffect } from 'react';
import { Button, Spin } from 'antd';
import List from './component/List';
import Modal from './component/Modal';
import './App.css';
function App(props) {
const [visible, setVisible] = useState(false);
function handleAccount() {
setVisible(true);
}
function handleOk() {
setVisible(false);
}
function handleCancel() {
setVisible(false);
}
return (
<div className="App">
<h1>Dong日帳目</h1>
<Spin spinning={loading}>
<List data={accounts} all={all} />
<Button type="primary" style={{ marginTop: 20 }} onClick={handleAccount}>開始記帳</Button>
</Spin>
<Modal isOpen={visible} onClose={handleCancel} type={0} data={null}/>
</div>
);
}
export default App;
/**
component/Modal.js
isOpen: 是否顯示
data: 數據
onClose: 關閉
type: 類型 0新增1修改
*/
import React from 'react';
import { Modal, Form, Input, InputNumber, Select, DatePicker } from 'antd';
import moment from 'moment';
const { Option } = Select;
function AccountModal(props) {
const { isOpen, onClose, data } = props;
function handleOk() {
onClose();
}
function handleCancel() {
onClose();
}
return (
<div className="Modal">
<Modal title="開始記帳" visible={isOpen} onOk={handleOk} onCancel={handleCancel} width={600} destroyOnClose={true} >
<Form>
<Form.Item label="名稱">
<Input placeholder="請輸入名稱" defaultValue={data && data.name} />
</Form.Item>
<Form.Item label="類型">
<Select placeholder="請輸入..." style={{ width: '100%' }} defaultValue={data && data.type} >
<Option value={0}>支出</Option>
<Option value={1}>收入</Option>
</Select>
</Form.Item>
<Form.Item label="支出/收入">
<InputNumber style={{ width: '100%' }} placeholder="請輸入..." defaultValue={data && data.money} />
</Form.Item>
<Form.Item label="日期">
<DatePicker style={{ width: '100%' }} defaultValue={data && moment(data.date)} />
</Form.Item>
</Form>
</Modal>
</div>
);
}
export default AccountModal;
/**
component/List.js
all: 總帳目
data: 數據
*/
import React, { useState } from 'react';
import { Button } from 'antd';
import Modal from '../Modal';
import './index.css';
function List(props) {
const [visible, setVisible] = useState(false);
const [select, setSelect] = useState({});
const { data, all } = props;
// 點擊修改
function handleAccount(data) {
setSelect(data);
setVisible(true);
}
//點擊取消
function handleCancel() {
setVisible(false);
}
return (
<ul className="list">
{
data && data.length > 0 ? data.map(item => {
return (
<li key={item.id}>
<span className="type">{item.name}</span>
<span className="money" style={{ color: item.type ? 'green' : 'red' }}>{item.money}</span>
<span className="date">{item.date.toLocaleString()}</span>
<div>
<Button type="" style={{ marginRight: 10 }} onClick={() => handleAccount(item)}>修改</Button>
<Button type="danger">刪除</Button>
</div>
</li>
)
}) : '暫無數據'
}
<Modal isOpen={visible} onClose={handleCancel} data={select} type={1} />
<span className="all">總計: {all}</span>
</ul>
)
}
export default List;
複製代碼
效果以下
// saga.js
function* fetchGetAcount() {
try {
yield put(actions.setLoading(true));// 加載
yield delay(1000); // 模擬接口
yield put(actions.setLoading(false));// 取消加載
yield put(actions.setAccount([ // 添加數據
{
name: '充值',
type: 0,
money: -100,
date: new Date(),
id: 1
},
{
name: '兼職',
type: 1,
money: 200,
date: new Date(),
id: 2
}
]));
yield put(actions.setAll(100)); // 設置總數
} catch (error) {
return error;
}
}
export default function* accountSaga() {
// actions.getAccount().type 至關於 ‘GET_ACCOUNT’
yield takeLatest(actions.getAccount().type, fetchGetAcount);
}
// App.js
//獲取store數據
const mapStateToProps = state => {
const { loading, accounts, all } = state.account;
return {
loading,
accounts,
all
}
}
//添加dispatch映射
const mapDispatchToProps = dispatch => {
return {
getAccount: () => {
dispatch(actions.getAccount());
}
}
}
connect(mapStateToProps, mapDispatchToProps)(App);
複製代碼
其實上面這種connect
寫法仍是適合Class
的寫法,React-Redux
也是提供了useSelector
和useDispatch
來幫助咱們更好地實現,替代了傳統的方案,在這裏就不一一介紹了,感興趣的小夥伴能夠去官網進行查看,而後咱們修改一下代碼。
...
const data = useSelector(state => state.account);
const dispatch = useDispatch();
useEffect(() => {
dispatch(actions.getAccount());
}, []);
...
複製代碼
是否是感受輕便了許多呢。
//saga.js
// 新增帳目
function* fetchAddAcount(action) {
try {
const allData = store.getState().account; // 獲取全部帳目
// 添加新帳目並按時間排序
const newData = sortBy(allData.accounts.concat(action.payload), function(item){
return item.date;
});
const money = action.payload.type ? action.payload.money : -action.payload.money;
yield put(actions.setAccount(newData));
yield put(actions.setAll(allData.all + money));
} catch (error) {
return error;
}
}
//Modal.js
const [name, setName] = useState('');
const [money, setMoney] = useState(0);
const [accountType, setType] = useState(-1);
const [date, setDate] = useState(null);
//點擊肯定
function handleOk() {
const { addAccount } = props;
//上傳的帳目格式
const data = {
name,
money,
type: accountType,
date: new Date(date),
id: Math.floor(Math.random() * 10000)
}
addAccount(data);
onClose();
}
複製代碼
//saga.js
// 修改帳目
function* fetchUpdateAcount(action) {
try {
//獲取store數據
const allData = store.getState().account;
//獲取修改的帳單index
const selectNum = allData.accounts.findIndex(item => {
return item.id === action.payload.id;
});
//將僞數組修改成數組
const [...accounts] = allData.accounts;
//刪除本來修改帳目
accounts.splice(selectNum, 1);
//刪除爲了更快的獲取以前帳目金額 而額外添加的屬性
delete action.payload.initial;
//添加修改後的帳目並排序
const newData = sortBy(accounts.concat(action.payload), function(item){
return item.date;
});
yield put(actions.setAccount(newData));
//修改總帳目
yield put(actions.setAll(allData.all - action.payload.initial + (action.payload.type ? action.payload.money : -action.payload.money)));
} catch (error) {
return error;
}
}
//Modal.js
const newData = {
...data,
name,
money,
type: accountType,
date: new Date(date),
//額外的屬性,保存初始金額
initial: data.money
}
//修改帳目的Action
updateAccount(newData);
複製代碼
//saga.js
// 刪除帳目
function* fetchDeleteAcount(action) {
try {
const allData = store.getState().account;
const [...accounts] = allData.accounts;
accounts.splice(action.payload.index, 1);
yield put(actions.setAccount(accounts));
yield put(actions.setAll(allData.all - action.payload.money));
} catch (error) {
return error;
}
}
//List.js
//刪除
function handleDel(item, index) {
//index:當前帳目位置 money: 當前帳目金額
dispatch(actions.deleteAccount({money: item.type ? item.money : -item.money, index}));
}
複製代碼
以上主要介紹了一下Redux
以及React-Redux
和Redux-Saga
的使用方法,最後完成了一個簡單地關於記帳的小Demo,實現了基本的增刪改查,固然,還有一些細節還未完善,以後有時間的話會抽空完善一下。
當你還在猶豫項目中是否須要使用Redux
的時候,其實就不必使用了,由於這部分依賴原本體積就很大,對於一些小型項目仍是不建議使用了,不過感興趣的話仍是能夠嘗試一下,畢竟技多不壓身嘛。
最後,文中若是錯誤或者不正當的地方,歡迎指正,若是對你有什麼幫助的話,歡迎Star。