react-redux實踐總結

標題

做者:趙瑋龍 前後就任於麪包旅行,阿里體育,如今就任於美團。涉及技術範圍React, AngularJS, gulp, grunt, webpack, redux, canvas, node等,如今專一於前端react周邊技術棧研究javascript

特此聲明:本篇文章都是圍繞react技術棧展開的若是你要跟我辯解是React,Vue,AngularJS之間區別,網絡上文章數不勝數,能夠移步別處.我假設你已經對react,redux有一些實戰經驗,基本的東西不會涉及

文章專著於如何儘可能作到react-redux最佳實踐前端

redux的必要性:

Eric在Medium上發文噴setState
這件事情很早就引發熱議
其實無非就是setState的對於新手不友好以及文檔的晦澀致使的
React抽象來講,就是一個公式
UI=f(state)
咱們把最終繪製出來的UI當作一個函數f運行的結果,f就是React和咱們基於React寫得代碼,而f的輸入參數就是state。
做爲React管理state的一個重要方法,setState確定很是重要,若是隻是簡單用法,也不會有任何問題,可是若是用得深,就會發現很……尷尬。
我剛開始接觸React的時候,就意識到React至關於一個jQuery的替代品,可是就像單獨依靠jQuery難以管理大型項目,因此也須要給配合使用的MVC框架找一個替代品,我選擇的替代品是Redux,我很早就將React和Redux配合使用;如今,回過頭來看看React的setState,發現坑真的很多,不由感嘆本身仍是挺走運的。
對setState用得深了,就容易犯錯,因此咱們開門見山先把理解setState的關鍵點列出來。
  • setState不會馬上改變React組件中state的值;
  • setState經過引起一次組件的更新過程來引起從新繪製;
  • 屢次setState函數調用產生的效果會合並。
    這幾個關鍵點實際上是相互關聯的
setState不會馬上改變React組件中state的值(他是異步觸發的,也就是考慮到列隊處理的必要性)
在React中,一個組件中要讀取當前狀態用是訪問this.state,可是更新狀態倒是用this.setState
若是須要同步咱們仍是更須要
函數式的setState用法
若是傳遞給this.setState的參數不是一個對象而是一個函數,那遊戲規則就變了。
這個函數會接收到兩個參數,第一個是當前的state值,第二個是當前的props,這個函數應該返回一個對象,這個對象表明想要對this.state的更改,換句話說,以前你想給this.setState傳遞什麼對象參數,在這種函數裏就返回什麼對象,不過,計算這個對象的方法有些改變,再也不依賴於this.state,而是依賴於輸入參數state。
能夠這麼寫一個函數。
function increment(state, props) {
  return {count: state.count + 1};
}複製代碼
能夠看到,一樣是把狀態中的count加1,可是狀態的來源不是this.state,而是輸入參數state。
對應incrementMultiple的函數就是這麼寫。
function incrementMultiple() {
  this.setState(increment);
  this.setState(increment);
  this.setState(increment);
}複製代碼
對於屢次調用函數式setState的狀況,React會保證調用每次increment時,state都已經合併了以前的狀態修改結果。
簡單說,加入當前this.state.count的值是0,第一次調用this.setState(increment),傳給increment的state參數是0,第二調用時,state參數是1,第三次調用是,參數是2,最終incrementMultiple的效果,真的就是讓this.state.count變成了3,這個函數incrementMultiple終於實至名歸。
值得一提的是,在increment函數被調用時,this.state並無被改變,依然,要等到render函數被從新執行時(或者shouldComponentUpdate函數返回false以後)才被改變。
讓setState接受一個函數的API設計很棒!由於這符合函數式編程的思想,讓開發者寫出沒有反作用的函數,咱們的increment函數並不去修改組件狀態,只是把「但願的狀態改變」返回給React,維護狀態這些苦力活徹底交給React去作。
正由於流程的控制權交給了React,因此React才能協調多個setState調用的關係。
讓咱們再往前推動一步,試着若是把兩種setState的用法混用,那會有什麼效果?
咱們把incrementMultiple改爲這樣。
function incrementMultiple() {
  this.setState(increment);
  this.setState(increment);
  this.setState({count: this.state.count + 1});
  this.setState(increment);
}複製代碼

