資料流架構學習筆記(二)-Redux

樂帥發表於2017-10-14

初期參加工作開發專案時,使用React Native + Flux進行手機應用跨平臺開發,在上一篇博文中資料流架構學習筆記(一)-Flux 對Flux做了一次總結,本文是我對資料流管理架構學習總結的第二篇資料流架構學習筆記(二)-Redux,是我在工作過程中對專案使用Redux進行重構狀態管理和資料流的學習記錄。

Redux的由來

2014年 Facebook 提出了 Flux 架構的概念和單向資料流管理的思想,並給出了管理狀態的基本資料流,但是隨著前端應用的複雜性指數級的提升,前端頁面需要管理的狀態也越來越多,於是出現了很多基於Flux基本的資料流概念和單向資料流思想的實現方式。2015年,Redux 出現,將 Flux 與函數語言程式設計結合一起,很短時間內就成為了最熱門的前端架構。

在實際專案中,你應該有遇到過以下這樣情況的發生:

  • 在debug專案進行問題查詢時,複雜的資料重新整理頁面時,由於一些不規範的監聽和觀察者機制使用或過多使用React中this.setState進行渲染頁面,由於是非同步進行資料渲染頁面,經常無法判斷本次造成是因為什麼資料狀態改變而造成頁面渲染,而且更可惡的是此時你無法知道當前狀態下,你的App實際的資料狀態是如何的,就是說,你無法知道你App當前的所有資料是多少,而你同樣也無法快速預測接下來你的App會如何變化。使用Redux就可以很好的解決這個問題。
  • 同樣的如果你還在專案中進行模組劃分,元件化開發,使用Redux可以快速將你的模組元件進行併入專案和拆分重組。

Redux工作原理

Redux 把自己標榜為一個“可預測的狀態容器 ”,它充分利用函式式的特性,讓整個實現更加優雅純粹,使用起來也更簡單。

Redux(oldState) => newState複製程式碼

Redux 可以看作是 Flux 的一次進化。Redux遵循以下三個基本原則:

  • 整個應用只有唯一一個可信資料來源,也就是隻有一個 Store
  • State 只能通過觸發 Action 來更改
  • State 的更改必須寫成純函式,也就是每次更改總是返回一個新的 State,在 Redux 裡這種函式稱為 Reducer
View 觸發資料更新 —> Actions 將資料傳遞到 Store —> Store 更新 state —> 更新 View。複製程式碼

Redux 中整個應用的狀態儲存在一顆 object tree 中,對應一個唯一的 Store,並且 state 是隻讀的,使用純函式 reducer 來更新 state 會生成一個新的 state 而不是直接修改原來的。

Redux 通過以上約束讓 state 的變化可預測。

如果無法理解這些概念,建議先學習Redux官方文件,再來檢視他人的部落格和使用方式,才能更快的使用。

Redux例項封裝

這裡以React Native實際專案登入部分展示如何將Redux應用到React Native開發中進行資料管理,在實際架構專案時,每個人有各自的編碼習慣,因而,雖然同樣是Redux,但是在各部分程式碼寫法總是有所不一樣,而實際專案中用起來的寫法也是不一樣的,但是思想總體上是一樣的,不要拘泥於程式碼的寫法,程式碼只是作為參考和總結,應該理解寫法的目的思想和如何體現Redux的融入和使用。

檢視層View

登入頁:

import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import LoginAction from '../../actions/loginAction';
...
class Login extends Component {
    ...
    _doLogin = () => {
        const param = {
          uid: this.state.uid,
          pwd: this.state.pwd
        };

        this.props.actions.doLogin(param)
          .then(() => {
            const { navigation, login } = this.props;
            if (login.status === 'done' && navigation) {
              navigation.resetRouteTo('TabBar', { title: '首頁', selectedTab: 'home' });
            } else {
              Alert.alert(
                '提示',
                login.message
              );
            }
          });
      };
    ...
}
...
const mapStateToProps = (state) => {
  return {
    login: state.loginReducer
  };
};
const mapDispatchToProps = dispatch => {
  return ({
    actions: bindActionCreators({ ...LoginAction }, dispatch)
  });
};
export default connect(mapStateToProps, mapDispatchToProps)(Login);複製程式碼

