關(guān)于Swagger優(yōu)化的實(shí)戰(zhàn)記錄
背景
盡管.net6已經(jīng)發(fā)布很久了,但是公司的項(xiàng)目由于種種原因依舊基于.net Framework。伴隨著版本迭代,后端的api接口不斷增多,每次在聯(lián)調(diào)的時候,前端開發(fā)叫苦不迭:“小胖,你們的swagger頁面越來越卡了,快優(yōu)化優(yōu)化!”。
先查看swagger頁面加載耗時:


以上分別是:
- v1加載了兩次
- 重新編譯程序后打開swagger頁面,加載v1(api json)竟然耗時兩分多鐘。
- 第一次完整加載頁面后重新刷新頁面,再次查看swagger的耗時,這次明顯頁面加載速度提升了不少,但依舊不盡人人意,json返回后渲染耗時太久。
探察&解決
swagger加載的卡慢問題,萌生了優(yōu)化swagger的想法,剛開始按傳統(tǒng)技能在網(wǎng)絡(luò)上搜索了一大圈依舊未找到解決方案。幸好swashbuckle開源,還能自己動手分析了。先下載好源碼GitHub - domaindrivendev/Swashbuckle.WebApi: Seamlessly adds a swagger to WebApi projects!
一、先看看v1加載慢,卻要加載兩次。
從上面的圖上不難發(fā)現(xiàn)第二次v1的加載是跟在lang.js后面,而lang.js實(shí)際上就是用來做漢化。打開項(xiàng)目中這個文件

原來是為了添加控制器注釋,重新訪問后端取一次接口文檔。在查看了源碼js后,得到一個更簡單的方式,頁面的漢化翻譯,是在數(shù)據(jù)取完頁面已經(jīng)渲染后才進(jìn)行的,可直接使用window.swaggerApi.swaggerObject.ControllerDesc。
setControllerSummary: function () {
var summaryDict = window.swaggerApi.swaggerObject.ControllerDesc;
var id, controllerName, strSummary;
$("#resources_container .resource").each(function (i, item) {
id = $(item).attr("id");
if (id) {
controllerName = id.substring(9);
try {
strSummary = summaryDict[controllerName];
if (strSummary) {
$(item).children(".heading").children(".options").first().prepend('<li class="controller-summary" style="color:green;" title="' + strSummary + '">' + strSummary + '</li>');
}
} catch (e) {
console.log(e);
}
}
});
},修改完文件以后,再看看頁面的加載,已經(jīng)不會重復(fù)去訪問v1。

二、接下來處理v1加載慢
先看看項(xiàng)目的的swagger配置:
GlobalConfiguration.Configuration
.EnableSwagger(c =>
{
c.IncludeXmlComments(GetXmlCommentsPath(thisAssembly.GetName().Name));
c.IncludeXmlComments(GetXmlCommentsPath("xxxx.Api.Dto"));
c.SingleApiVersion("v1", "xxxx.Api");
c.CustomProvider((defaultProvider) => new CachingSwaggerProvider(defaultProvider));
})
配置不多,其中有個CachingSwaggerProvider,實(shí)現(xiàn)了GetSwagger方法自定義返回?cái)?shù)據(jù),在這個方法里可以得知,實(shí)際上對api文檔是有做緩存處理,v1加載的數(shù)據(jù)也就是這個SwaggerDocument。這也意味著,v1加載慢的原因出在這里。
public SwaggerDocument GetSwagger(string rootUrl, string apiVersion)
{
var cacheKey = string.Format("{0}_{1}", rootUrl, apiVersion);
SwaggerDocument srcDoc = null;
//只讀取一次
if (!_cache.TryGetValue(cacheKey, out srcDoc))
{
srcDoc = (_swaggerProvider as Swashbuckle.Swagger.SwaggerGenerator).GetSwagger(rootUrl, apiVersion);
srcDoc.vendorExtensions = new Dictionary<string, object> { { "ControllerDesc", GetControllerDesc() } };
_cache.TryAdd(cacheKey, srcDoc);
}
return srcDoc;
}
調(diào)試程序的時候,swashbuckle提供的GetSwagger方法占據(jù)了大量的耗時。將源碼Swashbuckle.Core引用進(jìn)來,重新打開swagger時會有個小問題,資源文件都報(bào)404錯誤,這個是因?yàn)榍度胭Y源文件沒有找到
<ItemGroup>
<EmbeddedResource Include="..\swagger-ui\dist\**\*.*">
<LogicalName>%(RecursiveDir)%(FileName)%(Extension)</LogicalName>
<InProject>false</InProject>
</EmbeddedResource>
</ItemGroup>
根據(jù)路徑查看,swagger-ui下是空白的。將從其他地方找到的或者從反編譯文件里整理出來的文件放到該目錄下,并將swagger-ui作為依賴項(xiàng),重新編譯項(xiàng)目后swagger頁面加載資源文件就正常了。(如果有遇到依舊找不到資源文件的情況,重新再添加一次依賴項(xiàng)編譯項(xiàng)目即可)


