ASP.NET Core靜態文件處理源碼探究

前言

    靜態文件(如 HTML、CSS、圖像和 JavaScript)等是Web程序的重要組成部分。傳統的ASP.NET項目通常都是部署在IIS上,IIS是一個功能很是強大的服務器平臺,能夠直接處理接收到的靜態文件處理而不須要通過應用程序池處理,因此不少狀況下對於靜態文件的處理程序自己是無感知的。ASP.NET Core則不一樣,做爲Server的Kestrel服務是宿主到程序上的,由宿主運行程序啓動Server而後能夠監聽請求,因此經過程序咱們直接能夠處理靜態文件相關。靜態文件默認存儲到項目的wwwroot目錄中,固然咱們也能夠自定義任意目錄去處理靜態文件。總之,在ASP.NET Core咱們能夠處理靜態文件相關的請求。css

StaticFile三劍客

    一般咱們在說道靜態文件相關的時候會涉及到三個話題分別是啓用靜態文件、默認靜態頁面、靜態文件目錄瀏覽,在ASP.NET Core分別是經過UseStaticFiles、UseDefaultFiles、UseDirectoryBrowser三個中間件去處理。只有配置了相關中間件才能去操做對應的處理,相信你們對這種操做已經很熟了。靜態文件操做相關的源碼都位於GitHub aspnetcore倉庫中的https://github.com/dotnet/aspnetcore/tree/v3.1.6/src/Middleware/StaticFiles/src目錄。接下來咱們分別探究這三個中間件的相關代碼,來揭開靜態文件處理的神祕面紗。html

UseStaticFiles

UseStaticFiles中間件使咱們處理靜態文件時最常使用的中間件,由於只有開啓了這個中間件咱們才能使用靜態文件,好比在使用MVC開發的時候須要私用js css html等文件都須要用到它,使用的方式也比較簡單git

//使用默認路徑,即wwwroot
app.UseStaticFiles();
//或自定義讀取路徑
var fileProvider = new PhysicalFileProvider($"{env.ContentRootPath}/staticfiles");
app.UseStaticFiles(new StaticFileOptions {
    RequestPath="/staticfiles",
    FileProvider = fileProvider
});

咱們直接找到中間件的註冊類StaticFileExtensions[點擊查看StaticFileExtensions源碼]github

public static class StaticFileExtensions
{
    public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app)
    {
        return app.UseMiddleware<StaticFileMiddleware>();
    }

    public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app, string requestPath)
    {
        return app.UseStaticFiles(new StaticFileOptions
        {
            RequestPath = new PathString(requestPath)
        });
    }

    public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app, StaticFileOptions options)
    {
        return app.UseMiddleware<StaticFileMiddleware>(Options.Create(options));
    }
}

通常咱們最經常使用到的是無參的方式和傳遞自定義StaticFileOptions的方式比較多,StaticFileOptions是自定義使用靜態文件時的配置信息類,接下來咱們大體看一下具體包含哪些配置項[點擊查看StaticFileOptions源碼]瀏覽器

public class StaticFileOptions : SharedOptionsBase
{
    public StaticFileOptions() : this(new SharedOptions())
    {
    }

    public StaticFileOptions(SharedOptions sharedOptions) : base(sharedOptions)
    {
        OnPrepareResponse = _ => { };
    }

    /// <summary>
    /// 文件類型提供程序,也就是咱們經常使用的文件名對應MimeType的對應關係
    /// </summary>
    public IContentTypeProvider ContentTypeProvider { get; set; }

    /// <summary>
    /// 設置該路徑下默認文件輸出類型
    /// </summary>
    public string DefaultContentType { get; set; }

    public bool ServeUnknownFileTypes { get; set; }
    
    /// <summary>
    /// 文件壓縮方式
    /// </summary>
    public HttpsCompressionMode HttpsCompression { get; set; } = HttpsCompressionMode.Compress;

    /// <summary>
    /// 準備輸出以前能夠作一些自定義操做
    /// </summary>
    public Action<StaticFileResponseContext> OnPrepareResponse { get; set; }
}