在幾個函數式setState調用中插入一個傳統式setState調用(嗯,咱們姑且這麼稱呼之前的setState使用方式),最後獲得的結果是讓this.state.count增長了2,而不是增長4。

緣由也很簡單,由於React會依次合併全部setState產生的效果,雖然前兩個函數式setState調用產生的效果是count加2,可是半路殺出一個傳統式setState調用,一會兒強行把積攢的效果清空,用count加1取代。

這麼看來,傳統式setState的存在,會把函數式setState拖下水啊!只要有一個傳統式的setState調用,就把其餘函數式setState調用給害了。

若是說setState這兒API未來如何改進,也許就該徹底採用函數爲參數的調用方法,廢止對象爲參數的調用方法。

固然,React近期確定不會有這樣的驚世駭俗的改變,可是你們能夠先嚐試函數式setState用法,這纔是setState的將來。

固然這還不是問題所在這裏

關注react的人都知道,facebook提出的flux單向數據流控制,業界也出現了不少相似的flux數據流實現方式

他們其實起到的做用無非是如何去在全局狀態下再也不讓你的組建分而治之,而是具備統一管理state的能力

如今業界比較火的是Mobx和Redux

先對比下兩個庫的實現方式:

那麼具體到這兩種模型,又有一些特定的優缺點呈現出來

先談談 Redux 的優點:

  • 數據流流動很天然,由於任何 dispatch 都會致使廣播,須要依據對象引用是否變化來控制更新粒度。
  • 若是充分利用時間回溯的特徵,能夠加強業務的可預測性與錯誤定位能力。
  • 時間回溯代價很高,由於每次都要更新引用,除非增長代碼複雜度,或使用 immutable。
  • 時間回溯的另外一個代價是 action 與 reducer 徹底脫節,數據流過程須要自行腦補。緣由是可回溯必然不能保證引用關係。
  • 引入中間件,其實主要爲了解決異步帶來的反作用,業務邏輯或多或少參雜着 magic。
  • 可是靈活利用中間件,能夠經過約定完成許多複雜的工做。
  • 對 typescript 支持困難。

Mobx:

  • 數據流流動不天然,只有用到的數據纔會引起綁定,局部精確更新,但免去了粒度控制煩惱。
  • 沒有時間回溯能力,由於數據只有一份引用。
  • 自始至終一份引用,不須要 immutable,也沒有複製對象的額外開銷。
  • 沒有這樣的煩惱,數據流動由函數調用一鼓作氣,便於調試。
  • 業務開發不是腦力活,而是體力活,少一些 magic,多一些效率。
  • 因爲沒有 magic,因此沒有中間件機制,無法經過 magic 加快工做效率(這裏 magic 是指 action 分發到 reducer 的過程)。
  • 完美支持 typescript。

如何來確認這兩個庫的適用場景,實際中若是你的數據結構足夠複雜那麼仍是redux帶來的靈活性以及數據管理模式更加天然,

mobx上手會更快,若是數據結構通常則比較建議這種方式

那麼當咱們確認redux符合複雜業務場景後(後臺業務通常都是複雜業務場景的必發處)

如何善於利用redux爲咱們帶來更好的開發體驗和可維護性高的代碼是此次探討的重點

大致上,Redux 的數據流是這樣的:

界面 => action => reducer => store => react => virtual dom => 界面

每一步都很純淨,看起來很美好對吧?對於一些小小的嘗試性質的 DEMO #### 來講確實很美好。但其實當應用變得愈來愈大的時候,這其中存在諸多問題:

如何優雅地寫異步代碼?(從簡單的數據請求到複雜的異步邏輯)

狀態樹的結構應該怎麼設計?

狀態樹中的狀態愈來愈多,結構愈來愈複雜的時候,和 react #### 的組件映射如何避免混亂?

每次狀態的細微變化都會生成全新的 state #### 對象,其中大部分無變化的數據是不用從新克隆的,這裏如何提升性能?

如何拆分reducer?

state如何解耦選擇數據分片呢?

我只能從業務中一一示範,固然並非上面的問題都解決了,並且以一個更好的方式解決了,這裏只作到拋磚引玉的做用

