Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

redux 的 middleware 詳解 #4

Open
aszx87410 opened this issue Dec 3, 2018 · 0 comments
Open

redux 的 middleware 詳解 #4

aszx87410 opened this issue Dec 3, 2018 · 0 comments
Labels
Front-End Front-End React Reat

Comments

@aszx87410
Copy link
Owner

之前寫了一篇文章簡單的筆記一下自己在看redux的心得,在這邊還是要再次推薦官方文件,因為寫的超級清楚。

但是之前在看官方文件的時候,middleware的地方沒有完全看懂,看到後面就霧煞煞了
這次重看了一遍官方文件講middleware跟非同步操作的地方,邊看邊做筆記,總算是把middleware的實作原理弄懂了
依照慣例分享一下心得
官方文件(中譯版,但是還沒翻譯到這篇)

官方文件很棒的點就是這篇不只教你怎麼用,還從頭講起,讓你知道為什麼middleware會是現在這樣的形式。

正文

你在看了某些教學文章之後覺得redux實在是太棒了,於是開始採用redux做自己的產品
可是此時此刻你突然想要做一個功能:logging,你想記錄每個action,以及執行action完以後store的改變
該怎麼做呢?先從最簡單的方法開始吧!

第一次嘗試:最直覺的方法

假設我們原本dispatch action的code是這樣寫

store.dispatch(addTodo('Use Redux'));

我們可以直接改成

let action = addTodo('Use Redux');

console.log('dispatching', action);
store.dispatch(action);
console.log('next state', store.getState());

第二次嘗試:包成函式

但是第一種方法大家都知道不能這樣,因為程式裡面一定不只一個地方需要做這件事情
那接下來怎麼辦呢?把它包成函式吧

function dispatchAndLog(store, action) {
  console.log('dispatching', action);
  store.dispatch(action);
  console.log('next state', store.getState());
}
dispatchAndLog(store, addTodo('Use Redux'));

可是這樣子,你每個需要dispatch的地方都要import這個函式,有沒有更好的作法呢?

第三次嘗試:Monkeypatching

什麼是Monkeypatch?大家可以自己估狗一下
大意就是:在runtime的時候把某個東西換掉
就像是你可以在你的chrome devtool裡面寫

console.log = function(text){
  alert(text);
}

這樣子原本會在console裡面出現的訊息,就會全部用alert顯示了
那用在這裡我們要怎麼用呢?

//先把原本的存起來,因為之後會用到
let next = store.dispatch;

//覆蓋掉現在的
store.dispatch = function dispatchAndLog(action) {
  console.log('dispatching', action);
  
  //執行
  next(action);
  console.log('next state', store.getState());
  return;
};

//呼叫方式跟之前一樣
store.dispatch(addTodo('Use Redux'));

這樣子的話,原本的code完全不用更動,你只要在程式剛開始的地方把store.dispatch換掉就好了
實在是輕鬆又愉快,但是我們很快就碰到了一個新的問題

如果我現在想要一個錯誤回報的機制怎麼辦?當dispatch出錯的時候,我想把錯誤傳回server

嗯...好問題
我們可以把這兩個想做的事情獨立成兩個function,差不多像是這樣

function patchStoreToAddLogging(store) {
  let next = store.dispatch;
  store.dispatch = function dispatchAndLog(action) {
    console.log('dispatching', action);
    next(action);
    console.log('next state', store.getState());
    return;
  };
}

function patchStoreToAddCrashReporting(store) {
  let next = store.dispatch;
  store.dispatch = function dispatchAndReportErrors(action) {
    try {
      next(action);
    } catch (err) {
      console.error('Caught an exception!', err);
      Raven.captureException(err, {
        extra: {
          action,
          state: store.getState()
        }
      });
      throw err;
    }
  };
}

patchStoreToAddLogging(store);
patchStoreToAddCrashReporting(store);

剛開始讀到這段的時候我就有點暈了,不太懂為什麼這樣子可以
後面的不是會把前面的store.dispatch覆蓋掉嗎?後來再看幾次終於看懂,精髓就在於那個let next = store.dispatch;
上面code的行為大概會是這樣:

  1. 執行第一個函式patchStoreToAddLogging(store);
  2. 現在的dispatch(redux真正發送action的函式)被保存起來
  3. store.dispatch被換掉,換成dispatchAndLog這個函式,這個函式的行為會是:做紀錄並且呼叫原本的dispatch
  4. 執行第二個函式patchStoreToAddCrashReporting(store);
  5. 現在的dispatch(注意,已經變成dispatchAndLog這個函式了)被保存起來
  6. store.dispatch被換掉,換成dispatchAndReportErrors這個函式,這個函式的行為會是:做紀錄並且呼叫原本的dispatc