public abstract class SharedOptionsBase
{
    protected SharedOptionsBase(SharedOptions sharedOptions)
    {
        SharedOptions = sharedOptions;
    }

    protected SharedOptions SharedOptions { get; private set; }
    
    /// <summary>
    /// 請求路徑
    /// </summary>
    public PathString RequestPath
    {
        get { return SharedOptions.RequestPath; }
        set { SharedOptions.RequestPath = value; }
    }

    /// <summary>
    /// 文件提供程序,在.NET Core中若是須要訪問文件相關操做可以使用FileProvider文件提供程序獲取文件相關信息
    /// </summary>
    public IFileProvider FileProvider
    {
        get { return SharedOptions.FileProvider; }
        set { SharedOptions.FileProvider = value; }
    }
}

咱們自定義靜態文件訪問時,最經常使用到的就是RequestPath和FileProvider,一個設置請求路徑信息,一個設置讀取文件信息。若是須要自定義MimeType映射關係可經過ContentTypeProvider自定義設置映射關係緩存

var provider = new FileExtensionContentTypeProvider();
provider.Mappings[".myapp"] = "application/x-msdownload";
provider.Mappings[".htm3"] = "text/html";
app.UseStaticFiles(new StaticFileOptions
{
    ContentTypeProvider = provider,
    //能夠在輸出以前設置輸出相關
    OnPrepareResponse = ctx =>
    {
        ctx.Context.Response.Headers.Append("Cache-Control", $"public, max-age=3600");
    }
});

接下來咱們步入正題直接查看StaticFileMiddleware中間件的代碼[點擊查看StaticFileMiddleware源碼]安全

public class StaticFileMiddleware
{
    private readonly StaticFileOptions _options;
    private readonly PathString _matchUrl;
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;
    private readonly IFileProvider _fileProvider;
    private readonly IContentTypeProvider _contentTypeProvider;

    public StaticFileMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, IOptions<StaticFileOptions> options, ILoggerFactory loggerFactory)
    {
        _next = next;
        _options = options.Value;
        //設置文件類型提供程序
        _contentTypeProvider = options.Value.ContentTypeProvider ?? new FileExtensionContentTypeProvider();
       //文件提供程序
        _fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv);
       //匹配路徑
        _matchUrl = _options.RequestPath;
        _logger = loggerFactory.CreateLogger<StaticFileMiddleware>();
    }

    public Task Invoke(HttpContext context)
    {
        //判斷是夠獲取到終結點信息,這也就是爲何咱們使用UseStaticFiles要在UseRouting以前
        if (!ValidateNoEndpoint(context))
        {
        }
        //判斷HttpMethod,只能是Get和Head操做
        else if (!ValidateMethod(context))
        {
        }
        //判斷請求路徑是否存在
        else if (!ValidatePath(context, _matchUrl, out var subPath))
        {
        }
        //根據請求文件名稱判斷是否能夠匹配到對應的MimeType,若是匹配到則返回contentType
        else if (!LookupContentType(_contentTypeProvider, _options, subPath, out var contentType))
        {
        }
        else
        {   
            //執行靜態文件操做
            return TryServeStaticFile(context, contentType, subPath);
        }
        return _next(context);
    }

    private Task TryServeStaticFile(HttpContext context, string contentType, PathString subPath)
    {
        var fileContext = new StaticFileContext(context, _options, _logger, _fileProvider, contentType, subPath);
        //判斷文件是否存在
        if (!fileContext.LookupFileInfo())
        {
            _logger.FileNotFound(fileContext.SubPath);
        }
        else
        {   
            //靜態文件處理
            return fileContext.ServeStaticFile(context, _next);
        }
        return _next(context);
    }
}

關於FileExtensionContentTypeProvider這裏就不做講解了,主要是承載文件擴展名和MimeType的映射關係代碼不復雜,可是映射關係比較多,有興趣的能夠自行查看FileExtensionContentTypeProvider源碼,經過上面咱們能夠看到,最終執行文件相關操做的是StaticFileContext類[點擊查看StaticFileContext源碼]服務器

