前端如何正確使用中間件?

2020-08-28 技術聯盟總壇

折木 阿里技術

阿里妹導讀:中間件可以算是一種前端中常用的」設計模式「了,有的時候甚至可以說,整個應用的架構都是使用中間件為基礎搭建的。那麼中間件有哪些利弊?什麼才是中間件正確的使用姿勢?本文將分享作者在實際使用中的一些想法,歡迎同學們共同討論。


文末福利:下載《大促背後的前端核心業務實踐》電子書。


一 先簡單講講中間件


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);}


這是一段非常簡潔的中間件代碼,通過傳入的類似這樣的函數的列表:


const middlware = async (ctx, next) => { /** * do something to modify ctx */ if (/* let next run */true) { await next(ctx) } /** * do something to modify ctx */}


得到一個新的函數,這個函數的執行,會讓這些中間件逐個處理並且每個中間件可以決定:


  • 在下個中間件執行之前做些什麼?
  • 是否讓下個中間件執行?
  • 在下個中間件執行之後做些什麼?


現在的中間件都是使用的洋蔥模型,洋蔥模型的大致示意圖是這樣的:



按照這張圖,中間件的執行順序是:


middleware1 -> middleware2 -> middleware3 -> middleware2 -> middleware1


處理順序是先從外到內,再從內到外,這就是中間件的洋蔥模型。


在中間件的應用上,開發者可以將統一邏輯做成一個中間件,這樣就能在其他地方復用這個邏輯。我覺得這其實是中間件這種模式的初心吧,好,那我們先把這個初心放一放。


但實際上這個模式就是一個空殼,通過不同的中間件,就可以實現各種自定義的邏輯。比如:


const handler = compose([(ctx, next) => { if (ctx.question === &39;) { ctx.answer = &39;; return } if (next) [ next(ctx) ]}, (ctx, next) => { if (/age/.test(ctx.question)) { ctx.answer = &39;; return } if (next) [ next(ctx) ]}])const ctx = { question: &39; };handler(ctx)console.log(ctx.answer) // log helloctx.question = &39;handler(ctx)console.log(ctx.answer) // log i am 5 yours old


這樣看起來我們甚至可以去實現一個機器人,把中間件這麼拿來用,相當於是把中間件作為一個 if 語句展開了,通過不同的中間件對ctx的劫持來分離邏輯,看起來好像也不錯?


得益於中間件的靈活性,每個中間件可以實現:1)實現獨立的某個邏輯;2)控制後續的流程是否執行。


二 聊聊幾個慄子


今年有參與做個小程序的Bridge,先簡單的介紹一下Bridge的功能。


  • 從支付寶小程序的視角來抹平其他小程序的JSAPI。
  • Bridge擁有擴展能力,能夠擴展JSAPI。


看到「擴展能力」,熟練的同學應該就知道我可以切入正題了。


Bridge現在的設計採用插件的形式來注入一系列API,每個插件都有插件名、API名、中間件三個屬性,注入Bridge後,Bridge會將相同API名的插件整合在一起,讓這個API的實現指向這些插件帶有的中間件的 compose ,用這種方式來實現自定義API。



這種方式其實看起來是非常美妙的,因為所有的API都可以通過插件的形式注入到Bridge中,可以很靈活地擴展API。


眾所周知,有得必有失。這種模式其實有自己的缺點,具體的缺點我們可以從「面向開發者」和「面向使用者」兩方面來整理,面向開發者指的是面向寫插件(也就是寫中間件)的開發者,面向使用者(用戶)指的是最終使用Bridge的開發者。


1 面向開發者


API的不確定性


多個中間件註冊在同一個API上面,開發者自己的API是否能夠運行正常有的時候是依賴上下文的,而零散的中間件被載入Bridge,對於上下文的修改是未知的,因此會對API的執行帶來很多不確定性。


從洋蔥模型的圖上面,我們可以發現,內層往往會受外部的影響,當然在回流的時候,外部中間件也會受內部中間件的影響,在開發中間件的時候,我們需要考慮自己的依賴,在已知依賴沒有問題的情況下去做開發,才會比較穩妥,但是當前Bridge這種散裝載入Plugin的方式,讓依賴關係沒有辦法穩定的描述。


API的維護成本高


由於有多個插件註冊到單個API上,維護某個API的情況下就會有比較高的成本,就有點像是現在服務端排查問題的情況了,多個插件的情況下最差情況可能要逐個開發者去做排查,最終才能分鍋,雖然實際情況可能沒有這麼糟糕,但還是要考慮一下最差的情況。


