ASP.NET Core應用針對靜態檔案請求的處理[4]: DirectoryBrowserMiddleware中介軟體如何呈現目錄結構
和StaticFileMiddleware中介軟體一樣,DirectoryBrowserMiddleware中間本質上還是定義了一個請求地址與某個物理目錄之間的對映關係,而目標目錄體現為一個FileProvider物件。當這個中介軟體接收到匹配的請求後,會根據請求地址解析出對應目錄的相對路徑,並利用這個FileProvider獲取目錄的內容。目錄的內容最終會以一個HTML文件的形式被定義,而此HTML最終會被這個中介軟體作為響應的內容,“目錄瀏覽器”的實現原理就這麼簡單。 [本文已經同步到《ASP.NET Core框架揭祕》之中]
目錄
一、DirectoryBrowserMiddleware
二、DirectoryFormatter
三、具體請求處理邏輯
四、自定義DirectoryFormatter
一、DirectoryBrowserMiddleware
接下來我們來看看DirectoryBrowserMiddleware的定義。如下面的程式碼片段所示,DirectoryBrowserMiddleware的第二個建構函式具有四個引數,其中第二個引數是代表當前執行環境的HostingEnvironment。作為第三個引數的是一個HtmlEncoder物件,當目標目錄被呈現為一個HTML文件的時候,它被用於實現針對HTML的編碼,如果沒有顯式指定(呼叫第一個建構函式),預設的HtmlEncoder(HtmlEncoder.Default)會被使用。至於第四個型別為IOptions<DirectoryBrowserOptions>的引數,則承載了針對DirectoryBrowserMiddleware的配置選項,DirectoryBrowserOptions與前面介紹的StaticFileOptions一樣,它們都是SharedOptionsBase的子類。
1: public class DirectoryBrowserMiddleware
2: {
3: public DirectoryBrowserMiddleware(RequestDelegate next, IHostingEnvironment env, IOptions<DirectoryBrowserOptions> options)
4: public DirectoryBrowserMiddleware(RequestDelegate next, IHostingEnvironment hostingEnv, HtmlEncoder encoder, IOptions<DirectoryBrowserOptions> options);
5: public Task Invoke(HttpContext context);
6: }
7:
8: public class DirectoryBrowserOptions : SharedOptionsBase
9: {
10: public IDirectoryFormatter Formatter { get; set; }
11:
12: public DirectoryBrowserOptions();
13: public DirectoryBrowserOptions(SharedOptions sharedOptions);
14: }
二、DirectoryFormatter
DirectoryBrowserMiddleware中介軟體的目的很明確,就是將目錄下的內容(檔案和子目錄)格式化成一種可讀的形式響應給客戶端,針對目錄內容的響應最終實現在一個DirectoryFormatter物件上。DirectoryFormatter是我們對所有實現了IDirectoryFormatter介面的型別與對應物件的統稱,DirectoryBrowserOptions的Formatter屬性設定和返回的就是這個一個物件。
如下面的程式碼片段所示,IDirectoryFormatter介面僅僅包含一個GenerateContentAsync方法。當實現這個方法的時候,我們可以利用第一個型別為HttpContext的引數獲取當前請求上下文的資訊。該方法的另一個引數返回一組FileInfo的集合,每個FileInfo代表目標下的某個以檔案或者子目錄。
1: public interface IDirectoryFormatter
2: {
3: Task GenerateContentAsync(HttpContext context, IEnumerable<IFileInfo> contents);
4: }
我們知道預設情況下請求目錄的內容在頁面上是以一個表格的形式被呈現的,包含這個表格的HTML文件是預設使用的DirectoryFormatter生成的,它是一個型別為HtmlDirectoryFormatter的物件。如下面的程式碼片段所示,我們在構造一個HtmlDirectoryFormatter物件的時候需要指定一個HtmlEncoder物件,該物件最初來源於構造DirectoryBrowserMiddleware時指定的那個HtmlEncoder物件。
1: public class HtmlDirectoryFormatter : IDirectoryFormatter
2: {
3: public HtmlDirectoryFormatter(HtmlEncoder encoder);
4: public virtual Task GenerateContentAsync(HttpContext context, IEnumerable<IFileInfo> contents);
5: }
三、具體請求處理邏輯
既然最複雜的工作(呈現目錄內容)都已經交給DirectoryFormatter來完成了,DirectoryBrowserMiddleware自身的工作其實就沒有多少了。為了更好的說明這個中介軟體在處理請求是具體做了些什麼,我們採用一種比較好理解的方式對DirectoryBrowserMiddleware型別進行了重新定義,具體的實現體現在如下所示的程式碼片段中。
1: public class DirectoryBrowserMiddleware
2: {
3: private RequestDelegate _next;
4: private DirectoryBrowserOptions _options;
5:
6: public DirectoryBrowserMiddleware(RequestDelegate next, IHostingEnvironment env, IOptions<DirectoryBrowserOptions> options) : this(next, env, HtmlEncoder.Default,options)
7: { }
8:
9: public DirectoryBrowserMiddleware(RequestDelegate next, IHostingEnvironment env, HtmlEncoder encoder, IOptions<DirectoryBrowserOptions> options)
10: {
11: _next = next;
12: _options = options.Value;
13: _options.FileProvider = _options.FileProvider ?? env.WebRootFileProvider;
14: _options.Formatter = _options.Formatter ?? new HtmlDirectoryFormatter(encoder);
15: }
16:
17: public async Task Invoke(HttpContext context)
18: {
19: //只處理GET和HEAD請求
20: if (!new string[] { "GET", "HEAD" }.Contains(context.Request.Method, StringComparer.OrdinalIgnoreCase))
21: {
22: await _next(context);
23: return;
24: }
25:
26: //檢驗當前路徑是否與註冊的請求路徑相匹配
27: PathString path = new PathString(context.Request.Path.Value.TrimEnd('/') + "/");
28: PathString subpath;
29: if (!path.StartsWithSegments(_options.RequestPath, out subpath))
30: {
31: await _next(context);
32: return;
33: }
34:
35: //檢驗目標目錄是否存在
36: IDirectoryContents directoryContents = _options.FileProvider.GetDirectoryContents(subpath);
37: if (!directoryContents.Exists)
38: {
39: await _next(context);
40: return;
41: }
42:
43: //如果當前路徑不以"/"作為字尾,會響應一個針對“標準”URL的重定向
44: if (!context.Request.Path.Value.EndsWith("/"))
45: {
46: context.Response.StatusCode = 302;
47: context.Response.GetTypedHeaders().Location = new Uri(path.Value + context.Request.QueryString);
48: return;
49: }
50:
51: //利用DirectoryFormatter響應目錄內容
52: await _options.Formatter.GenerateContentAsync(context, directoryContents);
53: }
54: }
如上面的程式碼片段所示,當DirectoryBrowserMiddleware最終利用註冊的DirectoryFormatter來響應目標目錄的內容之前,它會做一系列的前期工作。比如它會驗證當前請求是否是GET或者HEAD請求,以及當前的URL是否與註冊的請求路徑相匹配,在匹配的情況下還需要驗證目標目錄是否存在。除此之外,這個中介軟體要求訪問目錄的請求路勁必須以字元“/”作為字尾,否則會在目前的路徑上新增這個字尾並針對最終的路徑傳送一個重定向。所以我們利用瀏覽器傳送針對某個目錄的請求的時候,URL明明沒有指定“/”作為字尾,這個字尾會自動給我們加上,這就是重定向的作用。
四、自定義DirectoryFormatter
由於目錄的內容在瀏覽器中的呈現方式完全由DirectoryFormatter完成,如果實現在HtmlDirectoryFormatter的預設呈現方式不能滿足需求(比如我們需要這個頁面與現有網站保持相同的風格),這可以通過註冊一個自定義的DirectoryFormatter來完成。接下來我們通過一個簡單的例項來演示如何定義這麼一個DirectoryFormatter。我們將自定義的DirectoryFormatter命名為ListDirectoryFormatter,應為它僅僅將所有檔案或者子目錄顯示為一個簡單的列表。
1: public class ListDirectoryFormatter : IDirectoryFormatter
2: {
3: public async Task GenerateContentAsync(HttpContext context, IEnumerable<IFileInfo> contents)
4: {
5: context.Response.ContentType = "text/html";
6: await context.Response.WriteAsync("<html><head><title>Index</title><body><ul>");
7: foreach (var file in contents)
8: {
9: string href = $"{context.Request.Path.Value.TrimEnd('/')}/{file.Name}";
10: await context.Response.WriteAsync($"<li><a href='{href}'>{file.Name}</a></li>");
11: }
12: await context.Response.WriteAsync("</ul></body></html>");
13: }
14: }
15:
16: public class Program
17: {
18: public static void Main()
19: {
20: new WebHostBuilder()
21: .UseContentRoot(Directory.GetCurrentDirectory())
22: .UseKestrel()
23: .Configure(app => app.UseDirectoryBrowser(new DirectoryBrowserOptions {Formatter = new ListDirectoryFormatter()}))
24: .Build()
25: .Run();
26: }
27: }
如上面的程式碼片段,ListDirectoryFormatter最終響應的是一個完整的HTML文件,它的主體部分只包含一個通過<ul>…</ul>表示的無序列表。列表元素(<li>)是一個針對檔案或者子目錄的連結。在呼叫擴充套件方法UseDirectoryBrowser註冊DirectoryBrowserMiddleware中介軟體的時候,我們為將一個ListDirectoryFormatter物件設定為DirectoryBrowserOptions的Formatter屬性。目錄內容最終將會採用如圖9所示的形式呈現在瀏覽器上。