internal struct StaticFileContext
{
    private const int StreamCopyBufferSize = 64 * 1024;

    private readonly HttpContext _context;
    private readonly StaticFileOptions _options;
    private readonly HttpRequest _request;
    private readonly HttpResponse _response;
    private readonly ILogger _logger;
    private readonly IFileProvider _fileProvider;
    private readonly string _method;
    private readonly string _contentType;

    private IFileInfo _fileInfo;
    private EntityTagHeaderValue _etag;
    private RequestHeaders _requestHeaders;
    private ResponseHeaders _responseHeaders;
    private RangeItemHeaderValue _range;

    private long _length;
    private readonly PathString _subPath;
    private DateTimeOffset _lastModified;

    private PreconditionState _ifMatchState;
    private PreconditionState _ifNoneMatchState;
    private PreconditionState _ifModifiedSinceState;
    private PreconditionState _ifUnmodifiedSinceState;

    private RequestType _requestType;

    public StaticFileContext(HttpContext context, StaticFileOptions options, ILogger logger, IFileProvider fileProvider, string contentType, PathString subPath)
    {
        _context = context;
        _options = options;
        _request = context.Request;
        _response = context.Response;
        _logger = logger;
        _fileProvider = fileProvider;
        _method = _request.Method;
        _contentType = contentType;
        _fileInfo = null;
        _etag = null;
        _requestHeaders = null;
        _responseHeaders = null;
        _range = null;

        _length = 0;
        _subPath = subPath;
        _lastModified = new DateTimeOffset();
        _ifMatchState = PreconditionState.Unspecified;
        _ifNoneMatchState = PreconditionState.Unspecified;
        _ifModifiedSinceState = PreconditionState.Unspecified;
        _ifUnmodifiedSinceState = PreconditionState.Unspecified;
        //再次判斷請求HttpMethod
        if (HttpMethods.IsGet(_method))
        {
            _requestType = RequestType.IsGet;
        }
        else if (HttpMethods.IsHead(_method))
        {
            _requestType = RequestType.IsHead;
        }
        else
        {
            _requestType = RequestType.Unspecified;
        }
    }

    /// <summary>
    /// 判斷文件是否存在
    /// </summary>
    public bool LookupFileInfo()
    {
        //判斷根據請求路徑是否能夠獲取到文件信息
        _fileInfo = _fileProvider.GetFileInfo(_subPath.Value);
        if (_fileInfo.Exists)
        {
            //獲取文件長度
            _length = _fileInfo.Length;
            //最後修改日期
            DateTimeOffset last = _fileInfo.LastModified;
            _lastModified = new DateTimeOffset(last.Year, last.Month, last.Day, last.Hour, last.Minute, last.Second, last.Offset).ToUniversalTime();
            //ETag標識
            long etagHash = _lastModified.ToFileTime() ^ _length;
            _etag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
        }
        return _fileInfo.Exists;
    }
    
    /// <summary>
    /// 處理文件輸出
    /// </summary>
    public async Task ServeStaticFile(HttpContext context, RequestDelegate next)
    {
        //1.準備輸出相關Header,主要是獲取和輸出靜態文件輸出緩存相關的內容
        //2.咱們以前提到的OnPrepareResponse也是在這裏執行的
        ComprehendRequestHeaders();
        //根據ComprehendRequestHeaders方法獲取到的文件狀態進行判斷
        switch (GetPreconditionState())
        {
            case PreconditionState.Unspecified:
            //處理文件輸出
            case PreconditionState.ShouldProcess:
                //判斷是不是Head請求
                if (IsHeadMethod)
                {
                    await SendStatusAsync(Constants.Status200Ok);
                    return;
                }
                try
                {
                    //判斷是否包含range請求,即文件分段下載的狀況
                    if (IsRangeRequest)
                    {
                        await SendRangeAsync();
                        return;
                    }
                    //正常文件輸出處理
                    await SendAsync();
                    _logger.FileServed(SubPath, PhysicalPath);
                    return;
                }
                catch (FileNotFoundException)
                {
                    context.Response.Clear();
                }
                await next(context);
                return;
            case PreconditionState.NotModified:
                await SendStatusAsync(Constants.Status304NotModified);
                return;
            case PreconditionState.PreconditionFailed:
                await SendStatusAsync(Constants.Status412PreconditionFailed);
                return;
            default:
                var exception = new NotImplementedException(GetPreconditionState().ToString());
                throw exception;
        }
    }