官方文檔裏介紹了一種很樸素的異步控制中間件 redux-thunk(若是你還不瞭解中間件的話請看 Middleware | Redux 中文文檔,事實上 redux-thunk 的代碼很簡單,簡單到只有幾行代碼:

function createThunkMiddleware(extraArgument) {
    return ({ dispatch, getState }) => next => action => {
        if (typeof action === 'function') {
            return action(dispatch, getState, extraArgument);
        }
        return next(action);
    };
}

//普通action
function foo(){
    return {
        type: 'foo',
        data: 123
    }
}

//異步action
function fooAsync(){
    return dispatch => {
        setTimeout(_ => dispatch(123), 3000);
    }
}複製代碼

但這種簡單的異步解決方法在應用變得複雜的時候,並不能知足需求,反而會使 action 變得十分混亂。

舉個簡單的例子

用普通的 redux-thunk 是這樣寫的:

function upload(data){
    return dispatch => {
        // view
        dispatch({ type: 'SHOW_WAITING_MODAL' });
        // upload
        api.upload(data)
            .then(res => {
            // 成功
            dispatch({ type: 'PRELOAD_IMAGES', data: res.images });
            dispatch({ type: 'HIDE_WAITING_MODAL' });
            })
        .catch(err => {
            // 錯誤
            dispatch({ type: 'SHOW_ERROR', data: err });
            dispatch({ type: 'HIDE_WAITING_MODAL' });
            setTimeout(_ => dispatch({ type: 'HIDE_ERROR' }), 2000);
        })
    }
}複製代碼

這裏的問題在於,一個異步的 upload action 執行過程當中會產生好幾個新的 action,更可怕的是這些新的 action 也是包含邏輯的(好比要判斷是否錯誤),這直接致使異步代碼中處處都是 dispatch(action),是很不可控的狀況。若是還要進一步考慮取消、超時、隊列的狀況,就更加混亂了。

下面咱們來看看若是換成 redux-saga 的話會怎麼樣:

import { take, put, call, delay } from 'redux-saga/effects'
// 上傳的異步流
function *uploadFlow(action) {
    // 顯示出加載效果
      yield put({ type: 'SHOW_WAITING_MODAL' });
      // 簡單的 try-catch
      try{
          const response = yield call(api.upload, action.data);
        yield put({ type: 'PRELOAD_IMAGES', data: response.images });
        yield put({ type: 'HIDE_WAITING_MODAL' });
      }catch(err){
          yield put({ type: 'SHOW_ERROR', data: err });
        yield put({ type: 'HIDE_WAITING_MODAL' });
        yield delay(2000);
          yield put({ type: 'HIDE_ERROR' });
      }     
}

function* watchUpload() {
  yield* takeEvery('BEGIN_REQUEST', uploadFlow)
}複製代碼

是否是規整不少呢?redux-saga 容許咱們使用簡單的 try-catch 來進行錯誤處理,更神奇的是居然能夠直接使用 delay 來替代 setTimeout 這種會形成回調和嵌套的不優雅的方法。

本質上講,redux-sage 提供了一系列的『反作用(side-effects)方法』,好比如下幾個

  • put(產生一個 action)
  • call(阻塞地調用一個函數)
  • fork(非阻塞地調用一個函數)
  • take(監聽且只監聽一次 action)
  • delay(延遲)
  • race(只處理最早完成的任務)

而且經過 Generator 實現對於這些反作用的管理,讓咱們能夠用同步的邏輯寫一個邏輯複雜的異步流。

下面這個例子出自於官方文檔,實現了一個對於請求的隊列,即讓程序同一時刻只會進行一個請求,其它請求則排隊等待,直到前一個請求結束:

import { buffers } from 'redux-saga';
import { take, actionChannel, call, ... } from 'redux-saga/effects';

function* watchRequests() {
  // 1- 建立一個針對請求事件的 channel
  const requestChan = yield actionChannel('REQUEST');
  while (true) {
    // 2- 從 channel 中拿出一個事件
    const {payload} = yield take(requestChan);
    // 3- 注意這裏咱們使用的是阻塞的函數調用
    yield call(handleRequest, payload);
  }
}複製代碼

可是我在項目中並無適用redux-saga一個是由於會增長組員的學習成本,一個是代碼迭代過快形成的落差

因此我在代碼中把請求異步處理封裝成一個簡單的只有開始,成功,和錯誤處理的機制

import 'whatwg-fetch'
import handleError from './handleError'

// 設定一個symbol類型作爲惟一的屬性名
export const CALL_API = Symbol('call_api')

const API_HOST = process.env.API_HOST || 'http://localhost:8080/pc'

export default store => next => action => {
  const callApi = action[CALL_API]
  if (typeof callApi === 'undefined') {
    return next(action)
  }

  // 獲取action中參數
  let { endpoint,
        types: [requestType, successType, failureType],
        method,
        body,
        ...options
      } = callApi
  let finalBody = body

  if (method) {
    options.method = method.toUpperCase()
  }
  if (typeof body === 'function') {
    finalBody = body(store.getState())
  }
  if (finalBody) {
    options.body = JSON.stringify(finalBody)
    options.headers = { 'content-type': 'application/json', 'agent': 'pc' }
  } else {
    options.headers = { 'cache-control': 'no-cache', 'agent': 'pc' }
  }
  // 替換action標記方法
  const actionWith = data => {
    const finalAction = Object.assign({}, action, data)
    delete finalAction[CALL_API]
    return finalAction
  }

  next(actionWith({ type:requestType }))

  return fetch(`${API_HOST}${endpoint}`,{
    credentials: 'include',
    ...options,
  })
  .then(response => {
    if (response.status === 204) {
      return { response }
    }
    const type = response.headers.get('content-type')
    if (type && type.split(';')[0] === 'application/json') {
      return response.json().then(json => ({ json, response }))
    }
    return response.text().then(text => ({ text, response }))
  })
  .then(({ json, text, response }) => {
    if (response.ok) {
      if (json) {
        if (json.status === 200 && json.data) {
          next(actionWith({ type: successType, payload: json.data }))
        } else if (json.status === 500) {
          next(actionWith({ type: successType, payload: json.msg }))
        } else {
          next(actionWith({ type: successType }))
        }
      }
    } else {
      if (json) {
        let error = { status: response.status }
        if (typeof json === 'object') {
          error = { ...error, ...json }
        } else {
          error.msg = json
        }
        throw error
      }
      const error = {
        name: 'FETCH_ERROR',
        status: response.status,
        text,
      }
      throw error
    }
  })
  .catch((error) => {
    next(actionWith({ type: failureType, error }))
    handleError(error)
  })
}複製代碼

咱們能夠利用symbol定一個咱們須要處理的機制而後去處理每次返回的結果,只是用到了redux-thunk 做爲一個thunk函數去返回有反作用的請求

結構狀態state應該如何去設計呢?vue

咱們考慮到官方給出的建議用entities去維護咱們所須要的數據,由於業務中表單居多,而且表單複雜,

考慮到適用場景咱們會根據reducer的概念去講解

reducer就是實現(state, action) => newState的純函數,也就是真正處理state的地方。值得注意的是,Redux並不但願你修改老的state,並且經過直接返回新state的方式去修改。

在講如何設計reducer以前,先介紹幾個術語:

reducer:實現(state, action) -> newState的純函數,能夠根據場景分爲如下好幾種

  • root reducer:根reducer,做爲createStore的第一個參數
  • slice reducer:分片reducer,相對根reducer來講的。用來操做state的一部分數據。多個分片reducer能夠合併成一個根reducer
  • higher-order reducer:高階reducer,接受reducer做爲參數的函數/返回reducer做爲返回值的函數。
  • case function:功能函數,接受指定action後的更新邏輯,能夠是簡單的reducer函數,也能夠接受其餘參數。

reducer的最佳實踐主要分爲如下幾個部分

  • 抽離工具函數,以便複用。
  • 抽離功能函數(case function),精簡reducer聲明部分的代碼。
  • 根據數據類別拆分,維護多個獨立的slice reducer。
  • 合併slice reducer。
  • 經過crossReducer在多個slice reducer中共享數據。
  • 減小reducer的模板代碼。

接下來,咱們詳細的介紹每一個部分

如何抽離工具函數?

抽離工具函數,幾乎在任何一個項目中都須要。要抽離的函數須要知足如下條件:

純淨,和業務邏輯不耦合
功能單一,一個函數只實現一個功能java

因爲reducer都是對state的增刪改查,因此會有較多的重複的基礎邏輯,針對reducer來抽離工具函數,簡直恰到好處。

// 好比對象更新,淺拷貝
export const updateObject = (oldObj, newObj) => {
    return assign({}, oldObj, newObj);
}
// 好比對象更新,深拷貝
export const deepUpdateObject = (oldObj, newObj) => {
    return deepAssign({}, oldObj, newObj);
}複製代碼

工具函數抽離出來,建議放到單獨的文件中保存。

如何抽離 case function 功能函數?

不要被什麼case function嚇到,直接給你看看代碼你就清楚了,也是體力活,目的是爲了讓reducer的分支判斷更清晰

// 抽離前,全部代碼都揉到slice reducer中,不夠清晰
function appreducer(state = initialState, action) {
    switch (action.type) {
        case 'ADD_TODO':
            ...
            ...
            return newState;
        case 'TOGGLE_TODO':
            ...
            ...
            return newState;
        default:
            return state;
    }
}

// 抽離後,將全部的state處理邏輯放到單獨的函數中,reducer的邏輯格外清楚
function addTodo(state, action) {
    ...
    ...
    return newState;
}
function toggleTodo(state, action) {
    ...
    ...
    return newState;
}
function appreducer(state = initialState, action) {
    switch (action.type) {
        case 'ADD_TODO':
            return addTodo(state, action);
        case 'TOGGLE_TODO':
            return toggleTodo(state, action);
        default:
            return state;
    }
}複製代碼

case function就是指定action的處理函數,是最小粒度的reducer。

抽離case function,可讓slice reducer的代碼保持結構上的精簡。

#### 如何設計slice reducer?

咱們須要對state進行拆分處理,而後用對應的slice reducer去處理對應的數據,好比article相關的數據用articlesReducer去處理,paper相關的數據用papersReducer去處理。

這樣能夠保證數據之間解耦,而且讓每一個slice reducer保持代碼清晰而且相對獨立。

好比業務中有shopInfo和bankInfol兩個類別的數據,咱們拆分state並扁平化改造

export default (state = initialState, action) => {
  switch (action.type) {
    case allTypes.SHOPINFO_REQ:
      return {
        ...state,
        isloading: true,
      }
    case allTypes.SHOPINFO_SUCCESS:
      return {
        ...state,
        isloading: false,
        productId: action.payload.productId,
      }
    default:
      return state
}複製代碼

注意一下這裏的解構對於shopinfo來講他並不感知到state的存在對於他來講他就是shop

那麼這裏的select對於要渲染的組建來說是一個道理,咱們不敢知如何在組建中渲染,只是選擇咱們這個分片中的數據

因爲咱們的state進行了扁平化改造,因此咱們須要在case function中進行normalizr化。

根據state的拆分,設計出對應的slice reducer,讓他們對本身的數據分別管理,這樣後代碼更便於維護,但也引出了兩個問題。

拆分多個slice reducer,但createStore只能接受一個reducer做爲參數,因此咱們怎麼合併這些slice reducer呢?

每一個slice reducer只負責管理自身的數據,對state並不知情。那麼shop怎麼去改變state.entities的數據呢?

這兩個問題,分別引出了兩部份內容,分別是:slice reducer合併、slice reducer數據共享。

如何合併多個slice reducer?

redux提供了combineReducer方法,能夠用來合併多個slice reducer,返回root reducer傳遞給createStore使用。直接上代碼,很是簡單。

combineReducers({
    entities: entitiesreducer,

    // 對於shopReducer來講,他接受(state, action) => newState,
    // 其中的state,是shop,也就是state.shopinfo
    // 它並不能獲取到state的數據,更不能獲取到state.papers的數據
    shopinfo: shopinfoReducer,
    bankinfo: bankinfoReducer
})複製代碼

傳遞給combineReducer的是key-value 鍵值對,其中鍵表示傳遞到對應reducer的數據,也就是說:slice reducer中的state並非全局state,而是state.articles/state.papers等數據。

結語

若是解決多個slice reducer間共享數據的問題?

slice reducer本質上是爲了實現專門數據專門管理,讓數據管理更清晰。那麼slice reducer間如何共享數據呢?

如何在一個回傳數據中拿到另外一個共享數據的的數據呢?透傳給一個reducer嗎?固然一點都不優雅。。。。預計下次再講

最後,團隊爲了招聘方便,整了個公衆號,主要是一些招聘信息,團隊信息,全部的技術文章在公衆號裏也能夠看到,對了,若是你想去美團其餘團隊,咱們也能夠幫你內推哦 ~ node

二維碼
二維碼
相關文章
相關標籤/搜索