好,接著若是我們呼叫dispatch(..),看一下流程會怎麼走

  1. 因為剛剛被換掉,所以執行dispatchAndReportErrors
  2. 執行next(action),而next就是之前保存的dispatch,就是dispatchAndLog
  3. 執行dispatchAndLog
  4. 做紀錄,然後執行next(action),而next就是之前保存的dispatch,就是最原始的dispatch
  5. 執行最原始的dispatch,執行完畢以後跳回dispatchAndLog這個function
  6. 回到dispatchAndLog,印出改變後的state
  7. 回到patchStoreToAddCrashReporting,因為都沒有錯誤,所以不做任何事
  8. 結束

這樣子就達成我們middleware最重要的功能,可以讓action經過一層又一層的中間件,最後抵達store
但是這樣子還是不夠好

第四次嘗試:把Monkeypatching藏起來

之前我們都是直接把store.dispatch換掉,那如果我們不要直接換掉,而是傳回一個function,會發生什麼事?

function logger(store) {
  let next = store.dispatch;

  // 之前的:
  // store.dispatch = function dispatchAndLog(action) {

  return function dispatchAndLog(action) {
    console.log('dispatching', action);
    let result = next(action);
    console.log('next state', store.getState());
    return result;
  };
}

假設我們的crashReporter也改成這種形式,那我們就可以

store.dispatcher = logger(store);
store.dispatcher = crashReporter(store);

其實只是把store.dispatcher抽出來,獨立在function外面而已
但這樣做的好處就是,我們可以這樣

function applyMiddlewareByMonkeypatching(store, middlewares) 
  // Transform dispatch function with each middleware.
  middlewares.forEach(middleware =>
    store.dispatch = middleware(store)
  );
}
applyMiddlewareByMonkeypatching(store, [logger, crashReporter]);

但是這樣子其實只是把monkeypatch的地方抽出來而已,實際上還是在
下一步,我們要把monkeypatch這個方法徹底移除掉

第五次嘗試:把monkeypatching移除掉

為什麼我們要override dispatch
有一個很重要的因素就是,這樣才能不斷呼叫之前的dispatch

function logger(store) {
  // 這行超重要,有了這行才能達成chaining
  let next = store.dispatch;

  return function dispatchAndLog(action) {
    console.log('dispatching', action);
    let result = next(action);
    console.log('next state', store.getState());
    return result;
  };
}

如果少了那重要的一行,那就無法達成chaining的效果
但其實除了這樣子寫,我們還有另外一個方法可以用
我們可以接收一個next的參數,達到相同目的
官方文件接著有點講太快了,咻咻咻就直接把最重要的一個部分講完
我這邊試著把進度放慢,講更多細節的東西,其實就是差在currying這個概念

繼續剛剛講的,我們可以接收一個next的參數,就會變成這樣

function logger(store, next) {
  return function dispatchAndLog(action) {
    console.log('dispatching', action);
    let result = next(action);
    console.log('next state', store.getState());
    return result;
  };
}

可以看到跟之前長得很像,只是原本的next是放在裡面,現在變成參數傳進來
那我們在使用的時候就變成

let dispatch = store.dispatch;
dispatch = crashReporter(store, dispatch);
dispatch = logger(store, dispatch);
dispatch(addTodo('Use Redux'));

function logger(store, next) {
  return function dispatchAndLog(action) {
    console.log('dispatching', action);
    let result = next(action);
    console.log('next state', store.getState());
    return result;
  };
}

function crashReport(store, next) {
  return function dispatchAndReportErrors(action) {
    try {
      return next(action);
    } catch (err) {
      console.error('Caught an exception!', err);
      throw err;
    }
  };
}

差別在於函式裡面的let next = store.dispatch;被拿掉了
我們的middleware函式loggercrashReport變得更乾淨了點
改成這樣之後,把我們原本的applyMiddleware也改掉,讓它符合新的寫法

function applyMiddleware(store, middlewares) {

  let dispatch = store.dispatch;
  middlewares.forEach(middleware =>
    dispatch = middleware(store,dispatch)
  );

  return Object.assign({}, store, { dispatch });
}

