swagger-typescript-api
最近用了一套第三方的若依框架做產(chǎn)品,技術(shù)棧是vue3+vite+TS,前端團(tuán)隊(duì)3個(gè)人,時(shí)間緊任務(wù)重,大家開發(fā)肯定不會(huì)太注重代碼風(fēng)格及質(zhì)量,為了統(tǒng)一api的使用和類型的定義,引入了swagger-typescript-api來(lái)統(tǒng)一api和類型定義,記錄踩坑。
搭建
swagger-typescript-api使用方案分為兩種:
- 通過命令行直接使用
- 通過node運(yùn)行
因?yàn)槲倚枰傻巾?xiàng)目中,每次啟動(dòng)和打包都執(zhí)行一次用來(lái)更新最新的接口和類型,所以我采用了node的啟動(dòng)方式。
配置
我們可以在項(xiàng)目中定義一個(gè) swagger.config.ts 文件用來(lái)啟動(dòng)。
如下為一個(gè)大致的配置,swagger-typescript-api文檔基本等于沒有,基本全靠同行分享、源碼閱讀和打斷點(diǎn)調(diào)試一點(diǎn)點(diǎn)理解。
- url:swagger文檔的json地址
- output:結(jié)果輸出路徑
- fileName:輸出文件名稱
- templates:模板路徑,通常不需要,但如果有定制化需求則需要使用
- httpClientType:http庫(kù)的選擇
- modular:是否模塊化,通常會(huì)采用,因?yàn)榻涌诮Y(jié)構(gòu)會(huì)更清晰,但是我這里并沒有采用,具體原因后面會(huì)說(shuō)
- unwrapResponseData:響應(yīng)攔截器是否返回res.data,響應(yīng)結(jié)構(gòu)類型也會(huì)相應(yīng)的改變
通過閱讀源碼我們可以了解到,啟動(dòng)方法為 generateApi 函數(shù)。
import { generateApi, GenerateApiConfiguration } from 'swagger-typescript-api';
import path from 'path';
const config: Partial<GenerateApiConfiguration['config']> = {
url: 'http://192.168.131.90:8080/wing/v3/api-docs',
output: path.resolve(process.cwd(), './src/api/logistics'),
fileName: 'swagger-api.ts',
templates: path.resolve(process.cwd(), './swagger/templates'),
httpClientType: 'axios',
modular: false,
unwrapResponseData: true,
}
};
generateApi(config);
啟動(dòng)
因?yàn)槲覀兪褂玫膖s文件,所以采用ts-node,在package.json中配置如下,通過 npm run openapi 即可執(zhí)行(便于調(diào)試),也可集成到項(xiàng)目dev或者build。
"scripts": { "openapi": "ts-node ./swagger/swagger.config.ts" }
定制化
如果我不滿足默認(rèn)提供的一些配置項(xiàng),就需要深入的進(jìn)行修改了,好在 swagger-typescript-api 文檔雖然不全,但是開源且提供了很多深入的修改方法。
Hooks
swagger-typescript-api提供了很多鉤子,在配置項(xiàng)的hooks中即可配置,有哪些我就不細(xì)說(shuō)了,簡(jiǎn)單說(shuō)下我的需求和使用。
接口命名
經(jīng)過和后端的溝通,我發(fā)現(xiàn)接口的命名是從后端的一個(gè)裝飾器中取的,后端給我解釋那個(gè)裝飾器是用作路由的,這就出現(xiàn)了一個(gè)問題,對(duì)于/的路由,裝飾器里是空的,swagger-typescript-api 就會(huì)根據(jù) Restful API 的風(fēng)格,對(duì)于無(wú)法讀取到名字的接口用 list / edit / add 等進(jìn)行命名,并會(huì)添加一個(gè)數(shù)字用作唯一性,例如 list6 / edit6 這樣子,這就出現(xiàn)了一個(gè)問題,如果后端對(duì)當(dāng)前模塊添加了新的接口,是否會(huì)亂序?
我們協(xié)商的結(jié)果是大概率會(huì)的(沒測(cè)),不能存有這種隱患,所以重命名就很重要了,我們采取了 method + 路由 的駝峰式命名,并對(duì) path 參數(shù)移除 {} 保留中間參數(shù)的方式加入命名規(guī)則中,雖然會(huì)導(dǎo)致有的接口函數(shù)命名較長(zhǎng),但反正會(huì)自動(dòng)生成自動(dòng)補(bǔ)全不用手寫,長(zhǎng)就長(zhǎng)吧。
onFormatRouteName(routeInfo, templateRouteName) { let newName = generateFunctionName(routeInfo.method, routeInfo.route); return newName; }
結(jié)果大概是這樣的。
/** * @description 刪除出庫(kù)單 * * @tags 出庫(kù)單 * @name DeleteOutboundOrderId * @summary 刪除出庫(kù)單 * @request DELETE:/outboundOrder/{ids} * @secure */ deleteOutboundOrderId: (ids: string[], params: RequestParams = {}) => this.request<RVoid, any>({ path: `/wing/outboundOrder/${ids}`, method: "DELETE", secure: true, ...params, }),
參數(shù)錯(cuò)誤
如下圖所示,這是一個(gè) get 請(qǐng)求列表的 swagger 文檔,有經(jīng)驗(yàn)的人可以看出來(lái),這并非 query 參數(shù)該有的形式,而使用的時(shí)候我們也并不需要 bo 和 pageQuery 這兩個(gè)參數(shù),而是將這兩個(gè)參數(shù)解構(gòu)后的參數(shù)進(jìn)行傳入以達(dá)到過濾等目的。
后端給我的解釋是,這是 swagger 的 bug,和后端的代碼結(jié)構(gòu)有一定關(guān)系,沒辦法,只能我們來(lái)解決。

