
淘系技術部前端技術專家 張挺
淘寶在 2017 年之前就開始探索 TypeScript 的落地方式,随着時間的推移已經将新的子產品和架構全部遷移到 TypeScript 體系,在 2019 年, TypeScript 應用已經遍地開花,提前完成了非常不錯的布局。
GMTC 大會上,淘系技術部前端技術專家 張挺,分享了淘寶的 Midway 部分想法和實踐,本次分享主要介紹淘寶最近開源的 Midway 架構在新的場景、新的體系下如何和現有的 Egg 體系保持良好的相容性,同時又能在 TypeScript 的使用中有着獨特的體驗,通過針對不同場景的情況,我們引入相同的解決方案,為未來打下了夯實的基礎。
▐ 跨平台方案
整個分享的内容基調是基于目前的 Node.js 開發背景來的。
BFF 應用
對于阿裡集團來說,大部分的應用都是 BFF 應用,這些應用表現為長尾應用,維護人不斷的疊代流失,可能最後也找不到人維護,而這 70% 的應用被逐漸放棄,但是依舊有一些同學還在不斷的使用,對于一個 BU 來說,是很不利的。而 Serverless 的出現,可能是給這些應用一個機會,一個能擺脫維護,擺脫人員投入的機會,但是具體如何,還要看今年的發展,畢竟 Serverless 和傳統的應用開發有很大的不同。
全棧應用
剩下的就是全棧應用了,去除那些不重要的 BFF 應用,我們還對其做了核心和非核心的劃分,在這些應用中,不乏有承載千萬流量的大應用。而這些應用都由前端同學來維護,整個研發,測試,釋出的流程都必須非常謹慎。
TypeScript應用
在集團應用中,TS 的使用沒有想象的那麼多,據我們采集的資料,也就隻占 5% 左右,基本都是 midway(TS 版本,内部還有 JS版本),而今年,我們希望新應用全量使用 TS。
在這種場景下,對于業務同學來說,也有很多苦惱,比如業務複雜,接口沒有定義,以前使用 schema,但是沒有很大的推廣開來,這些都需要自己去拿時間來填,反而并不友好。在集團内釋出 RPC 服務,也需要寫 jsdoc,用于比對 java 的類型,在 js 場景場景下,這些都是不得已的選擇。
這個時候引入 TypeScript,來幫助我們解決這些品質,習慣,方法上的問題,就拿 midway 團隊來說,自從使用了 TypeScript,品質提升的非常明顯,平常需要測試很久的代碼,幾乎不會出現低級的問題,反而暴露出的大多都是邏輯問題。
面向接口程式設計,也成為了大家的習慣,每次多人協作,也隻需要先定義 interface,再根據 interface 的約定去各自實作,效率也非常高。同時,我們将 RPC 生成的工具替換成了 TypeScript 解析,将 Java 類型和 TS 類型做了一些映射,也避免了再使用 JsDoc 描述的問題。
講了這麼多 TS 的使用,下面來解決具體的問題。
Midway 是淘寶去年開源,面向未來的全棧架構,所謂面向未來,我們希望在未來能夠不斷的疊代,而主代碼不需要做過多的變更,同時在技術疊代的浪潮中,我們的架構也能不斷的适用于新的場景。
Egg.js 解決了 Web 開發的場景,在不斷的演進中,淘寶産生了全棧場景,Egg.js 已經無法滿足目前的需求,一方面集團内需要編寫上層架構,另一方面我們希望有原生的 TS 體驗。
在現有的 Controller - Service 架構中,除了 Controller 是明确意義的,Service 承載了非常多的職能,把 API,服務,邏輯其實都放在了一起,如果想單獨拆分目錄,也不是特别友善。
在 Egg.js 的更新之後,加入 ts-helper 填補了 TS 方面的空缺,不過目前由于目錄約定,編譯前後的檔案是在一起的,略微有一些不舒服。
在體驗方面,不同之處,例如:Egg.js 是支援過程式寫法的,在類的寫法中,由于請求鍊路的關系,比如手動繼承一個基類,這在業務中,如果想要自行再繼承就無法滿足。
同時,核心的 Loader 機制把屬性方法都挂載到了 app 上,顯得不是特别優雅。這促使我們做了第一代的設計。
▐ 第一代的設計
淘寶使用 IoC 非常早,我們有許多熟悉 Java 的同學非常喜歡 spring,一開始沿用了 XML 的寫法來配置,但是轉到前端來寫,XML 就變成了桎梏,負累重重。
在參考了輕量的 inversify 之後,我們覺得提供兩個簡單的裝飾器是一個最好的辦法。
@injectable() 提供了暴露類可以被 IoC 注入的能力。而 @inject() 提供了相應的注入屬性的能力。同時 inversify 有個 bindding 的包,提供了自動綁定的能力,我們也沿用了裡面的裝飾器,這才有了自研的 injection 包,裡面包含了 @provide 和 @inject 兩個裝飾器方法。
經過了 IoC 之後,我們把所有的對象統一放在了 IoC 容器中管理,不再需要關心執行個體的來源,也不在需要自行去建立執行個體(new)。
在使用了 IoC 之後,我們發現所有的寫法都可以變成傳統的 class 形式,封裝繼承多态三大特性都可以完美的使用,不再受到其他限制。
抛開裝飾器,代碼就是原生的 class,不管是測試也好,開發也好,都友善的使用 TS 的類型描述,最直覺,也最簡單。
在集團内,大約有 10 來個中間件,為了讓使用者有 TS 定義,将原有的代碼進行了增強,這都是一次性的工作量,可以造福後人。
▐ 第二代設計
和 Egg.js 解耦
之前我們解決了 Service 的問題, 通過 IoC,我們可以随便建立目錄,調用 API,以及測試。但是在 Web 層,和 egg 耦合的地方還是沿用了 egg 的寫法,雖然有變通的辦法,但是需要在體驗上更進一步。
Midway 基于 Egg.js 進行疊代開發,要實作 egg 的插件化能力,是直接在 package.json 中依賴了 egg 包,同時由于 IoC 的産出,又希望能夠讓各種開發體驗保持一緻,全部使用 class 的寫法,這也促使我們和 egg 進行了解耦,使用裝飾器完成各種 web 層的能力。
- 通過 @config 能力,和 app.config 解耦
- 通過 @plugin,和 app.xxx 插件解耦
- 通過 @inject() ctx 和請求鍊路解耦
此外還有 @logger 等裝飾器,提供額外的能力。
和目錄結構解耦
在做完 IoC 自掃描能力之後,已經完全不需要考慮目錄結構了,如果還需要 egg 的插件能力,目錄還需要保留,如果不需要插件,就可以自由定義目錄,掃描能力會完成一切。
通過自掃描能力,在極端情況下,可以将原有應用按功能劃分,也可以随意拆分成子子產品,甚至是 npm 包,而每一個子產品都可以随時獨立開發部署,也可以随時聚合成一個大應用。
和自己解耦
在做完這些之後,我們覺得未來可能要面向不同的場景去了,這個時候如果一味的隻考慮一個架構入口,可能會被受限制,雖然我們将 Midway 的代碼分開抽象,但是核心還是在一起的,各個裝飾器的實作和定義都是在同一個包,這樣擴充插件或者新增裝飾器都需要改動到 Midway 本身。是以需要一次重構把和 Midway 依賴的東西都解耦掉。
首先将裝飾器的定義都單獨分離出來,形成一個新包,這個包中有所有的裝飾器,以及他們最基本的函數(裝飾器定義)。
抽離完定義之後,我們就可以将實作部分單獨成為新的包,這個時候才有 midway-web 等包的産生。
▐ 面向未來的設計
所謂面向未來,就要為未來考慮和設計,而幾年 Serverless 的大熱,也為 Node.js 開發者提供了新的機會,而作為集團唯一的 Node.js 架構團隊,自然當仁不讓的投入到了研究的浪潮中。
在考慮跨場景之時,正逢将裝飾器定義與實作分離的時候,我們順便也将通用的能力沉澱了下來,這樣未來不同的場景都可以共享這些能力。
我們沉澱出了 midway-core 這個包,包含以下幾種能力。
第一種是自掃描注入 IoC 的能力,injection 提供通用綁定能力。
第二種是适配 midway 的請求作用域能力,不同的場景必然有請求,這個能力也屬于通用的能力之一。
第三是統一的裝飾器擴充能力,比如 @config 的擴充。
在 Midway-core 之外,我們也實作了一個 Decorator Manager 用于裝飾器的編碼和管理。
以新建立一個裝飾器為例,比如 @autoload,某些類加了這個裝飾器,希望能在應用啟動時自動被執行個體化,執行 init 方法。在新的分離體系下,隻需要定義一個裝飾器(标準函數),将這個裝飾器的 key 通過 saveModule 方法進行儲存。
在子產品、插件等任意你希望實作這個裝飾器能力的地方,通過 listModule 就可以把用到這個裝飾器的類通通拿出來,接下去你隻要循環,然後執行個體化這個類,執行方法就行了。通過這樣的機制,我們把所有的裝飾器都進行了改造,實作了整個模式。
在這次改造之後,我們覺得多場景的方案基本可行,在 koa/express 上做了試點,通過編碼之後,基本上在 200 行左右就完成整個功能,同時達到整個代碼使用相同的裝飾器,并且邏輯基本不變。
在這之後,又逐漸實作了其他的一些場景,同時對這些場景完成了一些工具鍊,配套等等。這些工具鍊有些是複用的,例如:Midway-bin,有些又是特定場景使用。
Serverless 場景,也是我們的整個場景之一。Serverless 整體分為很多部分,這裡我們隻将将函數代碼部分。
FaaS 是 Serverless 的實作之一,我們本來覺得在 FaaS 體系中代碼比較簡單,無需架構的幫助,但是在實際調研中,我們發現使用者的代碼還是有不少,同時檔案和複雜度還是有一些,是以也同樣需要架構的幫助。
但是這個架構必須是非常精簡,非常小,隻需要完成基本的功能即可。由于我們多場景的設計,代碼的整體結構也和原來的基本保持一緻,最終我們實作的 midway-faas,大概在 120 行代碼,保留最基本的 IoC 能力。
可以看到代碼寫法基本一緻,隻有裝飾器的差別。
可以看到除了包名不同,入口的裝飾器略有差異外,整個寫法上依舊保持基本的 class 形态。
除了寫法一緻之外,對于 FaaS 本身,我們還有一些訴求。
1、代碼一緻,能力一緻,這個通過 IoC,基本能夠做到了
2、我們希望一套代碼,能夠部署到多個雲環境
對于不同的平台來說,調用方式(回調,async),函數參數(event),以及描述檔案(spec)都是不同的,要把他們統一其實比較困難,但是經過内部驗證,我們依舊可以在一些地方進行統一。
我們針對不同平台的入口檔案進行包裹,一般來說,入口檔案是通過描述檔案 (spec) 的 handler 字段指定的,例如: index.handler ,指的就是 index.js 檔案的 handler 方法。但是由于 TypeScript 目錄結構的關系,所有的檔案都在 src/dist 目錄下,正好在根目錄空缺出了這個檔案,使得我們可以進行一些黑科技操作。
舉個例子,針對阿裡雲 FC,我們可以做一些 callback 轉 async 的包裹操作,使得使用者端調用的代碼格式保持統一,這部分代碼目前還未開源,這部分方案我們希望盡快,比如在下半年能夠提供給社群。通過這樣的黑科技操作,我們能夠在多個平台之間使得使用者代碼保持一緻性。
當然 Midway-faas 我們還在演進中,除了保持小體積,基本完整的功能外也想提供更多的能力。我們通過不斷的改進,從解決實際問題出發,和各個子產品解耦,實作不同場景相同的代碼編寫方式,這些都是不斷的思考,不斷的沉澱,未來可能還會有很多挑戰和變化,我們希望也能夠一如既往的疊代下去。
▐ 總結
- 我們通過 IOC,解決了困擾我們多年的全棧開發問題。
- 我們通過裝飾器,解決了和某個架構依賴過深的問題。
- 我們通過多場景,拓寬了 Node.js 的開發職能,也創造了前端的新場景。
本文是淘寶從 Midway5 到 Midway6 開發的實踐積累,過程中的點點滴滴都在字裡行間流出,不知道大家有沒有Get到其中的每次變化的原因,從中能夠了解為什麼要做這些事情,做了之後能夠帶來什麼影響,最後希望本文能夠幫助各位思考和改進。有更多疑問,歡迎在文章留言區回複交流。