    /// <summary>
    /// 通用文件文件返回處理
    /// </summary>
    public async Task SendAsync()
    {
        SetCompressionMode();
        ApplyResponseHeaders(Constants.Status200Ok);
        string physicalPath = _fileInfo.PhysicalPath;
        var sendFile = _context.Features.Get<IHttpResponseBodyFeature>();
        //判斷是否設置過輸出特徵操做相關,好比是否啓動輸出壓縮,或者自定義的輸出處理好比輸出加密等等
        if (sendFile != null && !string.IsNullOrEmpty(physicalPath))
        {
            await sendFile.SendFileAsync(physicalPath, 0, _length, CancellationToken.None);
            return;
        }
        try
        {
            //不存在任何特殊處理的操做做,直接讀取文件返回
            using (var readStream = _fileInfo.CreateReadStream())
            {
                await StreamCopyOperation.CopyToAsync(readStream, _response.Body, _length, StreamCopyBufferSize, _context.RequestAborted);
            }
        }
        catch (OperationCanceledException ex)
        {
            _context.Abort();
        }
    }

    /// <summary>
    /// 分段請求下載操做處理
    /// </summary>
    internal async Task SendRangeAsync()
    {
        if (_range == null)
        {
            ResponseHeaders.ContentRange = new ContentRangeHeaderValue(_length);
            ApplyResponseHeaders(Constants.Status416RangeNotSatisfiable);
            _logger.RangeNotSatisfiable(SubPath);
            return;
        }
        //計算range相關header數據
        ResponseHeaders.ContentRange = ComputeContentRange(_range, out var start, out var length);
        _response.ContentLength = length;
        //設置輸出壓縮相關header
        SetCompressionMode();
        ApplyResponseHeaders(Constants.Status206PartialContent);

        string physicalPath = _fileInfo.PhysicalPath;
        var sendFile = _context.Features.Get<IHttpResponseBodyFeature>();
        //判斷是否設置過輸出特徵操做相關,好比是否啓動輸出壓縮,或者自定義的輸出處理好比輸出加密等等
        if (sendFile != null && !string.IsNullOrEmpty(physicalPath))
        {
            _logger.SendingFileRange(_response.Headers[HeaderNames.ContentRange], physicalPath);
            await sendFile.SendFileAsync(physicalPath, start, length, CancellationToken.None);
            return;
        }
        try
        {
            using (var readStream = _fileInfo.CreateReadStream())
            {
                readStream.Seek(start, SeekOrigin.Begin); 
                _logger.CopyingFileRange(_response.Headers[HeaderNames.ContentRange], SubPath);
                //設置文件輸出起始位置和讀取長度
                await StreamCopyOperation.CopyToAsync(readStream, _response.Body, length, _context.RequestAborted);
            }
        }
        catch (OperationCanceledException ex)
        {
            _context.Abort();
        }
    }
}

    因爲代碼較多刪除了處主流程處理之外的其餘代碼,從這裏咱們能夠看出,首先是針對輸出緩存相關的讀取設置和處理,其這次是針對正常返回和分段返回的狀況,在返回以前判斷是否有對輸出作特殊處理的狀況,好比輸出壓縮或者自定義的其餘輸出操做的IHttpResponseBodyFeature,分段返回和正常返回相比主要是多了一部分關於Http頭Content-Range相關的設置,對於讀取自己其實只是讀取的起始位置和讀取長度的差異。app

UseDirectoryBrowser

目錄瀏覽容許在指定目錄中列出目錄裏的文件及子目錄。出於安全方面考慮默認狀況下是關閉的能夠經過UseDirectoryBrowser中間件開啓指定目錄瀏覽功能。一般狀況下咱們會這樣使用async

