天天看點

ASP.NET Core靜态檔案處理源碼探究

前言

    靜态檔案(如 HTML、CSS、圖像和 JavaScript)等是Web程式的重要組成部分。傳統的ASP.NET項目一般都是部署在IIS上,IIS是一個功能非常強大的伺服器平台,可以直接處理接收到的靜态檔案處理而不需要經過應用程式池處理,是以很多情況下對于靜态檔案的處理程式本身是無感覺的。ASP.NET Core則不同,作為Server的Kestrel服務是宿主到程式上的,由宿主運作程式啟動Server然後可以監聽請求,是以通過程式我們直接可以處理靜态檔案相關。靜态檔案預設存儲到項目的wwwroot目錄中,當然我們也可以自定義任意目錄去處理靜态檔案。總之,在ASP.NET Core我們可以處理靜态檔案相關的請求。

StaticFile三劍客

    通常我們在說道靜态檔案相關的時候會涉及到三個話題分别是啟用靜态檔案、預設靜态頁面、靜态檔案目錄浏覽,在ASP.NET Core分别是通過UseStaticFiles、UseDefaultFiles、UseDirectoryBrowser三個中間件去處理。隻有配置了相關中間件才能去操作對應的處理,相信大家對這種操作已經很熟了。靜态檔案操作相關的源碼都位于GitHub aspnetcore倉庫中的https://github.com/dotnet/aspnetcore/tree/v3.1.6/src/Middleware/StaticFiles/src目錄。接下來我們分别探究這三個中間件的相關代碼,來揭開靜态檔案處理的神秘面紗。

UseStaticFiles

UseStaticFiles中間件使我們處理靜态檔案時最常使用的中間件,因為隻有開啟了這個中間件我們才能使用靜态檔案,比如在使用MVC開發的時候需要私用js css html等檔案都需要用到它,使用的方式也比較簡單

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

我們直接找到中間件的注冊類StaticFileExtensions[點選檢視StaticFileExtensions源碼]

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相關的設定,對于讀取本身其實隻是讀取的起始位置和讀取長度的差别。

UseDirectoryBrowser

目錄浏覽允許在指定目錄中列出目錄裡的檔案及子目錄。出于安全方面考慮預設情況下是關閉的可以通過UseDirectoryBrowser中間件開啟指定目錄浏覽功能。通常情況下我們會這樣使用

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

開啟之後當我們通路https:///MyImages位址的時候将會展示如下效果,通過一個表格展示目錄裡的檔案資訊等

ASP.NET Core靜态檔案處理源碼探究

找到中間件注冊類[點選檢視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處理靜态檔案的講解就到這裡,歡迎評論區探讨交流。

👇歡迎掃碼關注我的公衆号👇

ASP.NET Core靜态檔案處理源碼探究