簡介: 中間件能夠算是一種前端中經常使用的」設計模式「了,有的時候甚至能夠說,整個應用的架構都是使用中間件爲基礎搭建的。那麼中間件有哪些利弊?什麼纔是中間件正確的使用姿式?本文將分享做者在實際使用中的一些想法,歡迎同窗們共同討論。前端
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); }
這是一段很是簡潔的中間件代碼,經過傳入的相似這樣的函數的列表:json
const middlware = async (ctx, next) => { /** * do something to modify ctx */ if (/* let next run */true) { await next(ctx) } /** * do something to modify ctx */ }
獲得一個新的函數,這個函數的執行,會讓這些中間件逐個處理而且每一箇中間件能夠決定:redux
如今的中間件都是使用的洋蔥模型,洋蔥模型的大體示意圖是這樣的:小程序
按照這張圖,中間件的執行順序是:設計模式
middleware1 -> middleware2 -> middleware3 -> middleware2 -> middleware1架構
處理順序是先從外到內,再從內到外,這就是中間件的洋蔥模型。koa
在中間件的應用上,開發者能夠將統一邏輯作成一箇中間件,這樣就能在其餘地方複用這個邏輯。我以爲這實際上是中間件這種模式的初心吧,好,那咱們先把這個初心放一放。async
但實際上這個模式就是一個空殼,經過不一樣的中間件,就能夠實現各類自定義的邏輯。好比:函數
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
這樣看起來咱們甚至能夠去實現一個機器人,把中間件這麼拿來用,至關因而把中間件做爲一個 if 語句展開了,經過不一樣的中間件對ctx的劫持來分離邏輯,看起來好像也不錯?微服務
得益於中間件的靈活性,每一箇中間件能夠實現:1)實現獨立的某個邏輯;2)控制後續的流程是否執行。
今年有參與作個小程序的Bridge,先簡單的介紹一下Bridge的功能。
看到「擴展能力」,熟練的同窗應該就知道我能夠切入正題了。
Bridge如今的設計採用插件的形式來注入一系列API,每一個插件都有插件名、API名、中間件三個屬性,注入Bridge後,Bridge會將相同API名的插件整合在一塊兒,讓這個API的實現指向這些插件帶有的中間件的 compose ,用這種方式來實現自定義API。
這種方式其實看起來是很是美妙的,由於全部的API均可以經過插件的形式注入到Bridge中,能夠很靈活地擴展API。
衆所周知,有得必有失。這種模式其實有本身的缺點,具體的缺點咱們能夠從「面向開發者」和「面向使用者」兩方面來整理,面向開發者指的是面向寫插件(也就是寫中間件)的開發者,面向使用者(用戶)指的是最終使用Bridge的開發者。
API的不肯定性
多箇中間件註冊在同一個API上面,開發者本身的API是否可以運行正常有的時候是依賴上下文的,而零散的中間件被載入Bridge,對於上下文的修改是未知的,所以會對API的執行帶來不少不肯定性。
從洋蔥模型的圖上面,咱們能夠發現,內層每每會受外部的影響,固然在迴流的時候,外部中間件也會受內部中間件的影響,在開發中間件的時候,咱們須要考慮本身的依賴,在已知依賴沒有問題的狀況下去作開發,纔會比較穩妥,可是當前Bridge這種散裝載入Plugin的方式,讓依賴關係沒有辦法穩定的描述。
API的維護成本高
因爲有多個插件註冊到單個API上,維護某個API的狀況下就會有比較高的成本,就有點像是如今服務端排查問題的狀況了,多個插件的狀況下最差狀況可能要逐個開發者去作排查,最終才能分鍋,雖然實際狀況可能沒有這麼糟糕,但仍是要考慮一下最差的狀況。
那麼爲何服務端這種架構是合理的呢,由於服務端的微服務架構確實可以將多個業務邏輯拆分來解耦比較複雜的邏輯,可是Bridge這裏只是想要實現某個API的實現,也很明顯的發現實際在使用過程當中,基本都採用了單插件的註冊方式。因此感受用中間件來實現某個API,有點過渡設計了,反而形成了維護成本的提升。
面向使用者其實要分爲兩種不一樣的場景:直接使用插件和經過preset來使用插件的集成。
這種模式下,使用者要本身去引用插件,經過引用一系列插件來得到一個能夠正常使用的API,但是使用者每每指望的是可以開箱即用,也就是說拿到這個Bridge,看一下文檔,就可以調用某個API了,現在須要Bridge的使用者經過本身註冊一個Plugin這樣的東西來得到一個可用的API,顯然是不合理的,不合理的地方主要體如今:
API難理解
Bridge使用者本來只須要理解一下Bridge的文檔就可以輕鬆使用API,如今須要理解plugin的運做機制以及若是有若干個插件的話,還要理解插件單獨的運做和相互運做的實現。這些都很難讓一個Bridge使用者接受,對於業務開發來說,成本變高了。
問題排查難度上升
這點和以前提到的使用中間件這種方式會形成API的邏輯不連貫的狀況是相似的,Bridge在使用API的時候若是發現有問題,那麼排查問題的時候就會由於有多個Plugin實現而增長難度,總的來講他仍是須要簡單的去理解每一個插件基本實現和插件間的運做機制,對於業務開發來說,成本較高。
因爲上述Bridge使用者直接使用Bridge的問題,其實經過preset的封裝能夠解決一部分的痛點,而Bridge的preset的概念就是,經過編寫一個preset,這個preset去維護一個API和多個插件的關係,而後給到用戶的是一個集成好的Bridge,上述的兩個問題均可以被解決。
這個模式看起來形式上就是以前的Bridge用戶選了一個「最懂插件的人」來作他們的替身,作了以前的那個User的角色,讓這我的來理解全部的Plugin,並維護這些API,這個"最懂"趨向極限,基本就等於開發Plugin的人了,那麼饒了這麼大一圈,作的這麼靈活,最後維護插件的人是同一我的,也是這我的對外輸出API,那麼這個東西真的有複雜到要這麼拆分麼。就我我的來說以爲仍是直接簡單明瞭的的實現一個API來的方便。那是中間件這種模式辣雞嗎?
除了Bridge,老生常談的還有相似Fetch這樣的基礎庫,Fetch是另外一波同窗作的了,可是我也是小撇了幾眼代碼,發現竟然也用了中間件來作,正好能夠看看他們在設計API的時候使用中間件的合理性。先說說Fetch爲啥走了這條路吧,看看訴求:
由於實在是有太多種不一樣的請求類型了,所以想實如今相同的入參下,經過adaptor參數來區分最終走怎樣的請求邏輯。
所以Fetch在設計的時候,是這麼使用中間件的:
fetch.use(commonMiddleware) fetch.use('adaptor-xxx', [middleware]) // 好比adaptor-json fetch({ ...requestConfig, adaotpr: 'adaptor-xxx' })
Fetch的中間件使用會相對合理一點,經過利用中間件的特性,對外輸出了相同的出入參,再借助不一樣的中間件對請求的過程作流式處理。
但實際的使用過程當中,也要不少同窗反饋,有相似Bridge的使用問題。
和Bridge相似,業務在使用過程當中若是遇到問題,排查難度會比較高,首先業務開發同窗的理解能力就很難了,由於要同時理解這套中間件+每一箇中間件的實現原理,而adaptor開發同窗也比較難排查問題,首先他須要知道業務開發同窗本地是如何使用這些適配器的,在知道了以後再零散的逐個插件去排查,相比於直接看某個類型的請求的實現,難度會較高。
那麼回頭看看這兩個Bridge和Fetch究竟有必要使用中間件麼,有沒有更好的選擇。
先考慮假如咱們不使用中間件來作,是否是如今的困境都會不存在了,就好比:
fetch.rpc = () => {} fetch.mtop = () => {} fetch.json = () => {}
這樣實現不一樣類型的請求,每一個請求的實現就會比較直觀的收斂在具體的函數中,隨之帶來的應該有以下的問題:
不一樣請求實現之間的共享邏輯會不那麼直觀,說白了就是將中間件前置後置那堆東西拿放到各自的實現中,哪怕是抽了公共函數而後再放到各自函數的實現中,這些共享邏輯都不直觀,而中間件那種共享邏輯的處理,能夠減小必定的維護成本。
那麼會槓的同窗就要開始問了:剛纔你說多箇中間件會加大維護的成本,如今又說共享的邏輯作成中間件可以減小維護成本,你這先後矛盾啊!
這波流程Q的不錯。
那終於,要在這裏拋一個觀點:
中間件的這種模式,應該做爲某個函數的裝飾者模式來使用。
那麼既然提到裝飾者模式,咱們能夠引用一本《維基百科》中的描述:
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).
裝飾者模式是一個能夠在不影響其餘相同類的對象的狀況下,動態修改某個對象行爲的設計模式。
其實這段描述的體感不是很強,由於其實中間件自己已經不是一個對象了,而維基百科中的設計模式針對面向對象的語言作了描述。
爲了更有體感一點,附上一張《Head First設計模式》中的一圖:
能夠發現幾點:
看到上面這兩點就會發現其實裝飾器模式和中間件的概念是大體相同的,只不過在Javascript中,經過一個compose的函數將幾個絕不相干的函數串了起來,但最終的模式是和這個裝飾者模式基本一致的。
另外《Head First設計模式》中還有一張圖:
這是他舉的咖啡計算價格的例子,看到這張圖不是特別眼熟麼,這和咱們最開始說的洋蔥模型很是相近,這也再一次證實了其實咱們用的「中間件設計模式」其實就是「裝飾者模式」。
那麼聊了一下裝飾者模式,實際上是爲了說明我以前闡述的「中間件的這種模式,應該做爲某個函數的裝飾者模式來使用」的觀點,由於裝飾器自己是爲了解決繼承帶來的類的數量爆炸的問題的,而使用場景正如同它的名字通常,是有裝飾者和被裝飾者的區分的,儘管裝飾者最終也能成爲一個被裝飾者,就如同例子中,計算咖啡的價格,裝飾者能夠根據加奶或者加奶泡等等來計算收費,可是其實着這個場景下,去作對加奶的裝飾,就沒什麼意義了,也很難懂。反推我以爲中間件這種模式,亦是如此。
經過如上的分析,咱們得知,咱們在運用中間件的時候,起碼要有一個主要的函數,而其餘的中間件,都是用於裝飾使用。
就好比咱們在使用Koa作Node開發的時候,經常把業務邏輯放到某個中間件中,其餘的都是一些攔截或者預處理的中間件,在egg中主要的業務邏輯被作成了一個controller,固然他最後確定仍是一箇中間件,這是一種API的美化,很是科學。
再好比咱們在使用redux的時候,中間件每每都是作一些簡單的預處理或者action監聽等等,固然也有另類的作法,好比redux-saga整個將邏輯接管掉的,這塊另說,咱們此次先只聊常規用法。
那回過頭來,想好比Bridge這類如何作修改呢?
我以爲Bridge底層使用中間件來作API的處理流徹底沒有問題,但形成如今這樣的問題主要是他的API,就如同egg作了koa的API的美化通常,Bridge也應該在API的設計上美化一下,限制二次開發者的腦洞,API不是越自由就越好,有句話說的好「你在召喚多強大的自由,就是在召喚多強大的奴役」。
那麼咱們應該如何限制API呢?
依照以前闡述過的說法「中間件的這種模式,應該做爲某個函數的裝飾者模式來使用」,所以,首先要有一個顯式申明的主函數,這塊咱們的API應該以下設計:
bridge.API('APINAME', handler) // 或者更加直接的 bridge.APINAME = handler
這樣一來,開發者在查找API實現的時候,就可以比較明確的找到這塊的實現,而最底層Bridge仍是會吧這個handler丟到一箇中間件中去作處理,這樣就能作到對這個handler的裝飾。
在這個的基礎上,再設計一個可以支持中間件的API:
bridge.use(middleware) // 對全部的API生效 bridge.use('APINAME', middleware) // 對某個API生效
再回顧一下以前列出來的問題:
API的不肯定性
API的實現都會放到handler中,且僅有這個handler會作主要邏輯處理,開發者明確的知道這裏寫的就是主邏輯。
API的維護成本高
API的主要實現就在handler中,只須要維護handler就行,有特殊的問題,再去看使用的中間件。
API難理解
用戶明確的知道只須要理解handler的實現就行,中間件的邏輯大部分是用於公共使用,只要統一理解就行。
到這裏,會槓的同窗仍是會問,其實你這好像問題也沒有徹底解決,只要開發者想搞你,仍是會出現以前的問題,好比就會有騷的人把邏輯寫到中間件裏面,不寫到handler裏面,你這種設計不仍是同樣。
這說的一點都沒錯,由於設計這個API不免的就是要開放給開發者這樣的能力,也就是:1)自定義API;2)對若干API作一些個性化的統一邏輯。API的設計者可以作到的就是在API上傳達給開發者一種規範,就好比 bridge.plugin() 這種開放性的API,就沒有 bridge.API() 這種好,由於後者很明確的讓開發者申明一個API,而前者不明確,前者讓開發者以爲中間件就是API的實現。
本篇咱們從中間件聊到中間件的使用實例,再聊到了裝飾器模式,最後聊到了使用中間件的API的設計。在平常API設計中,我不只會面對底層設計的選型,還會面對對外開放API的設計,二者都一樣重要。不過本篇僅表明我的觀點,歡迎在評論區指教、討論。