Redux與Redux-Saga的故事

繼續上一篇文章,這篇文章主要是介紹一下ReduxRedux-Saga的基本使用以及我常在工做中使用的方法,文中若有錯誤,歡迎指正。本文對其中涉及到的一些概念不作具體介紹,你們能夠去Redux官網去查看。文末涉及到的項目,具體代碼請查看 Git倉庫javascript

1、Redux

1.什麼是Redux。

ReduxJavaScript 狀態容器,提供可預測化的狀態管理。css

2.安裝

npm install redux -Shtml

3.三大原則

  • 單一數據源
  • state是隻讀的
  • 使用純函數進行修改

4.使用方法

4.1 Action

一般state變化會致使View層出現變化,可是用戶實際上是接觸不到state的,只能經過View層進行修改。Action就是View層發出的一個通知,通知state要發生變化。java

Action是一個對象,type屬性是必須的,表示此次通知的名稱,其餘屬性能夠自由設置,我通常都是用payload屬性,你們能夠自由發揮。react

4.2 Reducer

Store 收到 Action 之後,必須給出一個新的 State,這樣 View 纔會發生變化。這種 State 的計算過程就叫作 Reducergit

Reducer 是一個函數,它接受 Action 和當前 State 做爲參數,返回一個新的 State。github

Reducer 函數最重要的特徵是,它是一個純函數。也就是說,只要是一樣的輸入,一定獲得一樣的輸出。npm

4.3 dispatch

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'}
});

複製代碼

4.4 createStore

爲了不每次手動調用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

4.5 combineReducers

用於 Reducer 的拆分。你只要定義各個子 Reducer 函數,而後用這個方法,將它們合成一個大的 Reducer

import { combineReducers } from 'redux';

const Reducer = combineReducers({
  reducer1,
  reducer2,
  reducer3
})

export default Reducer;

複製代碼

4.6 建立

建立的方法其實很簡單,我目前使用 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)

同來建立actionactionMap是一個對象,操做類型做爲鍵,其值必須是一個函數或者一個數組或者是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 }
);

複製代碼

2、React-Redux

1.介紹

Redux 官方提供的 React 綁定庫。 具備高效且靈活的特性。

2.安裝

npm install react-redux -S

3.UI組件和容器組件

React-Redux 將全部組件分紅兩大類:UI 組件和容器組件。

UI組件

  • 只負責 UI 的呈現,不帶有任何業務邏輯。
  • 沒有狀態(不使用this.state這個變量)。
  • 全部數據都由參數(this.props)提供。
  • 不使用任何 ReduxAPI。

容器組件

  • 負責管理數據和業務邏輯,不負責 UI 的呈現。
  • 帶有內部狀態。
  • 使用 Redux 的 API。

3.1 connect

鏈接 React 組件與 Redux store

鏈接操做不會改變原來的組件類。

反而返回一個新的已與 Redux store 鏈接的組件類。

connect方法接受兩個參數:mapStateToPropsmapDispatchToProps。它們定義了 UI 組件的業務邏輯。前者負責輸入邏輯,即將state映射到 UI 組件的參數(props),後者負責輸出邏輯,即將用戶對 UI 組件的操做映射成 Action

import { connect } from 'react-redux';

const Component = connect(
  mapStateToProps,
  mapDispatchToProps
)(Con);

複製代碼

3.1.1 mapStateToProps

組件將會監聽 Redux store 的變化。任什麼時候候,只要 Redux store 發生改變,mapStateToProps 函數就會被調用。該回調函數必須返回一個純對象,這個對象會與組件的 props 合併。

// mapStateToProps是一個函數,它接受state做爲參數,返回一個對象。
// mapStateToProps會訂閱 Store,每當state更新的時候,就會自動執行,從新計算 UI 組件的參數,從而觸發 UI 組件的從新渲染。
const mapStateToProps = state => {
  const { test } = state;
  return {
      test
  };
};

複製代碼

3.1.2 mapDispatchToProps

用來創建 UI 組件的參數到store.dispatch方法的映射。它定義了哪些用戶的操做應該看成 Action,傳給 Store。它能夠是一個函數,也能夠是一個對象。

