<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          前端網(wǎng)紅框架的插件機制全梳理(axios、koa、redux、vuex)

          共 11138字,需瀏覽 23分鐘

           ·

          2020-05-13 23:23


          前言

          前端中的庫很多,開發(fā)這些庫的作者會盡可能的覆蓋到大家在業(yè)務中千奇百怪的需求,但是總有無法預料到的,所以優(yōu)秀的庫就需要提供一種機制,讓開發(fā)者可以干預插件中間的一些環(huán)節(jié),從而完成自己的一些需求。

          本文將從koa、axiosvuexredux的實現(xiàn)來教你怎么編寫屬于自己的插件機制。

          • 對于新手來說:
            本文能讓你搞明白神秘的插件和攔截器到底是什么東西。

          • 對于老手來說:
            在你寫的開源框架中也加入攔截器或者插件機制,讓它變得更加強大吧!

          axios

          首先我們模擬一個簡單的 axios,這里不涉及請求的邏輯,只是簡單的返回一個 Promise,可以通過 config 中的 error 參數(shù)控制 Promise 的狀態(tài)。

          axios 的攔截器機制用流程圖來表示其實就是這樣的:

          8851b3d16c24786bf3d342e0d94e672d.webp流程圖
          const?axios?=?config?=>?{
          ??if?(config.error)?{
          ????return?Promise.reject({
          ??????error:?"error?in?axios"
          ????});
          ??}?else?{
          ????return?Promise.resolve({
          ??????...config,
          ??????result:?config.result
          ????});
          ??}
          };

          如果傳入的 config 中有 error 參數(shù),就返回一個 rejected 的 promise,反之則返回 resolved 的 promise。

          先簡單看一下 axios 官方提供的攔截器示例:

          axios.interceptors.request.use(
          ??function(config)?{
          ????//?在發(fā)送請求之前做些什么
          ????return?config;
          ??},
          ??function(error)?{
          ????//?對請求錯誤做些什么
          ????return?Promise.reject(error);
          ??}
          );

          //?添加響應攔截器
          axios.interceptors.response.use(
          ??function(response)?{
          ????//?對響應數(shù)據(jù)做點什么
          ????return?response;
          ??},
          ??function(error)?{
          ????//?對響應錯誤做點什么
          ????return?Promise.reject(error);
          ??}
          );

          可以看出,不管是 request 還是 response 的攔截器,都會接受兩個函數(shù)作為參數(shù),一個是用來處理正常流程,一個是處理失敗流程,這讓人想到了什么?

          沒錯,promise.then接受的同樣也是這兩個參數(shù)。

          axios 內(nèi)部正是利用了 promise 的這個機制,把 use 傳入的兩個函數(shù)作為一個intercetpor,每一個intercetpor都有resolvedrejected兩個方法。

          //?把
          axios.interceptors.response.use(func1,?func2)

          //?在內(nèi)部存儲為
          {
          ????resolved:?func1,
          ????rejected:?func2
          }

          接下來簡單實現(xiàn)一下,這里我們簡化一下,把axios.interceptor.request.use轉為axios.useRequestInterceptor來簡單實現(xiàn):

          //?先構造一個對象?存放攔截器
          axios.interceptors?=?{
          ??request:?[],
          ??response:?[]
          };

          //?注冊請求攔截器
          axios.useRequestInterceptor?=?(resolved,?rejected)?=>?{
          ??axios.interceptors.request.push({?resolved,?rejected?});
          };

          //?注冊響應攔截器
          axios.useResponseInterceptor?=?(resolved,?rejected)?=>?{
          ??axios.interceptors.response.push({?resolved,?rejected?});
          };

          //?運行攔截器
          axios.run?=?config?=>?{
          ??const?chain?=?[
          ????{
          ??????resolved:?axios,
          ??????rejected:?undefined
          ????}
          ??];

          ??//?把請求攔截器往數(shù)組頭部推
          ??axios.interceptors.request.forEach(interceptor?=>?{
          ????chain.unshift(interceptor);
          ??});

          ??//?把響應攔截器往數(shù)組尾部推
          ??axios.interceptors.response.forEach(interceptor?=>?{
          ????chain.push(interceptor);
          ??});

          ??//?把config也包裝成一個promise
          ??let?promise?=?Promise.resolve(config);

          ??//?暴力while循環(huán)解憂愁
          ??//?利用promise.then的能力遞歸執(zhí)行所有的攔截器
          ??while?(chain.length)?{
          ????const?{?resolved,?rejected?}?=?chain.shift();
          ????promise?=?promise.then(resolved,?rejected);
          ??}

          ??//?最后暴露給用戶的就是響應攔截器處理過后的promise
          ??return?promise;
          };

          axios.run這個函數(shù)看運行時的機制,首先構造一個chain作為 promise 鏈,并且把正常的請求也就是我們的請求參數(shù) axios 也構造為一個攔截器的結構,接下來

          • 把 request 的 interceptor 給 unshift 到chain頂部
          • 把 response 的 interceptor 給 push 到chain尾部

          以這樣一段調(diào)用代碼為例:

          //?請求攔截器1
          axios.useRequestInterceptor(resolved1,?rejected1);
          //?請求攔截器2
          axios.useRequestInterceptor(resolved2,?rejected2);
          //?響應攔截器1
          axios.useResponseInterceptor(resolved1,?rejected1);
          //?響應攔截器
          axios.useResponseInterceptor(resolved2,?rejected2);

          這樣子構造出來的 promise 鏈就是這樣的chain結構:

          [
          ????請求攔截器2,//?↓config
          ????請求攔截器1,//?↓config
          ????axios請求核心方法,?//?↓response
          ????響應攔截器1,?//?↓response
          ????響應攔截器//?↓response
          ]

          至于為什么 requestInterceptor 的順序是反過來的,仔細看看代碼就知道 XD。

          有了這個chain之后,只需要一句簡短的代碼:

          let?promise?=?Promise.resolve(config);

          while?(chain.length)?{
          ??const?{?resolved,?rejected?}?=?chain.shift();
          ??promise?=?promise.then(resolved,?rejected);
          }

          return?promise;

          promise 就會把這個鏈從上而下的執(zhí)行了。

          以這樣的一段測試代碼為例:

          axios.useRequestInterceptor(config?=>?{
          ??return?{
          ????...config,
          ????extraParams1:?"extraParams1"
          ??};
          });

          axios.useRequestInterceptor(config?=>?{
          ??return?{
          ????...config,
          ????extraParams2:?"extraParams2"
          ??};
          });

          axios.useResponseInterceptor(
          ??resp?=>?{
          ????const?{
          ??????extraParams1,
          ??????extraParams2,
          ??????result:?{?code,?message?}
          ????}?=?resp;
          ????return?`${extraParams1}?${extraParams2}?${message}`;
          ??},
          ??error?=>?{
          ????console.log("error",?error);
          ??}
          );
          1. 成功的調(diào)用

          在成功的調(diào)用下輸出 result1: extraParams1 extraParams2 message1

          (async?function()?{
          ??const?result?=?await?axios.run({
          ????message:?"message1"
          ??});
          ??console.log("result1:?",?result);
          })();
          1. 失敗的調(diào)用
          (async?function()?{
          ??const?result?=?await?axios.run({
          ????error:?true
          ??});
          ??console.log("result3:?",?result);
          })();

          在失敗的調(diào)用下,則進入響應攔截器的 rejected 分支:

          首先打印出攔截器定義的錯誤日志:
          error { error: 'error in axios' }

          然后由于失敗的攔截器

          error?=>?{
          ??console.log('error',?error)
          },

          沒有返回任何東西,打印出result3: undefined

          可以看出,axios 的攔截器是非常靈活的,可以在請求階段任意的修改 config,也可以在響應階段對 response 做各種處理,這也是因為用戶對于請求數(shù)據(jù)的需求就是非常靈活的,沒有必要干涉用戶的自由度。

          vuex

          vuex 提供了一個 api 用來在 action 被調(diào)用前后插入一些邏輯:

          https://vuex.vuejs.org/zh/api/#subscribeaction

          store.subscribeAction({
          ??before:?(action,?state)?=>?{
          ????console.log(`before?action?${action.type}`);
          ??},
          ??after:?(action,?state)?=>?{
          ????console.log(`after?action?${action.type}`);
          ??}
          });

          其實這有點像 AOP(面向切面編程)的編程思想。

          在調(diào)用store.dispatch({ type: 'add' })的時候,會在執(zhí)行前后打印出日志

          before?action?add
          add
          after?action?add

          來簡單實現(xiàn)一下:

          import?{
          ??Actions,
          ??ActionSubscribers,
          ??ActionSubscriber,
          ??ActionArguments
          }?from?"./vuex.type";

          class?Vuex?{
          ??state?=?{};

          ??action?=?{};

          ??_actionSubscribers?=?[];

          ??constructor({?state,?action?})?{
          ????this.state?=?state;
          ????this.action?=?action;
          ????this._actionSubscribers?=?[];
          ??}

          ??dispatch(action)?{
          ????//?action前置監(jiān)聽器
          ????this._actionSubscribers.forEach(sub?=>?sub.before(action,?this.state));

          ????const?{?type,?payload?}?=?action;

          ????//?執(zhí)行action
          ????this.action[type](this.state,?payload).then(()?=>?{
          ??????//?action后置監(jiān)聽器
          ??????this._actionSubscribers.forEach(sub?=>?sub.after(action,?this.state));
          ????});
          ??}

          ??subscribeAction(subscriber)?{
          ????//?把監(jiān)聽者推進數(shù)組
          ????this._actionSubscribers.push(subscriber);
          ??}
          }

          const?store?=?new?Vuex({
          ??state:?{
          ????count:?0
          ??},
          ??action:?{
          ????async?add(state,?payload)?{
          ??????state.count?+=?payload;
          ????}
          ??}
          });

          store.subscribeAction({
          ??before:?(action,?state)?=>?{
          ????console.log(`before?action?${action.type},?before?count?is?${state.count}`);
          ??},
          ??after:?(action,?state)?=>?{
          ????console.log(`after?action?${action.type},??after?count?is?${state.count}`);
          ??}
          });

          store.dispatch({
          ??type:?"add",
          ??payload:?2
          });

          此時控制臺會打印如下內(nèi)容:

          before?action?add,?before?count?is?0
          after?action?add,?after?count?is?2

          輕松實現(xiàn)了日志功能。

          當然 Vuex 在實現(xiàn)插件功能的時候,選擇性的將 type payload 和 state 暴露給外部,而不再提供進一步的修改能力,這也是框架內(nèi)部的一種權衡,當然我們可以對 state 進行直接修改,但是不可避免的會得到 Vuex 內(nèi)部的警告,因為在 Vuex 中,所有 state 的修改都應該通過 mutations 來進行,但是 Vuex 沒有選擇把 commit 也暴露出來,這也約束了插件的能力。

          redux

          想要理解 redux 中的中間件機制,需要先理解一個方法:compose

          function?compose(...funcs:?Function[])?{
          ??return?funcs.reduce((a,?b)?=>?(...args:?any)?=>?a(b(...args)));
          }

          簡單理解的話,就是compose(fn1, fn2, fn3) (...args) = > fn1(fn2(fn3(...args)))
          它是一種高階聚合函數(shù),相當于把 fn3 先執(zhí)行,然后把結果傳給 fn2 再執(zhí)行,再把結果交給 fn1 去執(zhí)行。

          有了這個前置知識,就可以很輕易的實現(xiàn) redux 的中間件機制了。

          雖然 redux 源碼里寫的很少,各種高階函數(shù)各種柯里化,但是抽絲剝繭以后,redux 中間件的機制可以用一句話來解釋:

          把 dispatch 這個方法不斷用高階函數(shù)包裝,最后返回一個強化過后的 dispatch

          以 logMiddleware 為例,這個 middleware 接受原始的 redux dispatch,返回的是

          const?typeLogMiddleware?=?dispatch?=>?{
          ??//?返回的其實還是一個結構相同的dispatch,接受的參數(shù)也相同
          ??//?只是把原始的dispatch包在里面了而已。
          ??return?({?type,?...args?})?=>?{
          ????console.log(`type?is?${type}`);
          ????return?dispatch({?type,?...args?});
          ??};
          };

          有了這個思路,就來實現(xiàn)這個 mini-redux 吧:

          function?compose(...funcs)?{
          ??return?funcs.reduce((a,?b)?=>?(...args)?=>?a(b(...args)));
          }

          function?createStore(reducer,?middlewares)?{
          ??let?currentState;

          ??function?dispatch(action)?{
          ????currentState?=?reducer(currentState,?action);
          ??}

          ??function?getState()?{
          ????return?currentState;
          ??}
          ??//?初始化一個隨意的dispatch,要求外部在type匹配不到的時候返回初始狀態(tài)
          ??//?在這個dispatch后 currentState就有值了。
          ??dispatch({?type:?"INIT"?});

          ??let?enhancedDispatch?=?dispatch;
          ??//?如果第二個參數(shù)傳入了middlewares
          ??if?(middlewares)?{
          ????//?用compose把middlewares包裝成一個函數(shù)
          ????//?讓dis
          ????enhancedDispatch?=?compose(...middlewares)(dispatch);
          ??}

          ??return?{
          ????dispatch:?enhancedDispatch,
          ????getState
          ??};
          }

          接著寫兩個中間件

          //?使用

          const?otherDummyMiddleware?=?dispatch?=>?{
          ??//?返回一個新的dispatch
          ??return?action?=>?{
          ????console.log(`type?in?dummy?is?${type}`);
          ????return?dispatch(action);
          ??};
          };

          //?這個dispatch其實是otherDummyMiddleware執(zhí)行后返回otherDummyDispatch
          const?typeLogMiddleware?=?dispatch?=>?{
          ??//?返回一個新的dispatch
          ??return?({?type,?...args?})?=>?{
          ????console.log(`type?is?${type}`);
          ????return?dispatch({?type,?...args?});
          ??};
          };

          //?中間件從右往左執(zhí)行。
          const?counterStore?=?createStore(counterReducer,?[
          ??typeLogMiddleware,
          ??otherDummyMiddleware
          ]);

          console.log(counterStore.getState().count);
          counterStore.dispatch({?type:?"add",?payload:?2?});
          console.log(counterStore.getState().count);

          //?輸出:
          //?0
          //?type?is?add
          //?type?in?dummy?is?add
          //?2

          koa

          koa 的洋蔥模型想必各位都聽說過,這種靈活的中間件機制也讓 koa 變得非常強大,本文也會實現(xiàn)一個簡單的洋蔥中間件機制。參考(umi-request 的中間件機制)

          99d8b94633f54c10777ff46aa4156e8a.webp洋蔥圈

          對應這張圖來看,洋蔥的每一個圈就是一個中間件,它即可以掌管請求進入,也可以掌管響應返回。

          它和 redux 的中間件機制有點類似,本質上都是高階函數(shù)的嵌套,外層的中間件嵌套著內(nèi)層的中間件,這種機制的好處是可以自己控制中間件的能力(外層的中間件可以影響內(nèi)層的請求和響應階段,內(nèi)層的中間件只能影響外層的響應階段)

          首先我們寫出Koa這個類

          class?Koa?{
          ??constructor()?{
          ????this.middlewares?=?[];
          ??}
          ??use(middleware)?{
          ????this.middlewares.push(middleware);
          ??}
          ??start({?req?})?{
          ????const?composed?=?composeMiddlewares(this.middlewares);
          ????const?ctx?=?{?req,?res:?undefined?};
          ????return?composed(ctx);
          ??}
          }

          這里的 use 就是簡單的把中間件推入中間件隊列中,那核心就是怎樣去把這些中間件組合起來了,下面看composeMiddlewares方法:

          function?composeMiddlewares(middlewares)?{
          ??return?function?wrapMiddlewares(ctx)?{
          ????//?記錄當前運行的middleware的下標
          ????let?index?=?-1;
          ????function?dispatch(i)?{
          ??????//?index向后移動
          ??????index?=?i;

          ??????//?找出數(shù)組中存放的相應的中間件
          ??????const?fn?=?middlewares[i];

          ??????//?最后一個中間件調(diào)用next?也不會報錯
          ??????if?(!fn)?{
          ????????return?Promise.resolve();
          ??????}

          ??????return?Promise.resolve(
          ????????fn(
          ??????????//?繼續(xù)傳遞ctx
          ??????????ctx,
          ??????????// next方法,允許進入下一個中間件。
          ??????????()?=>?dispatch(i?+?1)
          ????????)
          ??????);
          ????}
          ????//?開始運行第一個中間件
          ????return?dispatch(0);
          ??};
          }

          簡單來說 dispatch(n)對應著第 n 個中間件的執(zhí)行,而 dispatch(n)又擁有執(zhí)行 dispatch(n + 1)的權力,

          所以在真正運行的時候,中間件并不是在平級的運行,而是嵌套的高階函數(shù):

          dispatch(0)包含著 dispatch(1),而 dispatch(1)又包含著 dispatch(2) 在這個模式下,我們很容易聯(lián)想到try catch的機制,它可以 catch 住函數(shù)以及函數(shù)內(nèi)部繼續(xù)調(diào)用的函數(shù)的所有error。

          那么我們的第一個中間件就可以做一個錯誤處理中間件:

          //?最外層?管控全局錯誤
          app.use(async?(ctx,?next)?=>?{
          ??try?{
          ????//?這里的next包含了第二層以及第三層的運行
          ????await?next();
          ??}?catch?(error)?{
          ????console.log(`[koa?error]:?${error.message}`);
          ??}
          });

          在這個錯誤處理中間件中,我們把 next 包裹在 try catch 中運行,調(diào)用了 next 后會進入第二層的中間件:

          //?第二層?日志中間件
          app.use(async?(ctx,?next)?=>?{
          ??const?{?req?}?=?ctx;
          ??console.log(`req?is?${JSON.stringify(req)}`);
          ??await?next();
          ??//?next過后已經(jīng)能拿到第三層寫進ctx的數(shù)據(jù)了
          ??console.log(`res?is?${JSON.stringify(ctx.res)}`);
          });

          在第二層中間件的 next 調(diào)用后,進入第三層,業(yè)務邏輯處理中間件

          //?第三層?核心服務中間件
          //?在真實場景中?這一層一般用來構造真正需要返回的數(shù)據(jù)?寫入ctx中
          app.use(async?(ctx,?next)?=>?{
          ??const?{?req?}?=?ctx;
          ??console.log(`calculating?the?res?of?${req}...`);
          ??const?res?=?{
          ????code:?200,
          ????result:?`req?${req}?success`
          ??};
          ??//?寫入ctx
          ??ctx.res?=?res;
          ??await?next();
          });

          在這一層把 res 寫入 ctx 后,函數(shù)出棧,又會回到第二層中間件的await next()后面

          console.log(`req?is?${JSON.stringify(req)}`);
          await?next();
          //?<-?回到這里
          console.log(`res?is?${JSON.stringify(ctx.res)}`);

          這時候日志中間件就可以拿到ctx.res的值了。

          想要測試錯誤處理中間件 就在最后加入這個中間件

          //?用來測試全局錯誤中間件
          //?注釋掉這一個中間件?服務才能正常響應
          app.use(async?(ctx,?next)?=>?{
          ??throw?new?Error("oops!?error!");
          });

          最后要調(diào)用啟動函數(shù):

          app.start({?req:?"ssh"?});

          控制臺打印出結果:

          req?is?"ssh"
          calculating?the?res?of?ssh...
          res?is?{"code":200,"result":"req?ssh?success"}

          總結

          1. axios 把用戶注冊的每個攔截器構造成一個 promise.then 所接受的參數(shù),在運行時把所有的攔截器按照一個 promise 鏈的形式以此執(zhí)行。
          • 在發(fā)送到服務端之前,config 已經(jīng)是請求攔截器處理過后的結果
          • 服務器響應結果后,response 會經(jīng)過響應攔截器,最后用戶拿到的就是處理過后的結果了。
          1. vuex的實現(xiàn)最為簡單,就是提供了兩個回調(diào)函數(shù),vuex 內(nèi)部在合適的時機去調(diào)用(我個人感覺大部分的庫提供這樣的機制也足夠了)。
          2. redux的源碼里寫的最復雜最繞,它的中間件機制本質上就是用高階函數(shù)不斷的把 dispatch 包裝再包裝,形成套娃。本文實現(xiàn)的已經(jīng)是精簡了 n 倍以后的結果了,不過復雜的實現(xiàn)也是為了很多權衡和考量,Dan 對于閉包和高階函數(shù)的運用已經(jīng)爐火純青了,只是外人去看源碼有點頭禿...
          3. koa的洋蔥模型實現(xiàn)的很精妙,和 redux 有相似之處,但是在源碼理解和使用上個人感覺更優(yōu)于 redux 的中間件。

          中間件機制其實是非框架強相關的,請求庫一樣可以加入 koa 的洋蔥中間件機制(如 umi-request),不同的框架可能適合不同的中間件機制,這還是取決于你編寫的框架想要解決什么問題,想給用戶什么樣的自由度。

          希望看了這篇文章的你,能對于前端庫中的中間件機制有進一步的了解,進而為你自己的前端庫加入合適的中間件能力。

          本文所寫的代碼都整理在這個倉庫里了:
          https://github.com/sl1673495/tiny-middlewares

          代碼是使用 ts 編寫的,js 版本的代碼在 js 文件夾內(nèi),各位可以按自己的需求來看。




          推薦閱讀




          我的公眾號能帶來什么價值?(文末有送書規(guī)則,一定要看)

          每個前端工程師都應該了解的圖片知識(長文建議收藏)

          為什么現(xiàn)在面試總是面試造火箭?


          瀏覽 51
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  干我操我| 久久精品96无码内射 | 激情在线无码色综合播放视频 | 成人网站视频大香蕉 | 影音先锋成人网址 |