那麼為什麼服務端這種架構是合理的呢,因為服務端的微服務架構確實能夠將多個業務邏輯拆分來解耦比較複雜的邏輯,但是Bridge這裡只是想要實現某個API的實現,也很明顯的發現實際在使用過程中,基本都採用了單插件的註冊方式。所以感覺用中間件來實現某個API,有點過渡設計了,反而造成了維護成本的提高。


2 面向使用者


面向使用者其實要分為兩種不同的場景:直接使用插件和通過preset來使用插件的集成。


3 直接使用插件



這種模式下,使用者要自己去引用插件,通過引用一系列插件來獲得一個可以正常使用的API,可是使用者往往期望的是能夠開箱即用,也就是說拿到這個Bridge,看一下文檔,就能夠調用某個API了,如今需要Bridge的使用者通過自己註冊一個Plugin這樣的東西來獲得一個可用的API,顯然是不合理的,不合理的地方主要體現在:


API難理解


Bridge使用者原本只需要理解一下Bridge的文檔就能夠輕鬆使用API,現在需要理解plugin的運作機制以及如果有若干個插件的話,還要理解插件單獨的運作和相互運作的實現。這些都很難讓一個Bridge使用者接受,對於業務開發來講,成本變高了。


問題排查難度上升


這點和之前提到的使用中間件這種方式會造成API的邏輯不連貫的情況是類似的,Bridge在使用API的時候如果發現有問題,那麼排查問題的時候就會因為有多個Plugin實現而增加難度,總的來說他還是需要簡單的去理解每個插件基本實現和插件間的運作機制,對於業務開發來講,成本較高。


4 通過Preset來使用插件的集成


由於上述Bridge使用者直接使用Bridge的問題,其實通過preset的封裝可以解決一部分的痛點,而Bridge的preset的概念就是,通過編寫一個preset,這個preset去維護一個API和多個插件的關係,然後給到用戶的是一個集成好的Bridge,上述的兩個問題都可以被解決。



這個模式看起來形式上就是之前的Bridge用戶選了一個「最懂插件的人」來做他們的替身,做了之前的那個User的角色,讓這個人來理解所有的Plugin,並維護這些API,這個&34;趨向極限,基本就等於開發Plugin的人了,那麼饒了這麼大一圈,做的這麼靈活,最後維護插件的人是同一個人,也是這個人對外輸出API,那麼這個東西真的有複雜到要這麼拆分麼。就我個人來講覺得還是直接簡單明了的的實現一個API來的方便。那是中間件這種模式辣雞嗎?


5 抬走,我們來看下一個


除了Bridge,老生常談的還有類似Fetch這樣的基礎庫,Fetch是另一波同學做的了,但是我也是小撇了幾眼代碼,發現居然也用了中間件來做,正好可以看看他們在設計API的時候使用中間件的合理性。先說說Fetch為啥走了這條路吧,看看訴求:


因為實在是有太多種不同的請求類型了,因此想實現在相同的入參下,通過adaptor參數來區分最終走怎樣的請求邏輯。


因此Fetch在設計的時候,是這麼使用中間件的:





fetch.use(commonMiddleware)fetch.use(&39;, [middleware]) // 比如adaptor-jsonfetch({ ...requestConfig, adaotpr: &39; })



Fetch的中間件使用會相對合理一點,通過利用中間件的特性,對外輸出了相同的出入參,再藉助不同的中間件對請求的過程做流式處理。


但實際的使用過程中,也要很多同學反饋,有類似Bridge的使用問題。


6 調用過程排查困難


和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設計模式》中的一圖:



可以發現幾點:


  • 裝飾器和我們需要擴展的Class都是實現了同一個接口。


  • 裝飾器是通過接收一個Component對象來運作的。


看到上面這兩點就會發現其實裝飾器模式和中間件的概念是大致相同的,只不過在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(&39;, handler)// 或者更加直接的bridge.APINAME = handler


這樣一來,開發者在查找API實現的時候,就能夠比較明確的找到這塊的實現,而最底層Bridge還是會吧這個handler丟到一個中間件中去做處理,這樣就能做到對這個handler的裝飾。


在這個的基礎上,再設計一個能夠支持中間件的API:


bridge.use(middleware) // 對所有的API生效bridge.use(&39;, 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的設計,兩者都同樣重要。不過本篇僅代表個人觀點,歡迎在評論區指教、討論。