const mapDispatchToProps = dispatch => {
  return {
    onClick: () => {
      dispatch({
        type: 'TEST',
        payload: 'payload'
      });
    }
  };
};

複製代碼

3.2 Provider

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')
)

複製代碼

3、Redux-Saga

1.介紹

redux-saga 是 redux 一箇中間件,用於解決異步問題。

2.安裝

npm install --save redux-saga

3. 使用

  • 使用createSagaMiddleware建立一個 Saga middleware 和要運行的 Sagas
  • 使用applyMiddlewareSaga 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 後的 Promisemiddleware 會暫停 Saga,直到 Promise 完成。delay表示1s後執行resolve(), put實際上是redux-saga/effect 提供的一個方法,用來發送一個typetype屬性的Action,去觸發state的更新。

咱們建立的watchSaga,用takeEvery進行監聽,每當監聽到一個名爲INCREMENT_ASYNCAction,執行所對應的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')
);

複製代碼

4.經常使用輔助函數

4.1 takeEvery

在發起(dispatch)到 Store 而且匹配 pattern 的每個 action 上派生一個 saga

4.2 takeLatest

每當一個 action 被髮起到 Store,而且匹配 pattern 時,則 takeLatest 將會在後臺啓動一個新的 saga 任務。 若是此前已經有一個 saga 任務啓動了(在當前 action 以前發起的最後一個 action),而且仍在執行中,那麼這個任務將被取消。

4.3 put

建立一個 Effect 描述信息,用來命令 middlewareStore 發起一個 action。 這個 effect 是非阻塞型的,而且全部向下遊拋出的錯誤(例如在 reducer 中),都不會冒泡回到 saga 當中。

4.4 call

建立一個 Effect 描述信息,用來命令 middleware 以參數 args 調用函數 fn 。(通常用來調用接口)

4.5 fork

建立一個 Effect 描述信息,用來命令 middleware 以 非阻塞調用 的形式執行 fn

實現按順序調用執行,避免阻塞。

4.6 cancel

建立一個 Effect 描述信息,用來命令 middleware 取消以前的一個分叉任務。

4、項目準備

接下來咱們會根據以上內容,編寫一個關於記帳的小Demo,方便更好地理解。

目錄結構

4.1 安裝

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

複製代碼

4.2 建立action

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;

複製代碼

4.3 建立reducer

//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;

複製代碼

4.4 建立saga

// saga/index.js
import {
  all
} from 'redux-saga/effects';
import account from './account';

export default function* rootSaga() {
  yield all([account()]);
}

// saga/account.js
...
下面會有介紹

複製代碼

4.5 鏈接store

// 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();


複製代碼

5、編寫界面

// 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;

複製代碼

效果以下

6、功能完善

6.1 查詢

// 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也是提供了useSelectoruseDispatch來幫助咱們更好地實現,替代了傳統的方案,在這裏就不一一介紹了,感興趣的小夥伴能夠去官網進行查看,而後咱們修改一下代碼。

...
const data = useSelector(state => state.account);
const dispatch = useDispatch();

useEffect(() => {
    dispatch(actions.getAccount());
}, []);

...

複製代碼

是否是感受輕便了許多呢。

6.2 新增

//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();
}

複製代碼

6.3修改

//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);

複製代碼

6.4 刪除

//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}));
}

複製代碼

7、總結

以上主要介紹了一下Redux以及React-ReduxRedux-Saga的使用方法,最後完成了一個簡單地關於記帳的小Demo,實現了基本的增刪改查,固然,還有一些細節還未完善,以後有時間的話會抽空完善一下。

當你還在猶豫項目中是否須要使用Redux的時候,其實就不必使用了,由於這部分依賴原本體積就很大,對於一些小型項目仍是不建議使用了,不過感興趣的話仍是能夠嘗試一下,畢竟技多不壓身嘛。

最後,文中若是錯誤或者不正當的地方,歡迎指正,若是對你有什麼幫助的話,歡迎Star。

8、參考

相關文章
相關標籤/搜索