關於Swagger優化
背景
儘管.net6已經發布很久了,但是公司的項目由於種種原因依舊基於.net Framework。伴隨著版本迭代,後端的api介面不斷增多,每次在聯調的時候,前端開發叫苦不迭:「小胖,你們的swagger頁面越來越卡了,快優化優化!」。
先查看swagger頁面載入耗時:
以上分別是:
- v1載入了兩次
- 重新編譯程式後打開swagger頁面,載入v1(api json)竟然耗時兩分多鐘。
- 第一次完整載入頁面後重新刷新頁面,再次查看swagger的耗時,這次明顯頁面載入速度提升了不少,但依舊不盡人人意,json返回後渲染耗時太久。
探察&解決
swagger載入的卡慢問題,萌生了優化swagger的想法,剛開始按傳統技能在網路上搜索了一大圈依舊未找到解決方案。幸好swashbuckle開源,還能自己動手分析了。先下載好源碼GitHub – domaindrivendev/Swashbuckle.WebApi: Seamlessly adds a swagger to WebApi projects!
一、先看看v1載入慢,卻要載入兩次。
從上面的圖上不難發現第二次v1的載入是跟在lang.js後面,而lang.js實際上就是用來做漢化。打開項目中這個文件
原來是為了添加控制器注釋,重新訪問後端取一次介面文檔。在查看了源碼js後,得到一個更簡單的方式,頁面的漢化翻譯,是在數據取完頁面已經渲染後才進行的,可直接使用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);
}
}
});
},
修改完文件以後,再看看頁面的載入,已經不會重複去訪問v1。
二、接下來處理v1載入慢
先看看項目的的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,實現了GetSwagger方法自定義返回數據,在這個方法里可以得知,實際上對api文檔是有做快取處理,v1載入的數據也就是這個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;
}
調試程式的時候,swashbuckle提供的GetSwagger方法佔據了大量的耗時。將源碼Swashbuckle.Core引用進來,重新打開swagger時會有個小問題,資源文件都報404錯誤,這個是因為嵌入資源文件沒有找到
<ItemGroup>
<EmbeddedResource Include="..\swagger-ui\dist\**\*.*">
<LogicalName>%(RecursiveDir)%(FileName)%(Extension)</LogicalName>
<InProject>false</InProject>
</EmbeddedResource>
</ItemGroup>
根據路徑查看,swagger-ui下是空白的。將從其他地方找到的或者從反編譯文件里整理出來的文件放到該目錄下,並將swagger-ui作為依賴項,重新編譯項目後swagger頁面載入資源文件就正常了。(如果有遇到依舊找不到資源文件的情況,重新再添加一次依賴項編譯項目即可)
接下來就可以開始調試了,經過一番波折,最終將元兇定位到了SwaggerGenerator中GetSwagger方法里獲取paths這個地方,實際上就是在使用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));
剛開始嘗試用多執行緒的方式進行處理,儘管確實能夠縮短獲取json數據的時間,但依舊有兩個問題:
- 執行緒不安全,時不時頁面會報錯
- 即使能快速返回json數據,頁面渲染耗慢的問題依舊未解決。正如前面我們的項目中GetSwagger是使用到快取的,在重新刷新swagger時,依舊存在卡慢問題。
三、將需返回json數據
優化swagger載入,需要同時考慮到前端渲染頁面以及後端梳理json數據所導致的頁面載入慢問題。有什麼好的辦法么?swashbuckle core版本是支援分組的,但是項目使用的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));
}
}
相對應的修改ISwagger介面,以及介面的實現類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));
自己項目中關於ISwagger實現也要修改,然後開始重新編譯自己的項目,重新打開swagger頁面,頁面在後端編譯後第一次打開也非常迅速。默認打開的是Account控制器下的介面,如果切換到其他控制器下的介面只需要在url後加入對應的/Controller
四、修改Swagger頁面
以上我們已經把頁面的載入慢的問題解決了,但在切換控制器上是否過於麻煩,能不能提升前端開發人員的使用體驗,提供一個下拉列表選擇是不是更好呢?繼續干!
找到源碼目錄下的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)加入填充下拉數據的js程式碼以及添加下拉框觸發事件
找到SwaggerUi.Views.HeaderView,添加下拉事件
重新編譯後,刷新頁面試試效果,可以下拉選擇分組
結語
關於swagger優化,鑒於本人水平有限,還有許多不足和錯誤的地方,煩請諸位大佬指正,叩謝!