淺談ASP.NET Core靜態(tài)文件處理源碼探究
前言
靜態(tài)文件(如 HTML、CSS、圖像和 JavaScript)等是Web程序的重要組成部分。傳統(tǒng)的ASP.NET項(xiàng)目一般都是部署在IIS上,IIS是一個功能非常強(qiáng)大的服務(wù)器平臺,可以直接處理接收到的靜態(tài)文件處理而不需要經(jīng)過應(yīng)用程序池處理,所以很多情況下對于靜態(tài)文件的處理程序本身是無感知的。ASP.NET Core則不同,作為Server的Kestrel服務(wù)是宿主到程序上的,由宿主運(yùn)行程序啟動Server然后可以監(jiān)聽請求,所以通過程序我們直接可以處理靜態(tài)文件相關(guān)。靜態(tài)文件默認(rèn)存儲到項(xiàng)目的wwwroot目錄中,當(dāng)然我們也可以自定義任意目錄去處理靜態(tài)文件??傊?,在ASP.NET Core我們可以處理靜態(tài)文件相關(guān)的請求。
StaticFile三劍客
通常我們在說道靜態(tài)文件相關(guān)的時候會涉及到三個話題分別是啟用靜態(tài)文件、默認(rèn)靜態(tài)頁面、靜態(tài)文件目錄瀏覽,在ASP.NET Core分別是通過UseStaticFiles、UseDefaultFiles、UseDirectoryBrowser三個中間件去處理。只有配置了相關(guān)中間件才能去操作對應(yīng)的處理,相信大家對這種操作已經(jīng)很熟了。靜態(tài)文件操作相關(guān)的源碼都位于GitHub aspnetcore倉庫中的https://github.com/dotnet/aspnetcore/tree/v3.1.6/src/Middleware/StaticFiles/src目錄。接下來我們分別探究這三個中間件的相關(guān)代碼,來揭開靜態(tài)文件處理的神秘面紗。
UseStaticFiles
UseStaticFiles中間件使我們處理靜態(tài)文件時最常使用的中間件,因?yàn)橹挥虚_啟了這個中間件我們才能使用靜態(tài)文件,比如在使用MVC開發(fā)的時候需要私用js css html等文件都需要用到它,使用的方式也比較簡單
//使用默認(rèn)路徑,即wwwroot app.UseStaticFiles(); //或自定義讀取路徑 var fileProvider = new PhysicalFileProvider($"{env.ContentRootPath}/staticfiles"); app.UseStaticFiles(new StaticFileOptions { RequestPath="/staticfiles", FileProvider = fileProvider });
我們直接找到中間件的注冊類StaticFileExtensions[點(diǎn)擊查看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是自定義使用靜態(tài)文件時的配置信息類,接下來我們大致看一下具體包含哪些配置項(xiàng)[點(diǎn)擊查看StaticFileOptions源碼]
public class StaticFileOptions : SharedOptionsBase { public StaticFileOptions() : this(new SharedOptions()) { } public StaticFileOptions(SharedOptions sharedOptions) : base(sharedOptions) { OnPrepareResponse = _ => { }; } /// <summary> /// 文件類型提供程序,也就是我們常用的文件名對應(yīng)MimeType的對應(yīng)關(guān)系 /// </summary> public IContentTypeProvider ContentTypeProvider { get; set; } /// <summary> /// 設(shè)置該路徑下默認(rèn)文件輸出類型 /// </summary> public string DefaultContentType { get; set; } public bool ServeUnknownFileTypes { get; set; } /// <summary> /// 文件壓縮方式 /// </summary> public HttpsCompressionMode HttpsCompression { get; set; } = HttpsCompressionMode.Compress; /// <summary> /// 準(zhǔn)備輸出之前可以做一些自定義操作 /// </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中如果需要訪問文件相關(guān)操作可使用FileProvider文件提供程序獲取文件相關(guān)信息 /// </summary> public IFileProvider FileProvider { get { return SharedOptions.FileProvider; } set { SharedOptions.FileProvider = value; } } }
我們自定義靜態(tài)文件訪問時,最常用到的就是RequestPath和FileProvider,一個設(shè)置請求路徑信息,一個設(shè)置讀取文件信息。如果需要自定義MimeType映射關(guān)系可通過ContentTypeProvider自定義設(shè)置映射關(guān)系
var provider = new FileExtensionContentTypeProvider(); provider.Mappings[".myapp"] = "application/x-msdownload"; provider.Mappings[".htm3"] = "text/html"; app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = provider, //可以在輸出之前設(shè)置輸出相關(guān) OnPrepareResponse = ctx => { ctx.Context.Response.Headers.Append("Cache-Control", $"public, max-age=3600"); } });
接下來我們步入正題直接查看StaticFileMiddleware中間件的代碼[點(diǎn)擊查看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; //設(shè)置文件類型提供程序 _contentTypeProvider = options.Value.ContentTypeProvider ?? new FileExtensionContentTypeProvider(); //文件提供程序 _fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv); //匹配路徑 _matchUrl = _options.RequestPath; _logger = loggerFactory.CreateLogger<StaticFileMiddleware>(); } public Task Invoke(HttpContext context) { //判斷是夠獲取到終結(jié)點(diǎn)信息,這也就是為什么我們使用UseStaticFiles要在UseRouting之前 if (!ValidateNoEndpoint(context)) { } //判斷HttpMethod,只能是Get和Head操作 else if (!ValidateMethod(context)) { } //判斷請求路徑是否存在 else if (!ValidatePath(context, _matchUrl, out var subPath)) { } //根據(jù)請求文件名稱判斷是否可以匹配到對應(yīng)的MimeType,如果匹配到則返回contentType else if (!LookupContentType(_contentTypeProvider, _options, subPath, out var contentType)) { } else { //執(zhí)行靜態(tài)文件操作 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 { //靜態(tài)文件處理 return fileContext.ServeStaticFile(context, _next); } return _next(context); } }
關(guān)于FileExtensionContentTypeProvider這里就不作講解了,主要是承載文件擴(kuò)展名和MimeType的映射關(guān)系代碼不復(fù)雜,但是映射關(guān)系比較多,有興趣的可以自行查看FileExtensionContentTypeProvider源碼,通過上面我們可以看到,最終執(zhí)行文件相關(guān)操作的是StaticFileContext類[點(diǎn)擊查看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() { //判斷根據(jù)請求路徑是否可以獲取到文件信息 _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標(biāo)識 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.準(zhǔn)備輸出相關(guān)Header,主要是獲取和輸出靜態(tài)文件輸出緩存相關(guān)的內(nèi)容 //2.我們之前提到的OnPrepareResponse也是在這里執(zhí)行的 ComprehendRequestHeaders(); //根據(jù)ComprehendRequestHeaders方法獲取到的文件狀態(tài)進(jìn)行判斷 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>(); //判斷是否設(shè)置過輸出特征操作相關(guān),比如是否啟動輸出壓縮,或者自定義的輸出處理比如輸出加密等等 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; } //計(jì)算range相關(guān)header數(shù)據(jù) ResponseHeaders.ContentRange = ComputeContentRange(_range, out var start, out var length); _response.ContentLength = length; //設(shè)置輸出壓縮相關(guān)header SetCompressionMode(); ApplyResponseHeaders(Constants.Status206PartialContent); string physicalPath = _fileInfo.PhysicalPath; var sendFile = _context.Features.Get<IHttpResponseBodyFeature>(); //判斷是否設(shè)置過輸出特征操作相關(guān),比如是否啟動輸出壓縮,或者自定義的輸出處理比如輸出加密等等 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); //設(shè)置文件輸出起始位置和讀取長度 await StreamCopyOperation.CopyToAsync(readStream, _response.Body, length, _context.RequestAborted); } } catch (OperationCanceledException ex) { _context.Abort(); } } }
關(guān)的讀取設(shè)置和處理,其此次是針對正常返回和分段返回的情況,在返回之前判斷是否有對輸出做特殊處理的情況,比如輸出壓縮或者自定義的其他輸出操作的IHttpResponseBodyFeature,分段返回和正常返回相比主要是多了一部分關(guān)于Http頭Content-Range相關(guān)的設(shè)置,對于讀取本身其實(shí)只是讀取的起始位置和讀取長度的差別。
UseDirectoryBrowser
目錄瀏覽允許在指定目錄中列出目錄里的文件及子目錄。出于安全方面考慮默認(rèn)情況下是關(guān)閉的可以通過UseDirectoryBrowser中間件開啟指定目錄瀏覽功能。通常情況下我們會這樣使用
//啟用默認(rèn)目錄瀏覽,即wwwroot app.UseDirectoryBrowser(); //或自定義指定目錄瀏覽 var fileProvider = new PhysicalFileProvider($"{env.ContentRootPath}/MyImages"); app.UseDirectoryBrowser(new DirectoryBrowserOptions { RequestPath = "/MyImages", FileProvider = fileProvider });
開啟之后當(dāng)我們訪問https://
/MyImages地址的時候?qū)故救缦滦Ч?,通過一個表格展示目錄里的文件信息等
找
到中間件注冊類[點(diǎn)擊查看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傳遞了哪些信息[點(diǎn)擊查看DirectoryBrowserOptions源碼]
public class DirectoryBrowserOptions : SharedOptionsBase { public DirectoryBrowserOptions() : this(new SharedOptions()) { } public DirectoryBrowserOptions(SharedOptions sharedOptions) : base(sharedOptions) { } /// <summary> /// 目錄格式化提供,默認(rèn)是提供表格的形式展示,課自定義 /// </summary> public IDirectoryFormatter Formatter { get; set; } }
無獨(dú)有偶這個類和StaticFileOptions一樣也是集成自SharedOptionsBase類,唯一多了IDirectoryFormatter操作,通過它我們可以自定義展示到頁面的輸出形式,接下來我們就重點(diǎn)看下DirectoryBrowserMiddleware中間件的實(shí)現(xiàn)
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; //默認(rèn)是提供默認(rèn)目錄的訪問程序 _fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv); //默認(rèn)傳遞的是HtmlDirectoryFormatter類型,也就是我們看到的輸出表格的頁面 _formatter = options.Value.Formatter ?? new HtmlDirectoryFormatter(encoder); _matchUrl = _options.RequestPath; } public Task Invoke(HttpContext context) { //1.IsGetOrHeadMethod判斷是否為Get或Head請求 //2.TryMatchPath判斷請求的路徑和設(shè)置的路徑是否可以匹配的上 //3.TryGetDirectoryInfo判斷根據(jù)匹配出來的路徑能否查找到真實(shí)的物理路徑 if (context.GetEndpoint() == null && Helpers.IsGetOrHeadMethod(context.Request.Method) && Helpers.TryMatchPath(context, _matchUrl, forDirectory: true, subpath: out var subpath) && TryGetDirectoryInfo(subpath, out var contents)) { //判斷請求路徑是否是/為結(jié)尾 if (!Helpers.PathEndsInSlash(context.Request.Path)) { //如果不是以斜線結(jié)尾則重定向(個人感覺直接在服務(wù)端重定向就可以了,為啥還要返回瀏覽器在請求一次) 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; } //返回展示目錄的內(nèi)容 return _formatter.GenerateContentAsync(context, contents); } return _next(context); } /// <summary> /// 根據(jù)請求路徑匹配到物理路徑信息是否存在,存在則返回路徑信息 /// </summary> private bool TryGetDirectoryInfo(PathString subpath, out IDirectoryContents contents) { contents = _fileProvider.GetDirectoryContents(subpath.Value); return contents.Exists; } }
這個操作相對簡單了許多,主要就是判斷請求路徑能否和預(yù)設(shè)置的路徑匹配的到,如果匹配到則獲取可以操作當(dāng)前目錄內(nèi)容IDirectoryContents然后通過IDirectoryFormatter輸出如何展示目錄內(nèi)容,關(guān)于IDirectoryFormatter的默認(rèn)實(shí)現(xiàn)類HtmlDirectoryFormatter這里就不展示里面的代碼了,邏輯非常的加單就是拼接成table的html代碼然后輸出,有興趣的同學(xué)可自行查看源碼[點(diǎn)擊查看HtmlDirectoryFormatter源碼],如果自定義的話規(guī)則也非常簡單,主要看你想輸出啥
public class TreeDirectoryFormatter: IDirectoryFormatter { public Task GenerateContentAsync(HttpContext context, IEnumerable<IFileInfo> contents) { //遍歷contents實(shí)現(xiàn)你想展示的方式 } }
然后在UseDirectoryBrowser的時候給Formatter賦值即可
app.UseDirectoryBrowser(new DirectoryBrowserOptions { Formatter = new TreeDirectoryFormatter() });
UseDefaultFiles
很多時候出于安全考慮或者其他原因我們想在訪問某個目錄的時候返回一個默認(rèn)的頁面或展示,這個事實(shí)我們就需要使用UseDefaultFiles中間件,當(dāng)我們配置了這個中間件,如果命中了配置路徑,那么會直接返回默認(rèn)的頁面信息,簡單使用方式如下
//wwwroot目錄訪問展示默認(rèn)文件 app.UseDefaultFiles(); //或自定義目錄默認(rèn)展示文件 var fileProvider = new PhysicalFileProvider($"{env.ContentRootPath}/staticfiles"); app.UseDefaultFiles(new DefaultFilesOptions { RequestPath = "/staticfiles", FileProvider = fileProvider });
老規(guī)矩,我們查看下注冊UseDefaultFiles的源碼[點(diǎn)擊查看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是一樣,最終都是調(diào)用傳遞DefaultFilesOptions的方法,我們查看一下DefaultFilesOptions的大致實(shí)現(xiàn)[點(diǎn)擊查看源碼]
public class DefaultFilesOptions : SharedOptionsBase { public DefaultFilesOptions() : this(new SharedOptions()) { } public DefaultFilesOptions(SharedOptions sharedOptions) : base(sharedOptions) { //系統(tǒng)提供的默認(rèn)頁面的名稱 DefaultFileNames = new List<string> { "default.htm", "default.html", "index.htm", "index.html", }; } /// <summary> /// 通過這個屬性可以配置默認(rèn)文件名稱 /// </summary> public IList<string> DefaultFileNames { get; set; } }
和之前的方法如出一轍,都是繼承自SharedOptionsBase,通過DefaultFileNames我們可以配置默認(rèn)文件的名稱,默認(rèn)是default.html/htm和index.html/htm。我們直接查看中間件DefaultFilesMiddleware的源碼[點(diǎn)擊查看源碼]
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判斷請求的路徑和設(shè)置的路徑是否可以匹配的上 if (context.GetEndpoint() == null && Helpers.IsGetOrHeadMethod(context.Request.Method) && Helpers.TryMatchPath(context, _matchUrl, forDirectory: true, subpath: out var subpath)) { //根據(jù)匹配路徑獲取物理路徑對應(yīng)的信息 var dirContents = _fileProvider.GetDirectoryContents(subpath.Value); if (dirContents.Exists) { //循環(huán)配置的默認(rèn)文件名稱 for (int matchIndex = 0; matchIndex < _options.DefaultFileNames.Count; matchIndex++) { string defaultFile = _options.DefaultFileNames[matchIndex]; //匹配配置的啟用默認(rèn)文件的路徑+遍歷到的默認(rèn)文件名稱的路徑是否存在 var file = _fileProvider.GetFileInfo(subpath.Value + defaultFile); if (file.Exists) { //判斷請求路徑是否已"/"結(jié)尾,如果不是則從定向(這個點(diǎn)個人感覺可以改進(jìn)) 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; } //如果匹配的上,則將配置的啟用默認(rèn)文件的路徑+遍歷到的默認(rèn)文件名稱的路徑組合成新的Path交給_next(context) //比如將組成類似這種路徑/staticfiles/index.html向下傳遞 context.Request.Path = new PathString(context.Request.Path.Value + defaultFile); break; } } } } return _next(context); } }
這個中間件的實(shí)現(xiàn)思路也非常簡單主要的工作就是,匹配配置的啟用默認(rèn)文件的路徑+遍歷到的默認(rèn)文件名稱的路徑是否存在,如果匹配的上,則將配置的啟用默認(rèn)文件的路徑+遍歷到的默認(rèn)文件名稱的路徑組合成新的Path(比如/staticfiles/index.html)交給后續(xù)的中間件去處理。這里值得注意的是UseDefaultFiles 必須要配合UseStaticFiles一起使用,而且注冊位置要出現(xiàn)在UseStaticFiles之上。這也是為什么UseDefaultFiles只需要匹配到默認(rèn)文件所在的路徑并重新賦值給context.Request.Path既可的原因。
當(dāng)然我們也可以自定義默認(rèn)文件的名稱,因?yàn)橹灰芷ヅ涞牡骄唧w的文件既可
var defaultFilesOptions = new DefaultFilesOptions { RequestPath = "/staticfiles", FileProvider = fileProvider }; //我們可以清除掉系統(tǒng)默認(rèn)的默認(rèn)文件名稱 defaultFilesOptions.DefaultFileNames.Clear(); defaultFilesOptions.DefaultFileNames.Add("mydefault.html"); app.UseDefaultFiles(defaultFilesOptions);
總結(jié)
通過上面的介紹我們已經(jīng)大致了解了靜態(tài)文件處理的大致實(shí)現(xiàn)思路,相對于傳統(tǒng)的Asp.Net程序我們可以更方便的處理靜態(tài)文件信息,但是思路是一致的,IIS會優(yōu)先處理靜態(tài)文件,如果靜態(tài)文件處理不了的情況才會交給程序去處理。ASP.NET Core也不例外,通過我們查看中間件源碼里的context.GetEndpoint()==null判斷可以知道,ASP.NET Core更希望我們優(yōu)先去處理靜態(tài)文件,而不是任意出現(xiàn)在其他位置去處理。關(guān)于ASP.NET Core處理靜態(tài)文件的講解就到這里,歡迎評論區(qū)探討交流。
到此這篇關(guān)于淺談ASP.NET Core靜態(tài)文件處理源碼探究的文章就介紹到這了,更多相關(guān)ASP.NET Core靜態(tài)文件處理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
ASP.NET實(shí)現(xiàn)個人信息注冊頁面并跳轉(zhuǎn)顯示
這篇文章主要介紹了ASP.NET實(shí)現(xiàn)個人信息注冊頁面并跳轉(zhuǎn)顯示的相關(guān)資料,本文圖文并茂給大家介紹的非常詳細(xì),需要的朋友可以參考下2016-11-11巧用ASP.NET預(yù)編譯Web應(yīng)用程序規(guī)避調(diào)用延遲的方法
ASP.NET 1.x的開發(fā)人員常常聽到用戶抱怨首次調(diào)用應(yīng)用程序的時候會碰到初始化延遲。畢竟,初次請求會引發(fā)一個系列過程,包括運(yùn)行庫初始化、分析、把ASPX頁面編譯成中間語言、把方法即時編譯成本地代碼等等。2011-08-08EF使用數(shù)據(jù)注解特性創(chuàng)建表結(jié)構(gòu)
本文詳細(xì)講解了EF使用數(shù)據(jù)注解特性創(chuàng)建表結(jié)構(gòu)的方法,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-03-03獲取asp.net服務(wù)器控件的客戶端ID和Name的實(shí)現(xiàn)方法
下面小編就為大家?guī)硪黄@取asp.net服務(wù)器控件的客戶端ID和Name的實(shí)現(xiàn)方法。小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-05-05asp.net實(shí)現(xiàn)固定GridView標(biāo)題欄的方法(凍結(jié)列功能)
這篇文章主要介紹了asp.net實(shí)現(xiàn)固定GridView標(biāo)題欄的方法,即凍結(jié)列功能,涉及GridView結(jié)合前端js操作數(shù)據(jù)顯示的相關(guān)技巧,需要的朋友可以參考下2016-06-06