接下來就可以開始調(diào)試了,經(jīng)過一番波折,最終將元兇定位到了SwaggerGenerator中GetSwagger方法里獲取paths這個地方,實(shí)際上就是在使用CreatePathItem的時候耗時過久
var paths = GetApiDescriptionsFor(apiVersion)
.Where(apiDesc => !(_options.IgnoreObsoleteActions && apiDesc.IsObsolete()))
.OrderBy(_options.GroupingKeySelector, _options.GroupingKeyComparer)
.GroupBy(apiDesc => apiDesc.RelativePathSansQueryString())
.ToDictionary(group => "/" + group.Key, group => CreatePathItem(group, schemaRegistry));
剛開始嘗試用多線程的方式進(jìn)行處理,盡管確實(shí)能夠縮短獲取json數(shù)據(jù)的時間,但依舊有兩個問題:
線程不安全,時不時頁面會報(bào)錯即使能快速返回json數(shù)據(jù),頁面渲染耗慢的問題依舊未解決。正如前面我們的項(xiàng)目中GetSwagger是使用到緩存的,在重新刷新swagger時,依舊存在卡慢問題。
三、將需返回json數(shù)據(jù)
優(yōu)化swagger加載,需要同時考慮到前端渲染頁面以及后端梳理json數(shù)據(jù)所導(dǎo)致的頁面加載慢問題。有什么好的辦法么?swashbuckle core版本是支持分組的,但是項(xiàng)目使用的Framework版本不支持,既然不支持,就直接改造源碼,按控制器分組,說干就干:
找到HttpConfigurationExtensions類的EnableSwagger方法,這個方法用來配置路由
public static SwaggerEnabledConfiguration EnableSwagger(
this HttpConfiguration httpConfig,
string routeTemplate,
Action<SwaggerDocsConfig> configure = null)
{
var config = new SwaggerDocsConfig();
if (configure != null) configure(config);
httpConfig.Routes.MapHttpRoute(
name: "swagger_docs" + routeTemplate,
routeTemplate: routeTemplate,
defaults: null,
constraints: new { apiVersion = @".+" },
handler: new SwaggerDocsHandler(config)
);
//配置控制器路由
string controllRouteTemplate=DefaultRouteTemplate+"/{controller}";
httpConfig.Routes.MapHttpRoute(
name: "swagger_docs" + controllRouteTemplate,
routeTemplate: controllRouteTemplate,
defaults: null,
constraints: new { apiVersion = @".+" },
handler: new SwaggerDocsHandler(config)
);
return new SwaggerEnabledConfiguration(
httpConfig,
config.GetRootUrl,
config.GetApiVersions().Select(version => routeTemplate.Replace("{apiVersion}", version)));
}
接下來找到SwaggerDocsHandler類,修改SendAsync方法,獲取controller,并將controller傳遞到GetSwagger中
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var swaggerProvider = _config.GetSwaggerProvider(request);
var rootUrl = _config.GetRootUrl(request);
var apiVersion = request.GetRouteData().Values["apiVersion"].ToString();
var controller = request.GetRouteData().Values["controller"]?.ToString();
if (string.IsNullOrEmpty(controller))
{
controller = "Account";
}
try
{
var swaggerDoc = swaggerProvider.GetSwagger(rootUrl, apiVersion, controller);
var content = ContentFor(request, swaggerDoc);
return TaskFor(new HttpResponseMessage { Content = content });
}
catch (UnknownApiVersion ex)
{
return TaskFor(request.CreateErrorResponse(HttpStatusCode.NotFound, ex));
}
}
相對應(yīng)的修改ISwagger接口,以及接口的實(shí)現(xiàn)類SwaggerGenerator,增加按Controller篩選
public interface ISwaggerProvider
{
SwaggerDocument GetSwagger(string rootUrl, string apiVersion,string controller);
}
SwaggerGenerator的GetSwagger修改:
var temps = GetApiDescriptionsFor(apiVersion)
.Where(apiDesc => !(_options.IgnoreObsoleteActions && apiDesc.IsObsolete()));
if (string.IsNullOrEmpty(controller) == false)
{
temps = temps.Where(apiDesc => apiDesc.ActionDescriptor.ControllerDescriptor.ControllerName.ToLower() == controller.ToLower());
}
var paths = temps
.OrderBy(_options.GroupingKeySelector, _options.GroupingKeyComparer)
.GroupBy(apiDesc => apiDesc.RelativePathSansQueryString())
.ToDictionary(group => "/" + group.Key, group => CreatePathItem(group, schemaRegistry));
自己項(xiàng)目中關(guān)于ISwagger實(shí)現(xiàn)也要修改,然后開始重新編譯自己的項(xiàng)目,重新打開swagger頁面,頁面在后端編譯后第一次打開也非常迅速。默認(rèn)打開的是Account控制器下的接口,如果切換到其他控制器下的接口只需要在url后加入對應(yīng)的/Controller


