如何在 asp.net core 的中間件中返回具體的頁面

前言

在 asp.net core 中,存在着中間件這一概念,在中間件中,我們可以比過濾器更早的介入到 http 請求管道,從而實現對每一次的 http 請求、響應做切面處理,從而實現一些特殊的功能

在使用中間件時,我們經常實現的是鑑權、請求日誌記錄、全局異常處理等等這種非業務性的需求,而如果你有在 asp.net core 中使用過 swashbuckle(swagger)、health check、mini profiler 等等這樣的組件的話,你會發現,這些第三方的組件往往都提供了頁面,允許我們通過可視化的方式完成某些操作或瀏覽某些數據

因爲自己也需要實現類似的功能,雖然使用到的知識點很少、也很簡單,但是在網上搜了搜也沒有專門介紹這塊的文檔或文章,所以本篇文章就來說明如何在中間件中返回頁面,如果你有類似的需求,希望可以對你有所幫助

Step by Step

最終實現的功能其實很簡單,當用戶跳轉到某個指定的地址後,自定義的中間件通過匹配到該路徑,從而返回指定的頁面,所以這裏主要會涉及到中間件是如何創建,以及如何處理頁面中的靜態文件引用

因爲這塊並不會包含很多的代碼,所以這裏主要是通過分析 Swashbuckle.AspNetCore 的代碼,瞭解它是如何實現的這一功能,從而給我們的功能實現提供一個思路

在 asp.net core 中使用 Swashbuckle.AspNetCore 時,我們通常需要在 Startup 類中針對組件做如下的配置,根據當前程序的信息生成 json 文件 =》 公開生成的 json 文件地址 =》 根據 json 文件生成可視化的交互頁面

public class Startup
{
    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        // 生成 swagger 配置的 json 文件
        services.AddSwaggerGen(s =>
        {
            s.SwaggerDoc("v1", new OpenApiInfo
            {
                Contact = new OpenApiContact
                {
                    Name = "Danvic Wang",
                    Url = new Uri("https://yuiter.com"),
                },
                Description = "Template.API - ASP.NET Core 後端接口模板",
                Title = "Template.API",
                Version = "v1"
            });

            // 參數使用駝峯的命名方式
            s.DescribeAllParametersInCamelCase();
        });
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // 公開 swagger 生成的 json 文件節點
        app.UseSwagger();

        // 啓用 swagger 可視化交互頁面
        app.UseSwaggerUI(s =>
        {
            s.SwaggerEndpoint($"/swagger/v1/swagger.json",
                $"Swagger doc v1");
        });
    }
}

可以看到最終呈現給用戶的頁面,其實是在 Configure 方法中通過調用 UseSwaggerUI 方法來完成的,這個方法是在 Swashbuckle.AspNetCore.SwaggerUI 這個程序集中,所以這裏直接從 github 上找到對應的文件夾,clone 下源代碼,來看下是如何實現在中間件中返回特定的頁面

在 clone 下的代碼中,排除掉一些 c#、node.js 使用到的項目性文件,可以看到整個項目中的文件按照功能可以分爲三大塊,其中最核心的則是在 SwaggerUIMiddleware 類中,因此,這裏主要聚焦在這個中間件類的實現

源碼分析

在一個 asp.net core 中間件中,核心的處理邏輯是在 Invoke/InvokeAsync 方法中,結合我們使用 swagger 時的場景,可以看到,在將組件中所包含的頁面呈現給用戶時,主要存在如下兩個處理邏輯

1、當匹配到用戶訪問的是 /swagger 時,返回 301 的 http 狀態碼,瀏覽器重定向到 /swagger/index.html,從而再次觸發該中間件的執行

2、當匹配到請求的地址爲 /swagger/index.html 時,將嵌入到程序集中的文件通過 stream 流的形式獲取到,轉換成字符串,再指定請求的響應的類型爲 text/html,從而實現將頁面返回給用戶

public async Task Invoke(HttpContext httpContext)
{
    var httpMethod = httpContext.Request.Method;
    var path = httpContext.Request.Path.Value;

    // If the RoutePrefix is requested (with or without trailing slash), redirect to index URL
    if (httpMethod == "GET" && Regex.IsMatch(path, $"^/?{Regex.Escape(_options.RoutePrefix)}/?$"))
    {
        // Use relative redirect to support proxy environments
        var relativeRedirectPath = path.EndsWith("/")
            ? "index.html"
            : $"{path.Split('/').Last()}/index.html";

        RespondWithRedirect(httpContext.Response, relativeRedirectPath);
        return;
    }

    if (httpMethod == "GET" && Regex.IsMatch(path, $"^/{Regex.Escape(_options.RoutePrefix)}/?index.html$"))
    {
        await RespondWithIndexHtml(httpContext.Response);
        return;
    }

    await _staticFileMiddleware.Invoke(httpContext);
}

這裏需要注意,因爲類似於這種功能,我們可能會打包成獨立的 nuget 包,然後通過 nuget 進行引用,所以爲了能夠正確獲取到頁面及其使用到的靜態資源文件,我們需要將這些靜態文件的屬性修改成嵌入的資源,從而在打包時可以包含在程序集中

對於網頁來說,在引用這些靜態資源文件時存在一種相對的路徑關係,因此,這裏在中間件的構造函數中,我們需要將頁面需要使用到的靜態文件,通過構建 StaticFileMiddleware 中間件,將文件映射與網頁相同的 /swagger 路徑下面,從而確保頁面所需的資源可以正確加載

public class SwaggerUIMiddleware
{
    private const string EmbeddedFileNamespace = "Swashbuckle.AspNetCore.SwaggerUI.node_modules.swagger_ui_dist";

    private readonly SwaggerUIOptions _options;
    private readonly StaticFileMiddleware _staticFileMiddleware;

    public SwaggerUIMiddleware(
        RequestDelegate next,
        IHostingEnvironment hostingEnv,
        ILoggerFactory loggerFactory,
        SwaggerUIOptions options)
    {
        _options = options ?? new SwaggerUIOptions();

        _staticFileMiddleware = CreateStaticFileMiddleware(next, hostingEnv, loggerFactory, options);
    }

    private StaticFileMiddleware CreateStaticFileMiddleware(
        RequestDelegate next,
        IHostingEnvironment hostingEnv,
        ILoggerFactory loggerFactory,
        SwaggerUIOptions options)
    {
        var staticFileOptions = new StaticFileOptions
        {
            RequestPath = string.IsNullOrEmpty(options.RoutePrefix) ? string.Empty : $"/{options.RoutePrefix}",
            FileProvider = new EmbeddedFileProvider(typeof(SwaggerUIMiddleware).GetTypeInfo().Assembly, EmbeddedFileNamespace),
        };

        return new StaticFileMiddleware(next, hostingEnv, Options.Create(staticFileOptions), loggerFactory);
    }
}

映射文件路徑

當完成了頁面的呈現後,因爲一般我們會創建一個單獨的類庫來實現這些功能,在頁面中,可能會包含前後端的數據交互,由於我們在宿主的 API 項目中已經完成了對於路由規則的設定,所以這裏只需要在類庫中通過 nuget 引用 Microsoft.AspNetCore.Mvc.Core ,然後與 Web API 一樣的定義 controller,確保這個中間件在宿主程序的調用位於路由匹配規則之後即可

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseAuthorization();

	// Endpoint 路由規則設定
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });

    // 自定義中間件
    app.UseMiddleware<SampleUIMiddleware>();
}

參考

  1. ASP.NET Core 應用針對靜態文件請求的處理: 以 web 的形式發佈靜態文件
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章