簡單說View層主要作用就是響應使用者的操作,而實際在程式碼中我們主要的作用就是觸發Action,如程式碼中呼叫this.props.actions.doLogin()函式,在this.props會存在actions屬性是由於在最後使用bindActionCreators方法將對應的LoginAction繫結至頁面元件Login中,這樣造成我在View層只會做呼叫action的操作,不會直接使用dispatch進行訊息分發。這樣就完成了View -> Actions的過程。

行為Action


const _loginSuccess = (data) => {//eslint-disable-line
  return {
    type: ActionTypes.LOGIN_SUCCESS,
    payload: {
      user: data.uid
    }
  };
};

const _loginFailed = (error) => {
  return {
    type: ActionTypes.FAIL,
    payload: {
      message: error.message
    }
  };
};

const _doLogin = (url, param) => dispatch => {
  dispatch(CommonAction.showLoading());
  return Fetcher.postQsBodyFetch(url, param)
      .then((response) => {
        dispatch(CommonAction.dismissLoading());
        dispatch(_loginSuccess(param, response));
      }).catch((error) => {
        dispatch(CommonAction.dismissLoading());
        dispatch(_loginFailed(error));
      });
};

const LoginAction = {
  doLogin: (param) => _doLogin(NetLink.login, param),
  loginSuccess: (data) => _loginSuccess(data),
  loginFailed: (error) => _loginFailed(error),
};複製程式碼

Action通常都是在進行網路層呼叫、請求資料和分發資料,因在View層使用了bindActionCreators方法和元件繫結後,將會直接獲取View層元件dispatch屬性方法,使得在Action的純函式中在資料返回後呼叫dispatch()進行資料分發。這樣就完成了Actions -> Reducer的過程。

Reducer

import ActionType from '../constants/actionType';

const initialState = {
  status: 'init',
  user: '',
  message: null
};

const loginReducer = (state = initialState, action) => {
  switch (action.type) {
    case ActionType.LOGIN_SUCCESS:
      return Object.assign({}, state, {
        status: 'done',
        user: action.payload.user,
      });
    case ActionType.FAIL:
      return Object.assign({}, state, {
        status: 'fail',
        message: action.payload.message,
      });
    default:
      return state;
  }
};複製程式碼

Reducer類似原來Flux的store,作為資料倉儲來源,這裡將會收到來自呼叫dipatch()後獲得的訊息,並進行處理和儲存,並在及時更新資料後,通過redux的元件繫結,自動反饋至頁面元件中進行資料更新和非同步渲染,而在這裡你應該return一個全新的物件,redux才能知道你是更新了當前元件關聯的reducer,到了這一步你應該會產生疑問,資料狀態是如何反饋至View,而你寫的普普通通的Action和Reducer等js檔案是如何關聯上你的元件和應用。

繫結和引入

入口元件Root.js:

import React, { Component } from 'react';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import Fetcher from './network/fetcher';
import Main from './containers/mainContainer';
import rootReducer from './reducers/rootReducer';

const middlewares = [thunk];
const createStoreWithMiddleware = applyMiddleware(...middlewares)(createStore);
const createLogger = require('redux-logger');

if (process.env.NODE_ENV === 'development') {
  const logger = createLogger();
  middlewares.push(logger);
}

function configureStore(initialState) {
  const store = createStoreWithMiddleware(rootReducer, initialState);
  return store;
}

const store = configureStore();

export default class Root extends Component {
  constructor(props) {
    super(props);
    Fetcher.initNetworkState();
  }

  componentWillUnmount() {
    Fetcher.removeNetworkStateListener();
  }

  render() {
    return (
      <Provider store={store}>
        <Main {...this.props} />
      </Provider>
    );
  }
}複製程式碼

其中rootReducer.js:

import { combineReducers } from 'redux';
import LoginReducer from './loginReducer';
...

const rootReducer = combineReducers({
  loginReducer: LoginReducer,
  ...
});