//啓用默認目錄瀏覽,即wwwroot
app.UseDirectoryBrowser();
//或自定義指定目錄瀏覽
var fileProvider = new PhysicalFileProvider($"{env.ContentRootPath}/MyImages");
app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
    RequestPath = "/MyImages",
    FileProvider = fileProvider
});

開啓以後當咱們訪問https:// /MyImages地址的時候將會展現以下效果,經過一個表格展現目錄裏的文件信息等
找到中間件註冊類[ 點擊查看DirectoryBrowserExtensions源碼]

public static class DirectoryBrowserExtensions
{
    public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app)
    {
        return app.UseMiddleware<DirectoryBrowserMiddleware>();
    }

    public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app, string requestPath)
    {
        return app.UseDirectoryBrowser(new DirectoryBrowserOptions
        {
            RequestPath = new PathString(requestPath)
        });
    }

    public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app, DirectoryBrowserOptions options)
    {
        return app.UseMiddleware<DirectoryBrowserMiddleware>(Options.Create(options));
    }
}

這個中間件啓用的重載方法和UseStaticFiles相似最終都是在傳遞DirectoryBrowserOptions,接下來咱們就看DirectoryBrowserOptions傳遞了哪些信息[點擊查看DirectoryBrowserOptions源碼]

public class DirectoryBrowserOptions : SharedOptionsBase
{
    public DirectoryBrowserOptions()
        : this(new SharedOptions())
    {
    }

    public DirectoryBrowserOptions(SharedOptions sharedOptions)
        : base(sharedOptions)
    {
    }

    /// <summary>
    /// 目錄格式化提供,默認是提供表格的形式展現,課自定義
    /// </summary>
    public IDirectoryFormatter Formatter { get; set; }
}

無獨有偶這個類和StaticFileOptions同樣也是集成自SharedOptionsBase類,惟一多了IDirectoryFormatter操做,經過它咱們能夠自定義展現到頁面的輸出形式,接下來咱們就重點看下DirectoryBrowserMiddleware中間件的實現

public class DirectoryBrowserMiddleware
{
    private readonly DirectoryBrowserOptions _options;
    private readonly PathString _matchUrl;
    private readonly RequestDelegate _next;
    private readonly IDirectoryFormatter _formatter;
    private readonly IFileProvider _fileProvider;

    public DirectoryBrowserMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, IOptions<DirectoryBrowserOptions> options)
        : this(next, hostingEnv, HtmlEncoder.Default, options)
    {
    }

    public DirectoryBrowserMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, HtmlEncoder encoder, IOptions<DirectoryBrowserOptions> options)
    {
        _next = next;
        _options = options.Value;
        //默認是提供默認目錄的訪問程序
        _fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv);
       //默認傳遞的是HtmlDirectoryFormatter類型,也就是咱們看到的輸出表格的頁面
        _formatter = options.Value.Formatter ?? new HtmlDirectoryFormatter(encoder);
        _matchUrl = _options.RequestPath;
    }

    public Task Invoke(HttpContext context)
    {
        //1.IsGetOrHeadMethod判斷是否爲Get或Head請求
        //2.TryMatchPath判斷請求的路徑和設置的路徑是否能夠匹配的上
        //3.TryGetDirectoryInfo判斷根據匹配出來的路徑可否查找到真實的物理路徑
        if (context.GetEndpoint() == null &&
            Helpers.IsGetOrHeadMethod(context.Request.Method)
            && Helpers.TryMatchPath(context, _matchUrl, forDirectory: true, subpath: out var subpath)
            && TryGetDirectoryInfo(subpath, out var contents))
        {
            //判斷請求路徑是不是/爲結尾
            if (!Helpers.PathEndsInSlash(context.Request.Path))
            {
                //若是不是以斜線結尾則重定向(我的感受直接在服務端重定向就能夠了,爲啥還要返回瀏覽器在請求一次)
                context.Response.StatusCode = StatusCodes.Status301MovedPermanently;
                var request = context.Request;
                var redirect = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path + "/", request.QueryString);
                context.Response.Headers[HeaderNames.Location] = redirect;
                return Task.CompletedTask;
            }
            //返回展現目錄的內容
            return _formatter.GenerateContentAsync(context, contents);
        }
        return _next(context);
    }
    
    /// <summary>
    /// 根據請求路徑匹配到物理路徑信息是否存在,存在則返回路徑信息
    /// </summary>
    private bool TryGetDirectoryInfo(PathString subpath, out IDirectoryContents contents)
    {
        contents = _fileProvider.GetDirectoryContents(subpath.Value);
        return contents.Exists;
    }
}

