注:本文隸屬於《理解ASP.NET Core》系列文章,請檢視置頂部落格或點選此處檢視全文目錄
提供靜態檔案
靜態檔案預設存放在 Web根目錄(Web Root) 中,路徑為 專案根目錄(Content Root) 下的wwwroot
資料夾,也就是{Content Root}/wwwroot
。
如果你呼叫了Host.CreateDefaultBuilder
方法,那麼在該方法中,會通過UseContentRoot
方法,將程式當前工作目錄(Directory.GetCurrentDirectory()
)設定為專案根目錄。具體可以檢視主機一節。
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
當然,你也可以通過UseWebRoot
擴充套件方法將預設的路徑{Content Root}/wwwroot
修改為自定義目錄(不過,你改它幹啥捏?)
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
// 配置靜態資源的根目錄為 mywwwroot, 預設為 wwwroot
webBuilder.UseWebRoot("mywwwroot");
webBuilder.UseStartup<Startup>();
});
為了方便,後面均使用 wwwroot 來表示Web根目錄
首先,我們先在 wwwroot 資料夾下建立一個名為 config.json 的檔案,內容隨便填寫
注意,確保 wwwroot 下的檔案的屬性為“如果較新則複製”或“始終複製”。
接著,我們通過UseStaticFiles
擴充套件方法,來註冊靜態檔案中介軟體StaticFileMiddleware
:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseStaticFiles();
}
現在,嘗試一下通過 http://localhost:5000/config.json 來獲取 wwwroot/config.json 的檔案內容吧
如果你的專案中啟用SwaggerUI,那麼你會發現,即使你沒有手動通過呼叫
UseStaticFiles()
新增中介軟體,你也可以訪問 wwwroot 檔案下的檔案,這是因為 SwaggerUIMiddleware 中使用了 StaticFileMiddleware
提供Web根目錄之外的檔案
上面我們已經能夠提供 wwwroot 資料夾內的靜態檔案了,那如果我們的檔案不在 wwwroot 資料夾內,那如何提供呢?
很簡單,我們可以針對StaticFileMiddleware
中介軟體進行一些額外的配置,瞭解一下配置項:
public abstract class SharedOptionsBase
{
// 用於自定義靜態檔案的相對請求路徑
public PathString RequestPath { get; set; }
// 檔案提供程式
public IFileProvider FileProvider { get; set; }
// 是否補全路徑末尾斜槓“/”,並重定向
public bool RedirectToAppendTrailingSlash { get; set; }
}
public class StaticFileOptions : SharedOptionsBase
{
// ContentType提供程式
public IContentTypeProvider ContentTypeProvider { get; set; }
// 如果 ContentTypeProvider 無法識別檔案型別,是否仍作為預設檔案型別提供
public bool ServeUnknownFileTypes { get; set; }
// 當 ServeUnknownFileTypes = true 時,若出現無法識別的檔案型別,則將該屬性的值作為此檔案的型別
// 當 ServeUnknownFileTypes = true 時,必須賦值該屬性,才會生效
public string DefaultContentType { get; set; }
// 當註冊了HTTP響應壓縮中介軟體時,是否對檔案進行壓縮
public HttpsCompressionMode HttpsCompression { get; set; } = HttpsCompressionMode.Compress;
// 在HTTP響應的 Status Code 和 Headers 設定完畢之後,Body 寫入之前進行呼叫
// 用於新增或更改 Headers
public Action<StaticFileResponseContext> OnPrepareResponse { get; set; }
}
假設我們現在有這樣一個檔案目錄結構:
- wwwroot
- config.json
- files
- file.json
然後,除了用於提供 wwwroot 靜態檔案的中介軟體外,我們還要註冊一個用於提供 files 靜態檔案的中介軟體:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// 提供 wwwroot 靜態檔案
app.UseStaticFiles();
// 提供 files 靜態檔案
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, "files")),
// 指定檔案的訪問路徑,允許與 FileProvider 中的資料夾不同名
// 如果不指定,則可通過 http://localhost:5000/file.json 獲取,
// 如果指定,則需要通過 http://localhost:5000/files/file.json 獲取
RequestPath = "/files",
OnPrepareResponse = ctx =>
{
// 配置前端快取 600s(為了後續示例的良好執行,建議先不要配置該Header)
ctx.Context.Response.Headers.Add(HeaderNames.CacheControl, "public,max-age=600");
}
});
}
建議將公開訪問的檔案放置到 wwwroot 目錄下,而將需要授權訪問的檔案放置到其他目錄下(在呼叫
UseAuthorization
之後呼叫UseStaticFiles
並指定檔案目錄)
提供目錄瀏覽
上面,我們可以通過Url訪問某一個檔案的內容,而通過UseDirectoryBrowser
,註冊DirectoryBrowserMiddleware
中介軟體,可以讓我們在瀏覽器中以目錄的形式來訪問檔案列表。
另外,DirectoryBrowserMiddleware
中介軟體的可配置項除了SharedOptionsBase
中的之外,還有一個Formatter
,用於自定義目錄檢視。
public class DirectoryBrowserOptions : SharedOptionsBase
{
public IDirectoryFormatter Formatter { get; set; }
}
示例如下:
public void ConfigureServices(IServiceCollection services)
{
services.AddDirectoryBrowser();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// 通過 http://localhost:5000,即可訪問 wwwroot 目錄
app.UseDirectoryBrowser();
// 通過 http://localhost:5000/files,即可訪問 files 目錄
app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
// 如果指定了沒有在 UseStaticFiles 中提供的檔案目錄,雖然可以瀏覽檔案列表,但是無法訪問檔案內容
FileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, "files")),
// 這裡一定要和 StaticFileOptions 中的 RequestPath 一致,否則會無法訪問檔案
RequestPath = "/files"
});
}
提供預設頁
通過UseDefaultFiles
,註冊DefaultFilesMiddleware
中介軟體,允許在訪問靜態檔案、但未提供檔名的情況下(即傳入的是一個目錄的路徑),提供預設頁的展示。
注意:
UseDefaultFiles
必須在UseStaticFiles
之前進行呼叫。因為DefaultFilesMiddleware
僅僅負責重寫Url,實際上預設頁檔案,仍然是通過StaticFilesMiddleware
來提供的。
預設情況下,該中介軟體會按照順序搜尋檔案目錄下的HTML頁面檔案:
- default.htm
- default.html
- index.htm
- index.html
另外,DefaultFilesMiddleware
中介軟體的可配置項除了SharedOptionsBase
中的之外,還有一個DefaultFileNames
,是個列表,用於自定義預設頁的檔名,裡面的預設值就是上面提到的4個檔名。
public class DefaultFilesOptions : SharedOptionsBase
{
public IList<string> DefaultFileNames { get; set; }
}
示例如下:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// 會去 wwwroot 尋找 default.htm 、default.html 、index.htm 或 index.html 檔案作為預設頁
app.UseDefaultFiles();
// 設定 files 目錄的預設頁
var defaultFilesOptions = new DefaultFilesOptions();
defaultFilesOptions.DefaultFileNames.Clear();
// 指定預設頁名稱
defaultFilesOptions.DefaultFileNames.Add("index1.html");
// 指定請求路徑
defaultFilesOptions.RequestPath = "/files";
// 指定預設頁所在的目錄
defaultFilesOptions.FileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, "files"));
app.UseDefaultFiles(defaultFilesOptions);
}
UseFileServer
UseFileServer
整合了UseStaticFiles
、UseDefaultFiles
和UseDirectoryBrowser
的功能,用起來方便一些,也是我們專案中使用的首選擴充套件方法。
先看一下FileServerOptions
:
public class FileServerOptions : SharedOptionsBase
{
public FileServerOptions()
: base(new SharedOptions())
{
StaticFileOptions = new StaticFileOptions(SharedOptions);
DirectoryBrowserOptions = new DirectoryBrowserOptions(SharedOptions);
DefaultFilesOptions = new DefaultFilesOptions(SharedOptions);
EnableDefaultFiles = true;
}
public StaticFileOptions StaticFileOptions { get; private set; }
public DirectoryBrowserOptions DirectoryBrowserOptions { get; private set; }
public DefaultFilesOptions DefaultFilesOptions { get; private set; }
// 預設禁用目錄瀏覽
public bool EnableDirectoryBrowsing { get; set; }
// 預設啟用預設頁(在建構函式中初始化的)
public bool EnableDefaultFiles { get; set; }
}
可以看到,FileServerOptions
包含了StaticFileOptions
、DirectoryBrowserOptions
和DefaultFilesOptions
三個選項,可以針對StaticFileMiddleware
、DirectoryBrowserMiddleware
和DefaultFilesMiddleware
進行自定義配置。另外,其預設啟用了靜態檔案和預設頁,禁用了目錄瀏覽。
下面舉個例子熟悉一下:
假設檔案目錄:
- files
- images
- 1.jpg
- file.json
- myindex.html
- images
public void ConfigureServices(IServiceCollection services)
{
// 如果將 EnableDirectoryBrowsing 設為 true,記得註冊服務
services.AddDirectoryBrowser();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// 啟用 StaticFileMiddleware
// 啟用 DefaultFilesMiddleware
// 禁用 DirectoryBrowserMiddleware
// 預設指向 wwwroot
app.UseFileServer();
// 針對 files 資料夾配置
var fileServerOptions = new FileServerOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, "files")),
RequestPath = "/files",
EnableDirectoryBrowsing = true
};
fileServerOptions.StaticFileOptions.OnPrepareResponse = ctx =>
{
// 配置快取600s
ctx.Context.Response.Headers.Add(HeaderNames.CacheControl, "public,max-age=600");
};
fileServerOptions.DefaultFilesOptions.DefaultFileNames.Clear();
fileServerOptions.DefaultFilesOptions.DefaultFileNames.Add("myindex.html");
app.UseFileServer(fileServerOptions);
}
當訪問 http://localhost:5000/files 時,由於在DefaultFilesOptions.DefaultFileNames
中新增了檔名myindex.html
,所以可以找到預設頁,此時會顯示預設頁的內容。
假如我們沒有在DefaultFilesOptions.DefaultFileNames
中新增檔名myindex.html
,那麼便找不到預設頁,但由於啟用了DirectoryBrowsing
,所以此時會展示檔案列表。
核心配置項
FileProvider
上面我們已經見過PhysicalFileProvider
了,它僅僅是眾多檔案提供程式中的一種。所有的檔案提供程式均實現了IFileProvider
介面:
public interface IFileProvider
{
// 獲取給定路徑的目錄資訊,可列舉該目錄中的所有檔案
IDirectoryContents GetDirectoryContents(string subpath);
// 獲取給定路徑的檔案資訊
IFileInfo GetFileInfo(string subpath);
// 建立指定 filter 的 ChangeToken
IChangeToken Watch(string filter);
}
public interface IDirectoryContents : IEnumerable<IFileInfo>, IEnumerable
{
bool Exists { get; }
}
public interface IFileInfo
{
bool Exists { get; }
bool IsDirectory { get; }
DateTimeOffset LastModified { get; }
// 位元組(bytes)長度
// 如果是目錄或檔案不存在,則是 -1
long Length { get; }
// 目錄或檔名,純檔名,不包括路徑
string Name { get; }
// 檔案路徑,包含檔名
// 如果檔案無法直接訪問,則返回 null
string PhysicalPath { get; }
// 建立該檔案只讀流
Stream CreateReadStream();
}
常用的檔案提供程式有以下三種:
- PhysicalFileProvider
- ManifestEmbeddedFileProvider
- CompositeFileProvider
glob模式
在介紹這三種檔案提供程式之前,先說一下glob模式
,即萬用字元模式
。兩個萬用字元分別是*
和**
。
*
:匹配當前目錄層級(不包含子目錄)下的任何內容、任何檔名或任何副檔名,可以通過/
、\
和.
進行分隔。**
:匹配目錄多層級(包含子目錄)的任何內容,用於遞迴匹配多層級目錄的多個檔案。
PhysicalFileProvider
PhysicalFileProvider
用於提供物理檔案系統的訪問。該提供程式需要將檔案路徑範圍限定在一個目錄及其子目錄中,不能訪問目錄外部的內容。
當例項化該檔案提供程式時,需要提供一個絕對的目錄路徑,作為檔案目錄的root。
PhysicalFileProvider
目錄或檔案路徑不支援glob(萬用字元)模式。
ManifestEmbeddedFileProvider
ManifestEmbeddedFileProvider
用於提供嵌入在程式集中的檔案的訪問。
可能你對這個嵌入檔案比較陌生,沒關係,請按照下面的步驟來:
- 安裝Nuget包:
Install-Package Microsoft.Extensions.FileProviders.Embedded
- 編輯
.csproj
檔案:- 新增
<GenerateEmbeddedFilesManifest>
,並設定為true
- 使用
<EmbeddedResource>
新增要嵌入的檔案
- 新增
以下是 .csproj 檔案的示例:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="5.0.11" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="files\**" />
</ItemGroup>
</Project>
現在我們通過ManifestEmbeddedFileProvider
來提供嵌入到程式集的 files 目錄下檔案的訪問:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
var fileServerOptions = new FileServerOptions();
fileServerOptions.StaticFileOptions.FileProvider = new ManifestEmbeddedFileProvider(Assembly.GetExecutingAssembly(), "/files");
fileServerOptions.StaticFileOptions.RequestPath = "/files";
app.UseFileServer(fileServerOptions);
}
現在,你可以通過 http://localhost:5000/files/file.json 來訪問檔案了。
CompositeFileProvider
CompositeFileProvider
用於將多種檔案提供程式進行整合。
如:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
var fileServerOptions = new FileServerOptions();
var fileProvider = new CompositeFileProvider(
env.WebRootFileProvider,
new ManifestEmbeddedFileProvider(Assembly.GetExecutingAssembly(), "/files")
);
fileServerOptions.StaticFileOptions.FileProvider = fileProvider;
fileServerOptions.StaticFileOptions.RequestPath = "/composite";
app.UseFileServer(fileServerOptions);
}
現在,你可以通過 http://localhost:5000/composite/file.json 來訪問檔案了。
ContentTypeProvider
Http請求頭中的Content-Type
大家一定很熟悉,ContentTypeProvider
就是用來提供副檔名和MIME型別對映關係的。
若我們沒有顯示指定ContentTypeProvider
,則框架預設使用FileExtensionContentTypeProvider
,其實現了介面IContentTypeProvider
:
public interface IContentTypeProvider
{
// 嘗試根據檔案路徑,獲取對應的 MIME 型別
bool TryGetContentType(string subpath, out string contentType);
}
public class FileExtensionContentTypeProvider : IContentTypeProvider
{
public FileExtensionContentTypeProvider()
: this(new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
// ...此處省略一萬字
}
{
}
public FileExtensionContentTypeProvider(IDictionary<string, string> mapping)
{
Mappings = mapping;
}
public IDictionary<string, string> Mappings { get; private set; }
public bool TryGetContentType(string subpath, out string contentType)
{
string extension = GetExtension(subpath);
if (extension == null)
{
contentType = null;
return false;
}
return Mappings.TryGetValue(extension, out contentType);
}
private static string GetExtension(string path)
{
// 沒有使用 Path.GetExtension() 的原因是:當路徑中存在無效字元時,其會丟擲異常,而這裡不應丟擲異常。
if (string.IsNullOrWhiteSpace(path))
{
return null;
}
int index = path.LastIndexOf('.');
if (index < 0)
{
return null;
}
return path.Substring(index);
}
}
在FileExtensionContentTypeProvider
的無參建構函式中,預設新增了380種已知的副檔名和MIME型別的對映,存放在Mappings
屬性中。你也可以新增自定義的對映,或移除不想要的對映。
核心中介軟體
StaticFileMiddleware
通過UseStaticFiles
擴充套件方法,可以方便的註冊StaticFileMiddleware
中介軟體:
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));
}
}
緊接著檢視StaticFileMiddleware
的Invoke
方法:
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,則預設使用 FileExtensionContentTypeProvider
_contentTypeProvider = _options.ContentTypeProvider ?? new FileExtensionContentTypeProvider();
// 若未指定 FileProvider,則預設使用 hostingEnv.WebRootFileProvider
_fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv);
_matchUrl = _options.RequestPath;
_logger = loggerFactory.CreateLogger<StaticFileMiddleware>();
}
public Task Invoke(HttpContext context)
{
// 若已匹配到 Endpoint,則跳過
if (!ValidateNoEndpoint(context))
{
_logger.EndpointMatched();
}
// 若HTTP請求方法不是 Get,也不是 Head,則跳過
else if (!ValidateMethod(context))
{
_logger.RequestMethodNotSupported(context.Request.Method);
}
// 如果請求路徑不匹配,則跳過
else if (!ValidatePath(context, _matchUrl, out var subPath))
{
_logger.PathMismatch(subPath);
}
// 如果 ContentType 不受支援,則跳過
else if (!LookupContentType(_contentTypeProvider, _options, subPath, out var contentType))
{
_logger.FileTypeNotSupported(subPath);
}
else
{
// 嘗試提供靜態檔案
return TryServeStaticFile(context, contentType, subPath);
}
return _next(context);
}
private static bool ValidateNoEndpoint(HttpContext context) => context.GetEndpoint() == null;
private static bool ValidateMethod(HttpContext context) => Helpers.IsGetOrHeadMethod(context.Request.Method);
internal static bool ValidatePath(HttpContext context, PathString matchUrl, out PathString subPath) => Helpers.TryMatchPath(context, matchUrl, forDirectory: false, out subPath);
internal static bool LookupContentType(IContentTypeProvider contentTypeProvider, StaticFileOptions options, PathString subPath, out string contentType)
{
// 檢視 Provider 中是否支援該 ContentType
if (contentTypeProvider.TryGetContentType(subPath.Value, out contentType))
{
return true;
}
// 如果提供未知檔案型別,則將其設定為預設 ContentType
if (options.ServeUnknownFileTypes)
{
contentType = options.DefaultContentType;
return true;
}
return false;
}
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);
}
}
DirectoryBrowserMiddleware
通過UseDirectoryBrowser
擴充套件方法,可以方便的註冊DirectoryBrowserMiddleware
中介軟體:
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));
}
}
緊接著檢視DirectoryBrowserMiddleware
的Invoke
方法:
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,則預設使用 hostingEnv.WebRootFileProvider
_fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv);
_formatter = _options.Formatter ?? new HtmlDirectoryFormatter(encoder);
_matchUrl = _options.RequestPath;
}
public Task Invoke(HttpContext context)
{
// 若已匹配到 Endpoint,則跳過
// 若HTTP請求方法不是 Get,也不是 Head,則跳過
// 如果請求路徑不匹配,則跳過
// 若檔案目錄不存在,則跳過
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 (_options.RedirectToAppendTrailingSlash && !Helpers.PathEndsInSlash(context.Request.Path))
{
Helpers.RedirectToPathWithSlash(context);
return Task.CompletedTask;
}
// 生成檔案瀏覽檢視
return _formatter.GenerateContentAsync(context, contents);
}
return _next(context);
}
private bool TryGetDirectoryInfo(PathString subpath, out IDirectoryContents contents)
{
contents = _fileProvider.GetDirectoryContents(subpath.Value);
return contents.Exists;
}
}
DefaultFilesMiddleware
通過UseDefaultFiles
擴充套件方法,可以方便的註冊DefaultFilesMiddleware
中介軟體:
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));
}
}
緊接著檢視DefaultFilesMiddleware
的Invoke
方法:
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,則預設使用 hostingEnv.WebRootFileProvider
_fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv);
_matchUrl = _options.RequestPath;
}
public Task Invoke(HttpContext context)
{
// 若已匹配到 Endpoint,則跳過
// 若HTTP請求方法不是 Get,也不是 Head,則跳過
// 如果請求路徑不匹配,則跳過
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 (_options.RedirectToAppendTrailingSlash && !Helpers.PathEndsInSlash(context.Request.Path))
{
Helpers.RedirectToPathWithSlash(context);
return Task.CompletedTask;
}
// 重寫為預設頁的Url,後續通過 StaticFileMiddleware 提供該頁面
context.Request.Path = new PathString(Helpers.GetPathValueWithSlash(context.Request.Path) + defaultFile);
break;
}
}
}
}
return _next(context);
}
}
FileServer
FileServer並不是某個具體的中介軟體,它的實現還是依賴了StaticFileMiddleware
、DirectoryBrowserMiddleware
和DefaultFilesMiddleware
這3箇中介軟體。不過,我們可以看一下UseFileServer
裡的邏輯:
public static class FileServerExtensions
{
public static IApplicationBuilder UseFileServer(this IApplicationBuilder app)
{
return app.UseFileServer(new FileServerOptions());
}
public static IApplicationBuilder UseFileServer(this IApplicationBuilder app, bool enableDirectoryBrowsing)
{
return app.UseFileServer(new FileServerOptions
{
EnableDirectoryBrowsing = enableDirectoryBrowsing
});
}
public static IApplicationBuilder UseFileServer(this IApplicationBuilder app, string requestPath)
{
return app.UseFileServer(new FileServerOptions
{
RequestPath = new PathString(requestPath)
});
}
public static IApplicationBuilder UseFileServer(this IApplicationBuilder app, FileServerOptions options)
{
// 啟用預設頁
if (options.EnableDefaultFiles)
{
app.UseDefaultFiles(options.DefaultFilesOptions);
}
// 啟用目錄瀏覽
if (options.EnableDirectoryBrowsing)
{
app.UseDirectoryBrowser(options.DirectoryBrowserOptions);
}
return app.UseStaticFiles(options.StaticFileOptions);
}
}
FileProvider in IWebHostingEnvironment
在介面IHostingEnvironment
中,包含ContentRootFileProvider
和WebRootFileProvider
兩個檔案提供程式。下面我們就看一下他們是如何被初始化的。
internal class GenericWebHostBuilder : IWebHostBuilder, ISupportsStartup, ISupportsUseDefaultServiceProvider
{
private WebHostBuilderContext GetWebHostBuilderContext(HostBuilderContext context)
{
if (!context.Properties.TryGetValue(typeof(WebHostBuilderContext), out var contextVal))
{
var options = new WebHostOptions(context.Configuration, Assembly.GetEntryAssembly()?.GetName().Name);
var webHostBuilderContext = new WebHostBuilderContext
{
Configuration = context.Configuration,
HostingEnvironment = new HostingEnvironment(),
};
// 重點在這裡,看這個 Initialize 方法
webHostBuilderContext.HostingEnvironment.Initialize(context.HostingEnvironment.ContentRootPath, options);
context.Properties[typeof(WebHostBuilderContext)] = webHostBuilderContext;
context.Properties[typeof(WebHostOptions)] = options;
return webHostBuilderContext;
}
var webHostContext = (WebHostBuilderContext)contextVal;
webHostContext.Configuration = context.Configuration;
return webHostContext;
}
}
internal static class HostingEnvironmentExtensions
{
internal static void Initialize(this IWebHostEnvironment hostingEnvironment, string contentRootPath, WebHostOptions options)
{
hostingEnvironment.ApplicationName = options.ApplicationName;
hostingEnvironment.ContentRootPath = contentRootPath;
// 初始化 ContentRootFileProvider
hostingEnvironment.ContentRootFileProvider = new PhysicalFileProvider(hostingEnvironment.ContentRootPath);
var webRoot = options.WebRoot;
if (webRoot == null)
{
// 如果 /wwwroot 目錄存在,則設定為Web根目錄
var wwwroot = Path.Combine(hostingEnvironment.ContentRootPath, "wwwroot");
if (Directory.Exists(wwwroot))
{
hostingEnvironment.WebRootPath = wwwroot;
}
}
else
{
hostingEnvironment.WebRootPath = Path.Combine(hostingEnvironment.ContentRootPath, webRoot);
}
if (!string.IsNullOrEmpty(hostingEnvironment.WebRootPath))
{
hostingEnvironment.WebRootPath = Path.GetFullPath(hostingEnvironment.WebRootPath);
if (!Directory.Exists(hostingEnvironment.WebRootPath))
{
Directory.CreateDirectory(hostingEnvironment.WebRootPath);
}
// 初始化 WebRootFileProvider
hostingEnvironment.WebRootFileProvider = new PhysicalFileProvider(hostingEnvironment.WebRootPath);
}
else
{
hostingEnvironment.WebRootFileProvider = new NullFileProvider();
}
hostingEnvironment.EnvironmentName =
options.Environment ??
hostingEnvironment.EnvironmentName;
}
}
注意
- 使用
UseDirectoryBrowser
和UseStaticFiles
提供檔案瀏覽和訪問時,URL 受大小寫和基礎檔案系統字元的限制。例如,Windows 不區分大小寫,但 macOS 和 Linux 區分大小寫。 - 如果使用 IIS 託管應用,那麼 IIS 自帶的靜態檔案處理器是不工作的,均是使用 ASP.NET Core Module 進行處理的,包括靜態檔案處理。
小結
- 使用
UseFileServer
擴充套件方法提供檔案瀏覽和訪問,其整合了UseStaticFiles
、UseDirectoryBrowser
和UseDefaultFiles
三個中介軟體的功能。UseStaticFiles
:註冊StaticFilesMiddleware
,提供檔案訪問UseDirectoryBrowser
:註冊DirectoryBrowserMiddleware
,提供檔案目錄瀏覽UseDefaultFiles
:註冊DefaultFilesMiddleware
,當Url未指定訪問的檔名時,提供預設頁。
- 檔案提供程式均實現了介面
IFileProvider
,常用的檔案提供程式有以下三種:PhysicalFileProvider
:提供物理檔案系統的訪問ManifestEmbeddedFileProvider
:提供嵌入在程式集中的檔案的訪問CompositeFileProvider
:用於將多種檔案提供程式進行整合。
- 可通過
IWebHostingEnvironment
獲取ContentRootFileProvider
(預設目錄為專案根目錄)和WebRootFileProvider
(預設目錄為Web根目錄)。