首先分析原因,后端給我解釋說(shuō),bo 對(duì)應(yīng)的是 body,也就是說(shuō)出現(xiàn)這個(gè)問題的這個(gè)參數(shù)名字會(huì)比較固定,至于其他的,我目前就發(fā)現(xiàn)了另外兩種類似的情況,一種是圖中的 pageQuery,還有一個(gè)也類似叫 page,既然參數(shù)固定,那就好辦了,找特定的鉤子重寫吧。ps:雖然說(shuō)著簡(jiǎn)單,但是找鉤子還是花了好久的時(shí)間,因?yàn)楹枚嚆^子看著都可行,本著最優(yōu)解法挨個(gè)試,翻源碼看原理,試到吐血最后發(fā)現(xiàn)都不行,這個(gè)問題太棘手了,估計(jì)設(shè)計(jì)之初就沒想著給這種問題留口,只能在最后一步渲染前對(duì)生成的字符進(jìn)行 replace 了,這也是最先發(fā)現(xiàn)的可行但是最不想用的方法,在這一步耽誤了好久的時(shí)間,吐血...。
前后的更改:
![]() |
![]() |
|
這個(gè)鉤子是較為靠后的一個(gè)鉤子了,這個(gè)鉤子中我們做了4件事
- 對(duì) body 中的 參數(shù)進(jìn)行了可選,雖然這樣有違安全,但是從實(shí)際出發(fā)考慮,有時(shí)候 swagger 的必填項(xiàng)參數(shù)并非實(shí)際的必填,為了避免可能出現(xiàn)的麻煩,所以對(duì)他進(jìn)行可選泛型
- 解決上述圖片中的問題,fixQueryType 會(huì)把上圖(左)中的 type 變?yōu)樯蠄D(中),這里我也就是提供個(gè)思路,大家可以在這一步進(jìn)行深度的定制化修改
- query 參數(shù)本身變?yōu)榭蛇x,這里也是為了便利舍棄了部分安全性
- 最后,對(duì)接口的 path 進(jìn)行了修改,添加了指定的前綴,如上圖(右)
onCreateRoute(routeData) { let payload = routeData.request.payload; if(payload?.name === "data") { // data參數(shù)內(nèi)的屬性變?yōu)榭蛇x payload.type = `Partial<${payload.type}>`; } if (routeData.request.query) { let type = routeData.request.query?.type as string; // query參數(shù)內(nèi)的屬性變?yōu)榭蛇x let newType = fixQueryType(type); routeData.request.query.type = newType; // query參數(shù)本身可選 routeData.request.query.optional = true; } //增加接口請(qǐng)求前綴 routeData.request.path = `/wing${routeData.request.path}`; return routeData; },
模板修改
swagger-typescript-api 提供了 .ejs 的模板用來(lái)渲染結(jié)果文件,并提供了原模板的文件供我們使用。
http庫(kù)對(duì)接
若依后臺(tái)的中是集成了一套axios的,而 swagger-typescript-api 也會(huì)生成一套,如下圖所示。
紅色框中為若依原來(lái)的 axios 實(shí)例,該文件中同時(shí)也定義了攔截器,不僅大量系統(tǒng)管理接口引入了該文件,我們也需要該攔截器進(jìn)行權(quán)限校驗(yàn),該文件不能動(dòng)。
我們可以將 swagger-typescript-api 生成的 api 文件中的 axios 實(shí)例引入,讓若依使用該 axios 實(shí)例,同時(shí)再將 API 文件從這里導(dǎo)出,即可得到一個(gè)連接攔截器的 API 類。

上面說(shuō)過用模塊化會(huì)生成分文件的更清晰的結(jié)構(gòu),不使用模塊就會(huì)生成如下圖中的一個(gè)類,其將所有子模塊都集成在同一個(gè) API 類中。
但是對(duì)于我的項(xiàng)目中,共用一個(gè)類而非模塊化繼承的方式更有利于集成進(jìn)若依的 http 模塊中。

修改響應(yīng)返回值
在若依的攔截器中,其對(duì)響應(yīng)結(jié)果進(jìn)行了進(jìn)一步取值,最終返回的結(jié)果為 res.data,而我們通過配置項(xiàng)中的 unwrapResponseData 參數(shù)設(shè)置,也對(duì) swagger-typescript-api 生成的 axios 封裝做了同樣的操作,如下圖所示,其封裝的 request 方法會(huì)返回 response.data,如果若依再進(jìn)行攔截,結(jié)果就變成了 response.data.data,并非我們想要的結(jié)果。
此時(shí)有人有疑問,那問什么要配置這個(gè)參數(shù)呢,原因則是因?yàn)槿绻慌渲妙愋途蜁?huì)錯(cuò)誤,因?yàn)?nbsp;swagger-typescript-api 并不知道我們會(huì)對(duì)其 axios 接入一個(gè)攔截器,對(duì)其結(jié)果進(jìn)行修改,如果不配置,其所有接口返回值類型都無(wú)法使用。

