前端如何正确使用中间件?
一 先簡(jiǎn)單講講中間件
const compose = (middlewares) => {const reduce = (pre, cur) => {if (pre) {return (ctx) => cur(ctx, pre)} else {return (ctx) => cur(ctx, () => ctx)}}return [...middlewares].reverse().reduce(reduce, null); }這是一段非常簡(jiǎn)潔的中間件代碼,通過(guò)傳入的類(lèi)似這樣的函數(shù)的列表:
const middlware = async (ctx, next) => {/*** do something to modify ctx*/if (/* let next run */true) {await next(ctx)}/*** do something to modify ctx*/ }得到一個(gè)新的函數(shù),這個(gè)函數(shù)的執(zhí)行,會(huì)讓這些中間件逐個(gè)處理并且每個(gè)中間件可以決定:
- 在下個(gè)中間件執(zhí)行之前做些什么?
- 是否讓下個(gè)中間件執(zhí)行?
- 在下個(gè)中間件執(zhí)行之后做些什么?
現(xiàn)在的中間件都是使用的洋蔥模型,洋蔥模型的大致示意圖是這樣的:
按照這張圖,中間件的執(zhí)行順序是:
middleware1 -> middleware2 -> middleware3 -> middleware2 -> middleware1
處理順序是先從外到內(nèi),再?gòu)膬?nèi)到外,這就是中間件的洋蔥模型。
在中間件的應(yīng)用上,開(kāi)發(fā)者可以將統(tǒng)一邏輯做成一個(gè)中間件,這樣就能在其他地方復(fù)用這個(gè)邏輯。我覺(jué)得這其實(shí)是中間件這種模式的初心吧,好,那我們先把這個(gè)初心放一放。
但實(shí)際上這個(gè)模式就是一個(gè)空殼,通過(guò)不同的中間件,就可以實(shí)現(xiàn)各種自定義的邏輯。比如:
const handler = compose([(ctx, next) => {if (ctx.question === 'hello') {ctx.answer = 'hello';return}if (next) [next(ctx)] }, (ctx, next) => {if (/age/.test(ctx.question)) {ctx.answer = 'i am 5 yours old';return}if (next) [next(ctx)] }]) const ctx = { question: 'hello' }; handler(ctx) console.log(ctx.answer) // log hello ctx.question = 'how about your age?' handler(ctx) console.log(ctx.answer) // log i am 5 yours old這樣看起來(lái)我們甚至可以去實(shí)現(xiàn)一個(gè)機(jī)器人,把中間件這么拿來(lái)用,相當(dāng)于是把中間件作為一個(gè) if 語(yǔ)句展開(kāi)了,通過(guò)不同的中間件對(duì)ctx的劫持來(lái)分離邏輯,看起來(lái)好像也不錯(cuò)?
得益于中間件的靈活性,每個(gè)中間件可以實(shí)現(xiàn):1)實(shí)現(xiàn)獨(dú)立的某個(gè)邏輯;2)控制后續(xù)的流程是否執(zhí)行。
二 聊聊幾個(gè)栗子
今年有參與做個(gè)小程序的Bridge,先簡(jiǎn)單的介紹一下Bridge的功能。
- 從支付寶小程序的視角來(lái)抹平其他小程序的JSAPI。
- Bridge擁有擴(kuò)展能力,能夠擴(kuò)展JSAPI。
看到“擴(kuò)展能力”,熟練的同學(xué)應(yīng)該就知道我可以切入正題了。
Bridge現(xiàn)在的設(shè)計(jì)采用插件的形式來(lái)注入一系列API,每個(gè)插件都有插件名、API名、中間件三個(gè)屬性,注入Bridge后,Bridge會(huì)將相同API名的插件整合在一起,讓這個(gè)API的實(shí)現(xiàn)指向這些插件帶有的中間件的 compose ,用這種方式來(lái)實(shí)現(xiàn)自定義API。
這種方式其實(shí)看起來(lái)是非常美妙的,因?yàn)樗械腁PI都可以通過(guò)插件的形式注入到Bridge中,可以很靈活地?cái)U(kuò)展API。
眾所周知,有得必有失。這種模式其實(shí)有自己的缺點(diǎn),具體的缺點(diǎn)我們可以從“面向開(kāi)發(fā)者”和“面向使用者”兩方面來(lái)整理,面向開(kāi)發(fā)者指的是面向?qū)懖寮?#xff08;也就是寫(xiě)中間件)的開(kāi)發(fā)者,面向使用者(用戶(hù))指的是最終使用Bridge的開(kāi)發(fā)者。
1 面向開(kāi)發(fā)者
API的不確定性
多個(gè)中間件注冊(cè)在同一個(gè)API上面,開(kāi)發(fā)者自己的API是否能夠運(yùn)行正常有的時(shí)候是依賴(lài)上下文的,而零散的中間件被載入Bridge,對(duì)于上下文的修改是未知的,因此會(huì)對(duì)API的執(zhí)行帶來(lái)很多不確定性。
從洋蔥模型的圖上面,我們可以發(fā)現(xiàn),內(nèi)層往往會(huì)受外部的影響,當(dāng)然在回流的時(shí)候,外部中間件也會(huì)受內(nèi)部中間件的影響,在開(kāi)發(fā)中間件的時(shí)候,我們需要考慮自己的依賴(lài),在已知依賴(lài)沒(méi)有問(wèn)題的情況下去做開(kāi)發(fā),才會(huì)比較穩(wěn)妥,但是當(dāng)前Bridge這種散裝載入Plugin的方式,讓依賴(lài)關(guān)系沒(méi)有辦法穩(wěn)定的描述。
API的維護(hù)成本高
由于有多個(gè)插件注冊(cè)到單個(gè)API上,維護(hù)某個(gè)API的情況下就會(huì)有比較高的成本,就有點(diǎn)像是現(xiàn)在服務(wù)端排查問(wèn)題的情況了,多個(gè)插件的情況下最差情況可能要逐個(gè)開(kāi)發(fā)者去做排查,最終才能分鍋,雖然實(shí)際情況可能沒(méi)有這么糟糕,但還是要考慮一下最差的情況。
那么為什么服務(wù)端這種架構(gòu)是合理的呢,因?yàn)榉?wù)端的微服務(wù)架構(gòu)確實(shí)能夠?qū)⒍鄠€(gè)業(yè)務(wù)邏輯拆分來(lái)解耦比較復(fù)雜的邏輯,但是Bridge這里只是想要實(shí)現(xiàn)某個(gè)API的實(shí)現(xiàn),也很明顯的發(fā)現(xiàn)實(shí)際在使用過(guò)程中,基本都采用了單插件的注冊(cè)方式。所以感覺(jué)用中間件來(lái)實(shí)現(xiàn)某個(gè)API,有點(diǎn)過(guò)渡設(shè)計(jì)了,反而造成了維護(hù)成本的提高。
2 面向使用者
面向使用者其實(shí)要分為兩種不同的場(chǎng)景:直接使用插件和通過(guò)preset來(lái)使用插件的集成。
3 直接使用插件
這種模式下,使用者要自己去引用插件,通過(guò)引用一系列插件來(lái)獲得一個(gè)可以正常使用的API,可是使用者往往期望的是能夠開(kāi)箱即用,也就是說(shuō)拿到這個(gè)Bridge,看一下文檔,就能夠調(diào)用某個(gè)API了,如今需要Bridge的使用者通過(guò)自己注冊(cè)一個(gè)Plugin這樣的東西來(lái)獲得一個(gè)可用的API,顯然是不合理的,不合理的地方主要體現(xiàn)在:
API難理解
Bridge使用者原本只需要理解一下Bridge的文檔就能夠輕松使用API,現(xiàn)在需要理解plugin的運(yùn)作機(jī)制以及如果有若干個(gè)插件的話(huà),還要理解插件單獨(dú)的運(yùn)作和相互運(yùn)作的實(shí)現(xiàn)。這些都很難讓一個(gè)Bridge使用者接受,對(duì)于業(yè)務(wù)開(kāi)發(fā)來(lái)講,成本變高了。
問(wèn)題排查難度上升
這點(diǎn)和之前提到的使用中間件這種方式會(huì)造成API的邏輯不連貫的情況是類(lèi)似的,Bridge在使用API的時(shí)候如果發(fā)現(xiàn)有問(wèn)題,那么排查問(wèn)題的時(shí)候就會(huì)因?yàn)橛卸鄠€(gè)Plugin實(shí)現(xiàn)而增加難度,總的來(lái)說(shuō)他還是需要簡(jiǎn)單的去理解每個(gè)插件基本實(shí)現(xiàn)和插件間的運(yùn)作機(jī)制,對(duì)于業(yè)務(wù)開(kāi)發(fā)來(lái)講,成本較高。
4 通過(guò)Preset來(lái)使用插件的集成
由于上述Bridge使用者直接使用Bridge的問(wèn)題,其實(shí)通過(guò)preset的封裝可以解決一部分的痛點(diǎn),而B(niǎo)ridge的preset的概念就是,通過(guò)編寫(xiě)一個(gè)preset,這個(gè)preset去維護(hù)一個(gè)API和多個(gè)插件的關(guān)系,然后給到用戶(hù)的是一個(gè)集成好的Bridge,上述的兩個(gè)問(wèn)題都可以被解決。
這個(gè)模式看起來(lái)形式上就是之前的Bridge用戶(hù)選了一個(gè)“最懂插件的人”來(lái)做他們的替身,做了之前的那個(gè)User的角色,讓這個(gè)人來(lái)理解所有的Plugin,并維護(hù)這些API,這個(gè)"最懂"趨向極限,基本就等于開(kāi)發(fā)Plugin的人了,那么饒了這么大一圈,做的這么靈活,最后維護(hù)插件的人是同一個(gè)人,也是這個(gè)人對(duì)外輸出API,那么這個(gè)東西真的有復(fù)雜到要這么拆分么。就我個(gè)人來(lái)講覺(jué)得還是直接簡(jiǎn)單明了的的實(shí)現(xiàn)一個(gè)API來(lái)的方便。那是中間件這種模式辣雞嗎?
5 抬走,我們來(lái)看下一個(gè)
除了Bridge,老生常談的還有類(lèi)似Fetch這樣的基礎(chǔ)庫(kù),Fetch是另一波同學(xué)做的了,但是我也是小撇了幾眼代碼,發(fā)現(xiàn)居然也用了中間件來(lái)做,正好可以看看他們?cè)谠O(shè)計(jì)API的時(shí)候使用中間件的合理性。先說(shuō)說(shuō)Fetch為啥走了這條路吧,看看訴求:
因?yàn)閷?shí)在是有太多種不同的請(qǐng)求類(lèi)型了,因此想實(shí)現(xiàn)在相同的入?yún)⑾?#xff0c;通過(guò)adaptor參數(shù)來(lái)區(qū)分最終走怎樣的請(qǐng)求邏輯。
因此Fetch在設(shè)計(jì)的時(shí)候,是這么使用中間件的:
fetch.use(commonMiddleware) fetch.use('adaptor-xxx', [middleware]) // 比如adaptor-json fetch({ ...requestConfig, adaotpr: 'adaptor-xxx' })Fetch的中間件使用會(huì)相對(duì)合理一點(diǎn),通過(guò)利用中間件的特性,對(duì)外輸出了相同的出入?yún)?#xff0c;再借助不同的中間件對(duì)請(qǐng)求的過(guò)程做流式處理。
但實(shí)際的使用過(guò)程中,也要很多同學(xué)反饋,有類(lèi)似Bridge的使用問(wèn)題。
6 調(diào)用過(guò)程排查困難
和Bridge類(lèi)似,業(yè)務(wù)在使用過(guò)程中如果遇到問(wèn)題,排查難度會(huì)比較高,首先業(yè)務(wù)開(kāi)發(fā)同學(xué)的理解能力就很難了,因?yàn)橐瑫r(shí)理解這套中間件+每個(gè)中間件的實(shí)現(xiàn)原理,而adaptor開(kāi)發(fā)同學(xué)也比較難排查問(wèn)題,首先他需要知道業(yè)務(wù)開(kāi)發(fā)同學(xué)本地是如何使用這些適配器的,在知道了之后再零散的逐個(gè)插件去排查,相比于直接看某個(gè)類(lèi)型的請(qǐng)求的實(shí)現(xiàn),難度會(huì)較高。
三 引出觀點(diǎn)
那么回頭看看這兩個(gè)Bridge和Fetch究竟有必要使用中間件么,有沒(méi)有更好的選擇。
先考慮假如我們不使用中間件來(lái)做,是不是現(xiàn)在的困境都會(huì)不存在了,就比如:
fetch.rpc = () => {} fetch.mtop = () => {} fetch.json = () => {}這樣實(shí)現(xiàn)不同類(lèi)型的請(qǐng)求,每個(gè)請(qǐng)求的實(shí)現(xiàn)就會(huì)比較直觀的收斂在具體的函數(shù)中,隨之帶來(lái)的應(yīng)該有如下的問(wèn)題:
不同請(qǐng)求實(shí)現(xiàn)之間的共享邏輯會(huì)不那么直觀,說(shuō)白了就是將中間件前置后置那堆東西拿放到各自的實(shí)現(xiàn)中,哪怕是抽了公共函數(shù)然后再放到各自函數(shù)的實(shí)現(xiàn)中,這些共享邏輯都不直觀,而中間件那種共享邏輯的處理,可以減少一定的維護(hù)成本。
那么會(huì)杠的同學(xué)就要開(kāi)始問(wèn)了:剛才你說(shuō)多個(gè)中間件會(huì)加大維護(hù)的成本,現(xiàn)在又說(shuō)共享的邏輯做成中間件能夠減少維護(hù)成本,你這前后矛盾啊!
這波流程Q的不錯(cuò)。
那終于,要在這里拋一個(gè)觀點(diǎn):
中間件的這種模式,應(yīng)該作為某個(gè)函數(shù)的裝飾者模式來(lái)使用。
那么既然提到裝飾者模式,我們可以引用一本《維基百科》中的描述:
the decorator pattern is a design pattern) that allows behavior to be added to an individual object), dynamically, without affecting the behavior of other objects from the same class).
裝飾者模式是一個(gè)可以在不影響其他相同類(lèi)的對(duì)象的情況下,動(dòng)態(tài)修改某個(gè)對(duì)象行為的設(shè)計(jì)模式。
其實(shí)這段描述的體感不是很強(qiáng),因?yàn)槠鋵?shí)中間件本身已經(jīng)不是一個(gè)對(duì)象了,而維基百科中的設(shè)計(jì)模式針對(duì)面向?qū)ο蟮恼Z(yǔ)言做了描述。
為了更有體感一點(diǎn),附上一張《Head First設(shè)計(jì)模式》中的一圖:
可以發(fā)現(xiàn)幾點(diǎn):
- 裝飾器和我們需要擴(kuò)展的Class都是實(shí)現(xiàn)了同一個(gè)接口。
- 裝飾器是通過(guò)接收一個(gè)Component對(duì)象來(lái)運(yùn)作的。
看到上面這兩點(diǎn)就會(huì)發(fā)現(xiàn)其實(shí)裝飾器模式和中間件的概念是大致相同的,只不過(guò)在Javascript中,通過(guò)一個(gè)compose的函數(shù)將幾個(gè)毫不相干的函數(shù)串了起來(lái),但最終的模式是和這個(gè)裝飾者模式基本一致的。
另外《Head First設(shè)計(jì)模式》中還有一張圖:
這是他舉的咖啡計(jì)算價(jià)格的例子,看到這張圖不是特別眼熟么,這和我們最開(kāi)始說(shuō)的洋蔥模型非常相近,這也再一次證明了其實(shí)我們用的“中間件設(shè)計(jì)模式”其實(shí)就是“裝飾者模式”。
那么聊了一下裝飾者模式,其實(shí)是為了說(shuō)明我之前闡述的“中間件的這種模式,應(yīng)該作為某個(gè)函數(shù)的裝飾者模式來(lái)使用”的觀點(diǎn),因?yàn)檠b飾器本身是為了解決繼承帶來(lái)的類(lèi)的數(shù)量爆炸的問(wèn)題的,而使用場(chǎng)景正如同它的名字一般,是有裝飾者和被裝飾者的區(qū)分的,盡管裝飾者最終也能成為一個(gè)被裝飾者,就如同例子中,計(jì)算咖啡的價(jià)格,裝飾者可以根據(jù)加奶或者加奶泡等等來(lái)計(jì)算收費(fèi),但是其實(shí)著這個(gè)場(chǎng)景下,去做對(duì)加奶的裝飾,就沒(méi)什么意義了,也很難懂。反推我覺(jué)得中間件這種模式,亦是如此。
四 回應(yīng)
通過(guò)如上的分析,我們得知,我們?cè)谶\(yùn)用中間件的時(shí)候,起碼要有一個(gè)主要的函數(shù),而其他的中間件,都是用于裝飾使用。
就比如我們?cè)谑褂肒oa做Node開(kāi)發(fā)的時(shí)候,常常把業(yè)務(wù)邏輯放到某個(gè)中間件中,其他的都是一些攔截或者預(yù)處理的中間件,在egg中主要的業(yè)務(wù)邏輯被做成了一個(gè)controller,當(dāng)然他最后肯定還是一個(gè)中間件,這是一種API的美化,非常科學(xué)。
再比如我們?cè)谑褂胷edux的時(shí)候,中間件往往都是做一些簡(jiǎn)單的預(yù)處理或者action監(jiān)聽(tīng)等等,當(dāng)然也有另類(lèi)的做法,比如redux-saga整個(gè)將邏輯接管掉的,這塊另說(shuō),我們這次先只聊常規(guī)用法。
那回過(guò)頭來(lái),想比如Bridge這類(lèi)如何做修改呢?
我覺(jué)得Bridge底層使用中間件來(lái)做API的處理流完全沒(méi)有問(wèn)題,但造成現(xiàn)在這樣的問(wèn)題主要是他的API,就如同egg做了koa的API的美化一般,Bridge也應(yīng)該在API的設(shè)計(jì)上美化一下,限制二次開(kāi)發(fā)者的腦洞,API不是越自由就越好,有句話(huà)說(shuō)的好“你在召喚多強(qiáng)大的自由,就是在召喚多強(qiáng)大的奴役”。
那么我們應(yīng)該如何限制API呢?
依照之前闡述過(guò)的說(shuō)法“中間件的這種模式,應(yīng)該作為某個(gè)函數(shù)的裝飾者模式來(lái)使用”,因此,首先要有一個(gè)顯式申明的主函數(shù),這塊我們的API應(yīng)該如下設(shè)計(jì):
bridge.API('APINAME', handler) // 或者更加直接的 bridge.APINAME = handler這樣一來(lái),開(kāi)發(fā)者在查找API實(shí)現(xiàn)的時(shí)候,就能夠比較明確的找到這塊的實(shí)現(xiàn),而最底層Bridge還是會(huì)吧這個(gè)handler丟到一個(gè)中間件中去做處理,這樣就能做到對(duì)這個(gè)handler的裝飾。
在這個(gè)的基礎(chǔ)上,再設(shè)計(jì)一個(gè)能夠支持中間件的API:
bridge.use(middleware) // 對(duì)所有的API生效 bridge.use('APINAME', middleware) // 對(duì)某個(gè)API生效再回顧一下之前列出來(lái)的問(wèn)題:
API的不確定性
API的實(shí)現(xiàn)都會(huì)放到handler中,且僅有這個(gè)handler會(huì)做主要邏輯處理,開(kāi)發(fā)者明確的知道這里寫(xiě)的就是主邏輯。
API的維護(hù)成本高
API的主要實(shí)現(xiàn)就在handler中,只需要維護(hù)handler就行,有特殊的問(wèn)題,再去看使用的中間件。
API難理解
用戶(hù)明確的知道只需要理解handler的實(shí)現(xiàn)就行,中間件的邏輯大部分是用于公共使用,只要統(tǒng)一理解就行。
到這里,會(huì)杠的同學(xué)還是會(huì)問(wèn),其實(shí)你這好像問(wèn)題也沒(méi)有完全解決,只要開(kāi)發(fā)者想搞你,還是會(huì)出現(xiàn)之前的問(wèn)題,比如就會(huì)有騷的人把邏輯寫(xiě)到中間件里面,不寫(xiě)到handler里面,你這種設(shè)計(jì)不還是一樣。
這說(shuō)的一點(diǎn)都沒(méi)錯(cuò),因?yàn)樵O(shè)計(jì)這個(gè)API難免的就是要開(kāi)放給開(kāi)發(fā)者這樣的能力,也就是:1)自定義API;2)對(duì)若干API做一些個(gè)性化的統(tǒng)一邏輯。API的設(shè)計(jì)者能夠做到的就是在API上傳達(dá)給開(kāi)發(fā)者一種規(guī)范,就比如 bridge.plugin() 這種開(kāi)放性的API,就沒(méi)有 bridge.API() 這種好,因?yàn)楹笳吆苊鞔_的讓開(kāi)發(fā)者申明一個(gè)API,而前者不明確,前者讓開(kāi)發(fā)者覺(jué)得中間件就是API的實(shí)現(xiàn)。
五 結(jié)語(yǔ)
本篇我們從中間件聊到中間件的使用實(shí)例,再聊到了裝飾器模式,最后聊到了使用中間件的API的設(shè)計(jì)。在日常API設(shè)計(jì)中,我不僅會(huì)面對(duì)底層設(shè)計(jì)的選型,還會(huì)面對(duì)對(duì)外開(kāi)放API的設(shè)計(jì),兩者都同樣重要。不過(guò)本篇僅代表個(gè)人觀點(diǎn),歡迎在評(píng)論區(qū)指教、討論。
原文鏈接:https://developer.aliyun.com/article/770661?
版權(quán)聲明:本文內(nèi)容由阿里云實(shí)名注冊(cè)用戶(hù)自發(fā)貢獻(xiàn),版權(quán)歸原作者所有,阿里云開(kāi)發(fā)者社區(qū)不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。具體規(guī)則請(qǐng)查看《阿里云開(kāi)發(fā)者社區(qū)用戶(hù)服務(wù)協(xié)議》和《阿里云開(kāi)發(fā)者社區(qū)知識(shí)產(chǎn)權(quán)保護(hù)指引》。如果您發(fā)現(xiàn)本社區(qū)中有涉嫌抄襲的內(nèi)容,填寫(xiě)侵權(quán)投訴表單進(jìn)行舉報(bào),一經(jīng)查實(shí),本社區(qū)將立刻刪除涉嫌侵權(quán)內(nèi)容。總結(jié)
以上是生活随笔為你收集整理的前端如何正确使用中间件?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 为什么下一个十年的主战场在Serverl
- 下一篇: AI和物联网在零售环境中的长期应用