相關焦點

  • 前端如何正確使用中間件?
    阿里妹導讀:中間件可以算是一種前端中常用的」設計模式「了,有的時候甚至可以說,整個應用的架構都是使用中間件為基礎搭建的。那麼中間件有哪些利弊?什麼才是中間件正確的使用姿勢?本文將分享作者在實際使用中的一些想法,歡迎同學們共同討論。文末福利:下載《大促背後的前端核心業務實踐》電子書。
  • 前後端分離必備, Golang Gin中如何使用JWT(JsonWebToken)中間件?
    JSON Web Token(縮寫 JWT)是目前最流行的跨域認證解決方案,也是目前前後端分離項目中普遍使用的認證技術.本文介紹如何在Golang Gin Web框架中使用JWT認證中間件以及模擬測試, 以供參考, 關於JWT詳細原理可以參考: JWT RFC: https://tools.ietf.org/html/rfc7519 JWT IETF: http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html
  • MySql DAL中間件
    前言:mysql作為網際網路公司都會用到的資料庫,如果在使用過程中出現性能問題,會採用mysql的橫向擴展,使用主從複製來提高讀性能,要是解決寫入問題,需要進行分庫分表。本文不會去介紹mysql的高可用,主要介紹mysql的訪問中間件(DAL)的一些實現方案。
  • 前端Visual Studio Code的正確使用姿勢
    今天我們就來講一下VSCode在前端的正確使用姿勢。插件安裝我們在開發前端項目時,一定會用到像React、Vue這類前端框架,為了讓VSCode準確識別這且框架的排版語法,我們通常會安裝相關框架的輔助插件,比如Vue高亮格式化插件Vetur,經過簡單的配置就可以達到方便閱讀的效果。
  • 前端Visual Studio Code的正確使用姿勢
    今天我們就來講一下VSCode在前端的正確使用姿勢。插件安裝我們在開發前端項目時,一定會用到像React、Vue這類前端框架,為了讓VSCode準確識別這且框架的排版語法,我們通常會安裝相關框架的輔助插件,比如Vue高亮格式化插件Vetur,經過簡單的配置就可以達到方便閱讀的效果。
  • 使用消息中間件時,如何保證消息僅僅被消費一次?
    消息中間件使用廣泛,常用來削峰填谷、系統解耦、異步處理。異步處理可能是使用的最多的場景了,比如現在的技術博客網站,都採用積分制,用戶發表一篇文章後,可以獲取想要的積分,為了提升系統的性能,給用戶加積分的操作可以異步處理,並不需要放在同步流程中。
  • 如何區分前端BUG和後臺BUG?你知道答案嗎?
    如果是功能問題,控制臺的問題至少定位到:www的問題還是資料庫問題,如果是www問題至少要定位到是前端還是後端問題;如果是資料庫問題至少要定位到是服務端接口問題還是中間件問題。最後,跟進重點問題的修改進度和方案,詢問開發時如何修改的,反思開發的修改方案是否存在漏洞。為啥要學會區分前端和後臺BUG?
  • 如何正確使用天窗遮陽簾?
    那麼,如何正確使用遮陽簾,避免在使用過程中產生問題呢?手動遮陽簾 手動遮陽簾主要包括帘布、拉杆和用於開關手動遮陽簾的把手。小夥伴們需要注意的是在任何時候都不要拉動拉杆,因為拉動拉杆會使遮陽簾系統不平衡,甚至無法作動。
  • React系列十七 - Redux(三)中間件
    中間件的使用1.1. 組件中異步請求在之前簡單的案例中,redux中保存的counter是一個本地定義的數據,我們可以直接通過同步的操作來dispatch action,state就會被立即更新。答案就是使用中間件(Middleware);學習過Express或Koa框架的童鞋對中間件的概念一定不陌生;在這類框架中,Middleware可以幫助我們在請求和響應之間嵌入一些操作的代碼,比如cookie
  • udb 資料庫讀寫分離中間件
    2、在UDB控制臺上進入讀寫分離頁面,並開啟讀寫分離, 從而創建出讀寫分離中間件。該中間件作為業務程序和1主多從集群之間的代理,中轉業務程序發往主庫和從庫的請求。使用讀寫分離中間件時,客戶將業務的資料庫訪問地址,直接切換到該IP即可,無需修改業務程序代碼, 支持標準SQL、系統命令、事務、視圖、存儲過程、觸發器等MySQL功能。UDB讀寫分離中間件是永久免費的, 客戶只需要創建好主從庫,即可開啟並使用讀寫分離中間件,無需額外費用。普通版UDB和高可用UDB均支持創建讀寫分離中間件。
  • 腫瘤患者出院後,如何正確使用吸氧機吸氧?
    其實,隨著科技的進步,家庭制氧機也憑藉著經濟、安全的特點走進了患者的生活,今天就和大家來分享一下腫瘤患者出院後如何正確使用吸氧機吸氧,快跟著視頻一起動起來吧! 1、將吸氧機豎向平放,取下溼化杯,往杯中倒入1/3的蒸餾水或者是純淨水,加水後蓋上蓋子,以免氧氣外溢。 2、打開制氧機開關,將雙鼻導管連接到制氧機氧氣出口。
  • 2020Web前端開發常見面試題匯總-開課吧
    Web前端面試題:各個生命周期的作用是什麼?問題:你是如何理解fiber的?解析:React Fiber 是種基於瀏覽器的單線程調度算法.。Web前端面試題:redux中如何進行異步操作?解析:當然,我們可以在 componentDidmount 中直接進請求須藉助redux。
  • Django 之中間件7個保安
    自定義中間件中間件可以定義的五個方法?方法&39;exception:&39;我是第一個中間件裡面的process_exception&39;我是第二個自定義中間件裡面的process_request方法&39;我是第二個中間件裡面的process_reponse方法&39;我是第二個中間件裡面的process_view&39;我是第二個中間件裡面的process_template_reponse方法&39;exception:&39;
  • 移動中間件2.0時代,企業如何使用HTML5
    HTML5具有天生的跨平臺優勢、可調用底層設備和系統功能的能力、企業使用成本低廉等優勢。同時眼下HTML5也有一些不足,如運行性能不佳、體驗不好等。這種情況下,企業面臨兩難的抉擇。一方面HTML5是明確的標準發展方向,同時能大幅降低企業移動化成本。另一方面HTML5存在的不足阻礙企業使用。
  • 揭開RFID中間件的神秘面紗
    RFID 中間件扮演RFID標籤和應用程式之間的中介角色,從應用程式端使用 中間 件所提供一組通用的應用程式接口(API),即能連到RFID RFID 中間件是一種面向消息的中間件RFID 中間件的特徵,一般來說,RFID 中間件具有下列的特色: 獨立於架構RFID中間件獨立並介於RFID讀寫器與後端應用程式之間,並且能夠與多個RFID讀寫器以及多個後端應用程式連接,以減輕架構與維護的複雜性。 數據流RFID的主要目的在於將實體對象轉換為信息環境下的虛擬對象,因此數據處理是RFID最重要的功能。
  • WEB前端高頻面試題每天5道題第三波
    WEB前端高頻面試題每天5道題第三波嗨,大家好又見面了,今天看看都有哪些WEB前端工程師的面試題吧?1、你們公司代碼是怎麼管理的? git svn及提交流程?你使用過什麼?覺得nodejs對於前端來說最大的作用是什麼?答:會用express可以對mysql、mongodb進行增、刪、改、查;node可以做為中間件使用,對一些nosql操作比較好比如mongodb。如果用react做服務端渲染可以使用node的express來做中間件使用。3、說下圖片上傳是如何實現的?答:拿到服務端的圖片上傳接口。
  • 消息中間件大全
    為了應對這些三高問題,我們程式設計師可是,熬沒了頭髮,每天都在思考如何解決這些問題的概念呢,其實就是底層作業系統軟體,非業務應用軟體,不是直接給最終用戶使用的,不能直接給客戶帶來價值的軟體統稱為中間件,說人話就是,處在一個非核心的位置,有它沒它都行,並不影響系統正常運行,它在兩個系統中相當於一個橋梁的作用。
  • 400 道前端面試題!阿里、頭條、網易等 19 家大廠面經全公開!
    阿里使用過的Koa2中間件Koa-body原理介紹自己寫過的中間件有沒有涉及到Cluster介紹Pm2Master掛了的話Pm2怎麼處理如何和MySQL進行通信React聲明周期及自己的理解如何配置React-Router路由的動態加載模塊服務端渲染SSR介紹路由的History介紹Redux數據流的流程Redux如何實現多個組件之間的通信,多個組件使用相同狀態如何進行管理多個組件之間如何拆分各自的
  • 消息中間件企業級應用
    消息中間件企業級應用眾所周知,消息是大型分布式系統中不可或缺的重要組件。它使用簡單,卻解決了不少難題,比如異步處理,系統藕合,流量削鋒,分布式事務管理等。實現了一個高性能,高可用,高擴展的系統。本章通過介紹消息中間件的應用場景,消息中間件的傳輸模式, 三個方面來對消息中間件進行入門介紹。還在等什麼,趕快來學習吧!
  • 把中間件做到如此境界,牛!
    一、中間件分類   ① 數據訪問中間件:實現異構環境下的資料庫實現聯接或文件系統實現聯接   ② 遠程過程調用中間件(RPC)   ③ 面向消息的中間件:兼容多種通訊協議、語言、應用程式、硬體和軟體平臺,基於數據通信來進行分布式系統的集成   ④ 交易中間件:提供支持大規模事務處理的可靠運行環境   ⑤ 面向對象中間件