那么此時(shí)我們修改的方向就是如何把這個(gè) response.data 改為 response,通過查閱源碼,發(fā)現(xiàn)該段代碼是通過 swagger-typescript-api 提供的 .ejs 模型直接進(jìn)行渲染的,如下圖。
其正是對(duì) unwrapResponseData 參數(shù)進(jìn)行了判斷,所以我們對(duì)該模板進(jìn)行了修改,于是我又遇到了新的難題...。

將修改后的模板放到指定文件夾下,發(fā)現(xiàn)無(wú)論如何也不生效,我改了依賴中自帶的模板后,發(fā)現(xiàn)生效,也就是說(shuō)他沒走到我這里,在一通折騰+源碼閱讀后,我發(fā)現(xiàn),這個(gè)模板是通過另一個(gè)模板 http-client.ejs 引入的,我只需將 http-client.ejs 中引入的代碼修改為相對(duì)路徑,并將這兩個(gè)模板按照預(yù)定的依賴關(guān)系放在 templates 下,終于成功了!到此,配置就算完成了。
ps:原本到上圖的一步我以為我終于干完了,10點(diǎn)可以上床休息了,結(jié)果這最后一步硬是讓我1點(diǎn)才躺下,最后腦子都混沌了...
templates: path.resolve(process.cwd(), './swagger/templates'),
源碼中模板目錄下, default 文件夾對(duì)應(yīng)非模塊化, modular 文件夾對(duì)應(yīng)模塊化,根據(jù)不同的配置項(xiàng)會(huì)讀不同的模塊,而我們配置的模板路徑,源碼中會(huì)根據(jù)當(dāng)前目錄讀一遍所有引入的模板文件,如果存在就使用,如果不存在就使用默認(rèn)模板,我們不需要按照他的結(jié)構(gòu)進(jìn)行擺放,直接將我們修改的模板或自定義的模板放在該文件夾下即可,但是對(duì)于我上述的情況,有些模板并非直接引入的,而是在模板代碼中引入了其他模板,對(duì)于這種情況,按照我上述的做法即可。





浙公網(wǎng)安備 33010602011771號(hào)