四、修改Swagger頁面
以上我們已經(jīng)把頁面的加載慢的問題解決了,但在切換控制器上是否過于麻煩,能不能提升前端開發(fā)人員的使用體驗(yàn),提供一個下拉列表選擇是不是更好呢?繼續(xù)干!
找到源碼目錄下的SwaggerUi\CustomAssets\Index.html文件,添加一個id為select_baseUrl的select下拉選擇框,并將input_baseurl輸入框隱藏

修改swagger-ui-js下的window.SwaggerUi的render方法(要記得將index.html中的swagger-ui-min-js的引用改為swagger-ui-js)加入填充下拉數(shù)據(jù)的js代碼以及添加下拉框觸發(fā)事件

找到SwaggerUi.Views.HeaderView,添加下拉事件


重新編譯后,刷新頁面試試效果,可以下拉選擇分組

結(jié)語
到此這篇關(guān)于Swagger優(yōu)化的文章就介紹到這了,更多相關(guān)Swagger優(yōu)化實(shí)戰(zhàn)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
ASP.NET全棧開發(fā)教程之前后臺校驗(yàn)結(jié)合詳解
這篇文章主要給大家介紹了關(guān)于ASP.NET全棧開發(fā)教程之前后臺校驗(yàn)結(jié)合的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-07-07
ASP.NET web.config中數(shù)據(jù)庫連接字符串connectionStrings節(jié)的配置方法
ASP.NET web.config中數(shù)據(jù)庫連接字符串connectionStrings節(jié)的配置方法,需要的朋友可以參考一下2013-05-05
在FireFox/IE下Response中文文件名亂碼問題解決方案
只是針對沒有空格和IE的情況下使用Response.AppendHeader()如果想在FireFox下輸出沒有編碼的文件,并且IE下輸出的文件名中空格不為+號,就要多一次判斷了,接下來將詳細(xì)介紹下感興趣的朋友可以了解下,或許對你有所幫助2013-02-02
asp.net中ListBox 綁定多個選項(xiàng)為選中及刪除實(shí)現(xiàn)方法
文章介紹了關(guān)于在asp.net中的listbox的綁定多個選項(xiàng)和同時選中多個選項(xiàng)以及刪除多個選項(xiàng)的方法2012-04-04
jQuery+Ajax用戶登錄功能的實(shí)現(xiàn)
前幾天把jbox源碼修改成仿QQ空間模擬窗口后發(fā)現(xiàn)有很多人在關(guān)注。今天就貼一下我利用該模擬窗口實(shí)現(xiàn)的用戶登錄功能的代碼。2009-11-11
ASP.NET實(shí)現(xiàn)偽靜態(tài)網(wǎng)頁方法小結(jié)
這篇文章主要介紹了ASP.NET實(shí)現(xiàn)偽靜態(tài)網(wǎng)頁方法小結(jié),主要包括了利用Httphandler實(shí)現(xiàn)URL重寫、地址重寫、利用Mircosoft URLRewriter.dll實(shí)現(xiàn)頁面?zhèn)戊o態(tài)等,需要的朋友可以參考下2014-09-09