//呼叫的方式一模一樣,只是差別在於這個function現在會回傳一個store
applyMiddleware(store, [logger, crashReporter]);

接著我們回來看官方給的文件跟範例,跟現在我們寫的有哪邊不一樣
第一個點是,loggercrashReport我們是傳入兩個參數,但是官方的實現只傳入了一個
用了一種叫做currying的技巧
什麼是currying?就是把多個參數的函數切成很多只有一個參數的函數
直接看範例比較了解:

function max(a,b){
	return a>b?a:b;
}
max(1,5);

function maxCurrying(a){
	return function inner(b){
	  return a>b?a:b;
	}
}
maxCurrying(1)(5);

有了基本概念以後,就可以把我們剛剛的logger函式也做這樣的改變

//可以跟原來的比較,發現只是多一層函數包住而已
function logger(store) {
  return function wrapDispatchToAddLogging(next) {
    return function dispatchAndLog(action) {
      console.log('dispatching', action);
      let result = next(action);
      console.log('next state', store.getState());
      return result;
    };
  }
}

//ES6的寫法
const logger = store => next => action => {
  console.log('dispatching', action);
  let result = next(action);
  console.log('next state', store.getState());
  return result;
};

//原來的
function logger(store, next) {
  return function dispatchAndLog(action) {
    console.log('dispatching', action);
    let result = next(action);
    console.log('next state', store.getState());
    return result;
  };
}

在我當初看官方文件的時候,最困擾我的就是這一段
因為之前很少用這種function回傳function又回傳function的寫法
所以一下子被弄得頭昏眼花
於是只好先找出不要那麼多層函數的方法,然後了解currying之後再去了解原本的code
對我來說這樣子會比較容易啦,不然一下子跳太快

applyMiddleware就可以改成這樣

function applyMiddleware(store, middlewares) {
  let dispatch = store.dispatch;
  middlewares.forEach(middleware =>
    dispatch = middleware(store)(dispatch) //差在這裡而已
  );
  return Object.assign({}, store, { dispatch });
}

//原來的
function applyMiddleware(store, middlewares) {

  let dispatch = store.dispatch;
  middlewares.forEach(middleware =>
    dispatch = middleware(store,dispatch) //差在這裡而已
  );

  return Object.assign({}, store, { dispatch });
}

其實做到這邊,就已經跟redux本身的實現有個八分像了
事實上middleware的寫法其實就是redux要求的寫法
最後來看一下官方提供的用法要怎麼用

import { createStore, combineReducers, applyMiddleware } from 'redux';

// applyMiddleware takes createStore() and returns
// a function with a compatible API.
// 注意到這邊,我們剛剛的寫法是:applyMiddleware(store, [logger, crashReporter]);
// 這邊做了currying,所以把兩個參數變成兩個函數
// 本來我們是傳進陣列,這邊把陣列拿掉了,只要依序傳進去就好
let createStoreWithMiddleware = applyMiddleware(
  logger,
  crashReporter
)(createStore);

// Use it like you would use createStore()
let todoApp = combineReducers(reducers);

//用createStoreWithMiddleware取代原本的createStore
let store = createStoreWithMiddleware(todoApp);

總結

在看官方文件的時候,一直到前面都還看得懂,只是一下突然跳到currying那邊
因為對這個概念比較陌生,所以就暈頭轉向了
後來下定決心一定要看懂,重新看一次之後,一行一行去理解是什麼意思、流程怎麼跑
之後就比較清楚了

這邊大幅度參考官方範例,code也是直接抄來的,只是有做了一點小改變所以在這邊說明
像是官方範例的applyMiddleware裡面,其實前面都有

middlewares = middlewares.slice();
middlewares.reverse();

reverse()是因為執行順序的關係,slice()是複製這個array
但是為什麼要複製我就不知道了,是因為不要改到原本的參數嗎?

總之,希望這篇文章能幫助到一些跟我一樣迷惑的初學者
最後還是要推薦大家去看官方文件
如果有哪邊寫錯,還麻煩留言或是寄信跟我說,感謝大家

ref:
http://camsong.github.io/redux-in-chinese/docs/advanced/Middleware.html

@aszx87410 aszx87410 added React Reat Front-End Front-End labels Dec 3, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Front-End Front-End React Reat
Projects
None yet
Development

No branches or pull requests

1 participant