這個操做相對簡單了許多,主要就是判斷請求路徑可否和預設置的路徑匹配的到,若是匹配到則獲取能夠操做當前目錄內容IDirectoryContents而後經過IDirectoryFormatter輸出如何展現目錄內容,關於IDirectoryFormatter的默認實現類HtmlDirectoryFormatter這裏就不展現裏面的代碼了,邏輯很是的加單就是拼接成table的html代碼而後輸出,有興趣的同窗可自行查看源碼[點擊查看HtmlDirectoryFormatter源碼],若是自定義的話規則也很是簡單,主要看你想輸出啥

public class TreeDirectoryFormatter: IDirectoryFormatter
{
    public Task GenerateContentAsync(HttpContext context, IEnumerable<IFileInfo> contents)
    {
        //遍歷contents實現你想展現的方式
    }
}

而後在UseDirectoryBrowser的時候給Formatter賦值便可

app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
    Formatter = new TreeDirectoryFormatter()
});

UseDefaultFiles

不少時候出於安全考慮或者其餘緣由咱們想在訪問某個目錄的時候返回一個默認的頁面或展現,這個事實咱們就須要使用UseDefaultFiles中間件,當咱們配置了這個中間件,若是命中了配置路徑,那麼會直接返回默認的頁面信息,簡單使用方式以下

//wwwroot目錄訪問展現默認文件
app.UseDefaultFiles();
//或自定義目錄默認展現文件
var fileProvider = new PhysicalFileProvider($"{env.ContentRootPath}/staticfiles");
app.UseDefaultFiles(new DefaultFilesOptions
{
    RequestPath = "/staticfiles",
    FileProvider = fileProvider
});

老規矩,咱們查看下注冊UseDefaultFiles的源碼[點擊查看DefaultFilesExtensions源碼]

public static class DefaultFilesExtensions
{
    public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app)
    {
        return app.UseMiddleware<DefaultFilesMiddleware>();
    }

    public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app, string requestPath)
    {
        return app.UseDefaultFiles(new DefaultFilesOptions
        {
            RequestPath = new PathString(requestPath)
        });
    }

    public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app, DefaultFilesOptions options)
    {
        return app.UseMiddleware<DefaultFilesMiddleware>(Options.Create(options));
    }
}

使用方式和UseStaticFiles、UseDirectoryBrowser是同樣,最終都是調用傳遞DefaultFilesOptions的方法,咱們查看一下DefaultFilesOptions的大體實現[點擊查看源碼]

public class DefaultFilesOptions : SharedOptionsBase
{
    public DefaultFilesOptions()
        : this(new SharedOptions())
    {
    }

    public DefaultFilesOptions(SharedOptions sharedOptions)
        : base(sharedOptions)
    {
        //系統提供的默認頁面的名稱
        DefaultFileNames = new List<string>
        {
            "default.htm",
            "default.html",
            "index.htm",
            "index.html",
        };
    }

    /// <summary>
    /// 經過這個屬性能夠配置默認文件名稱
    /// </summary>
    public IList<string> DefaultFileNames { get; set; }
}

和以前的方法一模一樣,都是繼承自SharedOptionsBase,經過DefaultFileNames咱們能夠配置默認文件的名稱,默認是default.html/htm和index.html/htm。咱們直接查看中間件DefaultFilesMiddleware的源碼[點擊查看源碼]

public class DefaultFilesMiddleware
{
    private readonly DefaultFilesOptions _options;
    private readonly PathString _matchUrl;
    private readonly RequestDelegate _next;
    private readonly IFileProvider _fileProvider;