export default rootReducer;複製程式碼

先使用combineReducer將所有的reducer合併成一個rootReducer,使用rootReducer,在接著開始通過createStore方法建立了store物件,通過redux提供的Provider元件直接將store物件繫結至實際的View元件上,這樣就完成了

View 觸發資料更新 —> Actions 將資料傳遞到 Store —> Store 更新 state —> 更新 View。複製程式碼

關於其中的applyMiddlewarebindActionCreators等等方法是關於非同步actions、非同步資料流、Middleware的進階知識。具體內容建議檢視Redux官方文件瞭解相關內容。以下Redux進階也將會大概講解他們的使用和為什麼使用。

Redux進階

使用redux-thunk和redux-logger框架,並使用applyMiddleware和Middleware來加強Redux的使用,使Redux更加強大、規範和合理。其中redux-logger框架簡單理解就是新增一個Redux日誌列印和處理框架,開發者無需知道他如何規範列印出Redux的dispatch日誌的,但是在開發時,用於debug等是很有用的工具。redux-thunk屬於處理非同步Actions和非同步資料流的框架。

非同步Actions和非同步資料流

什麼是非同步Actions和非同步資料流,簡單來說,就是網路請求來控制的Actions。如App中你點選一個按鈕立即發出了一個dispatch(), 這是你對App控制作出的dispatch(), 這叫做同步Actions,而非同步Actions並不是你控制的,如網路請求成功或失敗後,才會發出一個dispatch(),這就是非同步Actions,你無法知道這個dispatch()是什麼時間做出的操作,也不知道你發出的是成功的dispatch()或是失敗的dispatch()。

如我loginAction中方法,現在應該就能很好的理解這個方法這樣寫的原理:

const _doLogin = (url, param) => dispatch => {
  dispatch(CommonAction.showLoading());
  return Fetcher.postQsBodyFetch(url, param)
      .then((response) => {
        dispatch(CommonAction.dismissLoading());
        dispatch(_loginSuccess(param, response));
      }).catch((error) => {
        dispatch(CommonAction.dismissLoading());
        dispatch(_loginFailed(error));
      });
};複製程式碼

Middleware

middleware翻譯成中文意思中介軟體,很貼切也很容易理解,像redux-thunk 或 redux-promise就可以叫做中介軟體,如果你想使用這些中介軟體,就需要使用applyMiddleware等等相關方法為你的專案新增上這些框架。使專案使用redux更加強大和規範。

像redux-thunk 或 redux-promise 這樣支援非同步的 middleware 都包裝了 store 的 dispatch() 方法,以此來讓你 dispatch 一些除了 action 以外的其他內容,例如:函式或者 Promise。你所使用的任何 middleware 都可以以自己的方式解析你 dispatch 的任何內容,並繼續傳遞 actions 給下一個 middleware。比如,支援 Promise 的 middleware 能夠攔截 Promise,然後為每個 Promise 非同步地 dispatch 一對 begin/end actions。

當 middleware 鏈中的最後一個 middleware 開始 dispatch action 時,這個 action 必須是一個普通物件。這是 同步式的 Redux 資料流 開始的地方(譯註:這裡應該是指,你可以使用任意多非同步的 middleware 去做你想做的事情,但是需要使用普通物件作為最後一個被 dispatch 的 action ,來將處理流程帶回同步方式)。

middleware 可以完成包括非同步 API 呼叫在內的各種事情,瞭解它的演化過程是一件相當重要的事。而他們是如何演化過來的,並如何加強你的應用的,這裡不再具體說明。

總結

Redux很強大,也相對複雜。簡單的專案也許並不需要使用到,但是如果你的專案越來越大,資料越來越複雜,Redux將會使你專案更加規範和健壯。在專案中使用規範的框架架構,是一件非常重要的事情,你可以為你的專案使用架構,這是一件很有趣的事情。

文章很長,Redux也很複雜,文中如有不對請告知,我會及時改正。一起進步和學習。謝謝!

參考

Redux 中文文件

Redux 入門教程-阮一峰

相關文章