SwaggerUI看煩了,IGeekFan.AspNetCore.Knife4jUI 幫你換個新面板
背景
好像是上週四,看到微信群有人說java有輪子swagger-bootstrap-ui,而c#,就是找不到。
於是我一看,就說大話:“這個只是一套UI,他這個有開源地址麼”
被@at說:你試試...
當天晚上就把swagger-ui, Knife4j,Swashbuckle.AspNetCore專案的原始碼都下載下來研究下,看看能不能整合到AspNETCore下,這樣我們就能給Swagger UI換套新面板。
knife4j
knife4j 是swagger-bootstrap-ui庫的升級版,作者已全面升級,全部以knife4j命名。
Gitee上也有2.8K
- 效果圖
IGeekFan.AspNetCore.Knife4jUI
他是swagger ui 庫:knife4j UI 的.NET Core封裝,支援 .NET Core3.0+或.NET Standard2.0。
概念對應關係如下
功能 | c# | java |
---|---|---|
實現swagger規範 | Swashbuckle.AspNetCore | spring-fox |
封裝成nuget包/maven包的UI庫 | Swashbuckle.AspNetCore.SwaggerUI | knife4j-v3-spring-ui |
UI庫 | swagger-ui-dist | knife4j-vue-v3(swagger v3版本) |
注意
swagger v2和v3版本不一樣,我只實現了swagger v3版本的封裝。
原始碼下載
Swashbuckle.AspNetCore.SwaggerUI。
通過中介軟體SwaggerUIM中介軟體iddleware,Invoke方法中,替換了Index.html中的%(DocumentTitle) %(HeadContent),%(ConfigObject)等等 。
private readonly SwaggerUIOptions _options;
//xxx
public async Task Invoke(HttpContext httpContext)
{
//xxx
if (httpMethod == "GET" && Regex.IsMatch(path, $"^/{Regex.Escape(_options.RoutePrefix)}/?index.html$"))
{
await RespondWithIndexHtml(httpContext.Response);
return;
}
//xxx
}
private async Task RespondWithIndexHtml(HttpResponse response)
{
response.StatusCode = 200;
response.ContentType = "text/html;charset=utf-8";
using (var stream = _options.IndexStream())
{
// Inject arguments before writing to response
var htmlBuilder = new StringBuilder(new StreamReader(stream).ReadToEnd());
foreach (var entry in GetIndexArguments())
{
htmlBuilder.Replace(entry.Key, entry.Value);
}
await response.WriteAsync(htmlBuilder.ToString(), Encoding.UTF8);
}
}
private IDictionary<string, string> GetIndexArguments()
{
return new Dictionary<string, string>()
{
{ "%(DocumentTitle)", _options.DocumentTitle },
{ "%(HeadContent)", _options.HeadContent },
{ "%(ConfigObject)", JsonSerializer.Serialize(_options.ConfigObject, _jsonSerializerOptions) },
{ "%(OAuthConfigObject)", JsonSerializer.Serialize(_options.OAuthConfigObject, _jsonSerializerOptions) }
};
}
在index.html中。
<title>%(DocumentTitle)</title>
var configObject = JSON.parse('%(ConfigObject)');
var oauthConfigObject = JSON.parse('%(OAuthConfigObject)');
當我們寫的aspnetcore專案整合swagger元件後,只會有一個ajax的非同步請求
knife4j-v3-spring-ui
效果(2.X版):http://knife4j.xiaominfo.com/doc.html
由於官方也沒有v3的demo,我們可以暫時通過v2分析,發現他有3個非同步請求,有一個請求返回相似的。另一個則是swagger的配置項,可以發現,返回值與SwaggerUIOptions一致。
功能 | c# (swagger v3) | java(swagger v2) |
---|---|---|
獲取分組配置 | 無 | /swagger-resources |
swagger配置項 | 無 | /swagger-resources/configuration/ui |
api文件 | https://api.igeekfan.cn/swagger/v1/swagger.json | /v2/api-docs?group=2.X版本 |
結構如下。
[
{
"name":"2.X版本",
"url":"/v2/api-docs?group=2.X版本",
"swaggerVersion":"2.0",
"location":"/v2/api-docs?group=2.X版本"
},
{
"name":"分組介面",
"url":"/v2/api-docs?group=分組介面",
"swaggerVersion":"2.0",
"location":"/v2/api-docs?group=分組介面"
},
{
"name":"預設介面",
"url":"/v2/api-docs?group=預設介面",
"swaggerVersion":"2.0",
"location":"/v2/api-docs?group=預設介面"
}
]
- swagger 配置項
- http://knife4j.xiaominfo.com/swagger-resources/configuration/ui
請求方法: GET
{
"deepLinking":true,
"displayOperationId":false,
"defaultModelsExpandDepth":1,
"defaultModelExpandDepth":1,
"defaultModelRendering":"example",
"displayRequestDuration":false,
"docExpansion":"none",
"filter":false,
"operationsSorter":"alpha",
"showExtensions":false,
"tagsSorter":"alpha",
"validatorUrl":"",
"apisSorter":"alpha",
"jsonEditor":false,
"showRequestHeaders":false,
"supportedSubmitMethods":[
"get",
"put",
"post",
"delete",
"options",
"head",
"patch",
"trace"
]
}
接下來我們看下knife4j,可以看到,他有knife4j-vue-v3專案,這個是swagger v3版本的vue實現。
我們開啟knife4j-vue-v3專案,修改配置項vue.config.js,devServer 反向代理的地址(後臺地址)
proxy: {
"/": {
target: 'http://localhost:5000/',
ws: true,
changeOrigin: true
}
}
安裝依賴,並執行他
yarn install
yarn serve
我們會看到一個請求錯誤。Knife4j文件請求異常,因為後臺並沒有:'/v3/api-docs/swagger-config'。
也就是上文中的/swagger-resources/configuration/ui,我們可以在SwaggerUIMiddleware中介軟體獲取這些引數,原本是通過替換字串,現在,我們可以寫一個api。怎麼寫呢。
下載Swashbuckle.AspNetCore的原始碼,開啟Swashbuckle.AspNetCore.sln。
我們嘗試修改Swashbuckle.AspNetCore.SwaggerUI專案中,SwaggerUIMiddleware中的原始碼。
Invoke方法增加如下處理,將配置項直接返回json串。
if (httpMethod == "GET" && Regex.IsMatch(path, $"^/v3/api-docs/swagger-config$"))
{
await httpContext.Response.WriteAsync(JsonSerializer.Serialize(_options.ConfigObject, _jsonSerializerOptions));
return;
}
在swagger v3 版本中,/v3/api-docs/swagger-config,返回了分組資訊,urls欄位。
效果如下
設定test/WebSites/Basic專案為啟動專案,執行後,打開了http://localhost:5000/index.html,這個還是原本的swagger ui,我們開啟http://localhost:8080/#/home,前臺依舊提示有問題。
AddSwaggerGen 需要增加Server,前臺判斷有BUG,非空。
servers.length得到的是0,問號表示式就會執行後面的servers[0].url,
臨時方案
services.AddSwaggerGen(c =>
{
c.AddServer(new OpenApiServer()
{
Url = "",
Description = "v1"
});
});
但還有一個問題,前臺根據operationId生成的路由, [HttpPost(Name = "CreateProduct")]比如CreateProduct。有些沒有設定 Name的,點選後就會出現空白介面。
增加CustomOperationIds的配置,通過反射獲取方法名。
services.AddSwaggerGen(c =>
{
//xx
c.CustomOperationIds(apiDesc =>
{
return apiDesc.TryGetMethodInfo(out MethodInfo methodInfo) ? methodInfo.Name : null;
});
});
解決了這些問題。
我們建立一個新類庫,起名IGeekFan.AspNetCore.Knife4jUI
將前端打包。修改打包檔案配置,vue.config.js
assetsDir: "knife4j",
indexPath: "index.html"
打包
yarn run build
複製到根目錄,設定為嵌入檔案,刪除不需要的images和txt文字。
<ItemGroup>
<EmbeddedResource Include="knife4j/**/*" />
<EmbeddedResource Include="favicon.ico" />
<EmbeddedResource Include="index.html" />
</ItemGroup>
將後臺Swashbuckle.AspNetCore.SwaggerUI的程式碼複製過來,全部重新命名。比如中介軟體名字為
SwaggerUIMiddleware -> Knife4jUIMiddleware。即SwaggerUI都改成Knife4jUI。
Knife4jUIMiddleware修改位置
private const string EmbeddedFileNamespace = "IGeekFan.AspNetCore.Knife4jUI";
刪除無用的替換變數,增加
Knife4UIOptions 修改
public Func<Stream> IndexStream { get; set; } = () => typeof(Knife4UIOptions).GetTypeInfo().Assembly
.GetManifestResourceStream("IGeekFan.AspNetCore.Knife4jUI.index.html");
Startup 中的Configure中介軟體
將UseSwaggerUI()改成UseKnife4UI()
app.UseKnife4UI(c =>
{
c.RoutePrefix = ""; // serve the UI at root
c.SwaggerEndpoint("/v1/api-docs", "V1 Docs");
c.SwaggerEndpoint("/gp/api-docs", "登入模組");
});
不用IGeekFan.AspNetCore.Knife4jUI也能實現?
當然,可以。
我們也能通過其他方式,在SwaggerUI的基礎上,替換比如替換Index.html頁面,自己打包前端UI,複製到專案中等。
將knife4j-vue-v3專案打包,放到wwwwroot目錄中。
需要配置靜態檔案。
app.UseStaticFiles();
app.UseSwaggerUI(c =>
{
c.RoutePrefix = ""; // serve the UI at root
c.SwaggerEndpoint("/v1/api-docs", "V1 Docs");//這個配置無效。
c.IndexStream = () => new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot")).GetFileInfo("index.html").CreateReadStream();
});
重寫/v3/api-docs/swagger-config路由
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapSwagger("{documentName}/api-docs");
endpoints.MapGet("/v3/api-docs/swagger-config", async (httpContext) =>
{
JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions();
_jsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
_jsonSerializerOptions.IgnoreNullValues = true;
_jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, false));
SwaggerUIOptions _options = new SwaggerUIOptions()
{
ConfigObject = new ConfigObject()
{
Urls = new List<UrlDescriptor>
{
new UrlDescriptor()
{
Url="/v1/api-docs",
Name="V1 Docs"
}
}
}
};
await httpContext.Response.WriteAsync(JsonSerializer.Serialize(_options.ConfigObject, _jsonSerializerOptions));
});
});
IGeekFan.AspNetCore.Knife4jUI指南
相關依賴項
knife4j
- knife4j-vue-v3(不是vue3,而是swagger-ui-v3版本)
Swashbuckle.AspNetCore
- Swashbuckle.AspNetCore.Swagger
- Swashbuckle.AspNetCore.SwaggerGen