    public DefaultFilesMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, IOptions<DefaultFilesOptions> options)
    {
        _next = next;
        _options = options.Value;
        _fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv);
        _matchUrl = _options.RequestPath;
    }

    public Task Invoke(HttpContext context)
    {
        //1.咱們使用UseDefaultFiles中間件的時候要置於UseRouting之上,不然就會不生效
        //2.IsGetOrHeadMethod判斷請求爲Get或Head的狀況下才生效
        //3.TryMatchPath判斷請求的路徑和設置的路徑是否能夠匹配的上
        if (context.GetEndpoint() == null &&
            Helpers.IsGetOrHeadMethod(context.Request.Method)
            && Helpers.TryMatchPath(context, _matchUrl, forDirectory: true, subpath: out var subpath))
        {
            //根據匹配路徑獲取物理路徑對應的信息
            var dirContents = _fileProvider.GetDirectoryContents(subpath.Value);
            if (dirContents.Exists)
            {
                //循環配置的默認文件名稱
                for (int matchIndex = 0; matchIndex < _options.DefaultFileNames.Count; matchIndex++)
                {
                    string defaultFile = _options.DefaultFileNames[matchIndex];
                    //匹配配置的啓用默認文件的路徑+遍歷到的默認文件名稱的路徑是否存在
                    var file = _fileProvider.GetFileInfo(subpath.Value + defaultFile);
                    if (file.Exists)
                    {
                        //判斷請求路徑是否已"/"結尾,若是不是則從定向(這個點我的感受能夠改進)
                        if (!Helpers.PathEndsInSlash(context.Request.Path))
                        {
                            context.Response.StatusCode = StatusCodes.Status301MovedPermanently;
                            var request = context.Request;
                            var redirect = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path + "/", request.QueryString);
                            context.Response.Headers[HeaderNames.Location] = redirect;
                            return Task.CompletedTask;
                        }
                        //若是匹配的上,則將配置的啓用默認文件的路徑+遍歷到的默認文件名稱的路徑組合成新的Path交給_next(context)
                        //好比將組成相似這種路徑/staticfiles/index.html向下傳遞
                        context.Request.Path = new PathString(context.Request.Path.Value + defaultFile);
                        break;
                    }
                }
            }
        }
        return _next(context);
    }
}

這個中間件的實現思路也很是簡單主要的工做就是,匹配配置的啓用默認文件的路徑+遍歷到的默認文件名稱的路徑是否存在,若是匹配的上,則將配置的啓用默認文件的路徑+遍歷到的默認文件名稱的路徑組合成新的Path(好比/staticfiles/index.html)交給後續的中間件去處理。這裏值得注意的是UseDefaultFiles 必需要配合UseStaticFiles一塊兒使用,並且註冊位置要出如今UseStaticFiles之上。這也是爲何UseDefaultFiles只須要匹配到默認文件所在的路徑並從新賦值給context.Request.Path既可的緣由。
固然咱們也能夠自定義默認文件的名稱,由於只要能匹配的到具體的文件既可

var defaultFilesOptions = new DefaultFilesOptions
{
    RequestPath = "/staticfiles",
    FileProvider = fileProvider
};
//咱們能夠清除掉系統默認的默認文件名稱
defaultFilesOptions.DefaultFileNames.Clear();
defaultFilesOptions.DefaultFileNames.Add("mydefault.html");
app.UseDefaultFiles(defaultFilesOptions);

總結

    經過上面的介紹咱們已經大體瞭解了靜態文件處理的大體實現思路,相對於傳統的Asp.Net程序咱們能夠更方便的處理靜態文件信息,可是思路是一致的,IIS會優先處理靜態文件,若是靜態文件處理不了的狀況纔會交給程序去處理。ASP.NET Core也不例外,經過咱們查看中間件源碼裏的context.GetEndpoint()==null判斷能夠知道,ASP.NET Core更但願咱們優先去處理靜態文件,而不是任意出如今其餘位置去處理。關於ASP.NET Core處理靜態文件的講解就到這裏,歡迎評論區探討交流。

👇歡迎掃碼關注個人公衆號👇
相關文章
相關標籤/搜索