關於Swagger優化

胖嘟嘟的梆子發表於2022-04-17

背景

  儘管.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頁面載入資原始檔就正常了。(如果有遇到依舊找不到資原始檔的情況,重新再新增一次依賴項編譯專案即可)

image-20220416224418539

  接下來就可以開始除錯了,經過一番波折,最終將元凶定位到了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優化,鑑於本人水平有限,還有許多不足和錯誤的地方,煩請諸位大佬指正,叩謝!

相關文章