雖然ASP.NET Core是一款“動態”的Web服務端框架,但是在很多情況下都需要處理針對靜態文件的請求,最為常見的就是這對JavaScript腳本文件、CSS樣式文件和圖片文件的請求。針對不同格式的靜態文件請求的處理,ASP.NET Core為我們提供了三個中間件,它們將是本系列文章論述的重點。不過在針對對它們展開介紹之前,我們照理通過一些簡單的實例來體驗一下如何在一個ASP.NET Core應用中發布靜態文件。[本文已經同步到《ASP.NET Core框架揭秘》之中]
目錄
一、以Web的形式讀取文件
二、浏覽目錄內容
三、顯示默認頁面
四、映射媒體類型
我們創建的演示實例是一個簡單的ASP.NET Core控制台應用,它具有如下圖所示的項目結構。我們可以看到在默認作為WebRoot的目錄(wwwroot)下,我們將JavaScript腳本文件、CSS樣式文件和圖片文件存放到對應的子目錄(js、css和img)下,我們將把這個目錄的所有文件以Web的形式發布出來,客戶端可以訪問相應的URL來獲取這些文件。
針對靜態文件的請求是通過一個名為StaticFileMiddleware的中間件來實現的,這個中間件類型定義在NuGet包“Microsoft.AspNetCore.StaticFiles”中,所以我們需要預先按照這個NuGet包。整個應用只包含如下所示的這幾行代碼,StaticFileMiddleware這個中間件的注冊是通過調用ApplicationBuilder的擴展方法UseStaticFiles來完成的。
1: public class Program
2: {
3: public static void Main()
4: {
5: new WebHostBuilder()
6: .UseContentRoot(Directory.GetCurrentDirectory())
7: .UseKestrel()
8: .Configure(app => app.UseStaticFiles())
9: .Build()
10: .Run();
11: }
12: }
除了注冊必需的StaticFileMiddleware中間件之外,我們還調用了WebHostBuilder的UseContentRoot方法將當前項目的根目錄作為ContentRoot目錄。我們知道ASP.NET Core應用具有兩個重要的根目錄,它們分別是ContentRoot和WebRoot,後者也是對外發布的靜態文件默認使用的根目錄。由於WebRoot目錄的默認路徑就是“{contentroot}/wwwroot”,所示上面這段程序就是將項目中的這個wwwroot目錄下的所有靜態文件發布出來。
當這個程序運行之後,我們就可以通過向對應URL發送HTTP請求的方式來獲取某個的文件,這個URL由文件相當於wwwroot目錄的路徑來決定。比如JPG文件“~/wwwroot/img/dophin1.jpg”對應的URL為“http://
localhost:5000/img/dophin1.jpg”。我們直接利用浏覽器訪問這個URL,目標圖片會直接顯示出來。
上面我們通過一個簡單的實例將WebRoot所在目錄下的所有靜態文件直接發布出來。如果我們需要發布的靜態文件存儲在其他目錄下呢?依舊是演示的這個應用,現在我們將一些文檔存儲在如下圖所示的“~/doc/”目錄下並以Web的形式發布出來,我們的程序又該如何編寫呢?
我們知道ASP.NET Core應用大部分情況下都是利用一個FileProvider對象來讀取文件的,它在處理針對靜態文件的請求是也不例外。對於我們調用ApplicationBuilder的擴展方法UseStaticFiles方法注冊的這個類型為StaticFileMiddleware的中間件,其內部具有一個FileProvider和請求路徑的映射關系。如果調用UseStaticFiles方法沒有指定任何的參數,那麼這個映射關系的請求路徑就是應用的基地址(PathBase),而FileProvider自然就是指向WebRoot目錄的PhysicalFileProvider。
上述的這個需求可以通過顯式注冊這個映射的方式來實現,為此我們在現有程序的基礎上額外添加了一次針對UseStaticFiles方法的調用,並通過指定的參數(是一個StaticFileOptions對象)顯式指定了采用的FileProvider(針對“~/doc/”的PhysicalFileProvider)和請求路徑(“/documents”)。
1: public class Program
2: {
3: public static void Main()
4: {
5: string contentRoot = Directory.GetCurrentDirectory();
6: new WebHostBuilder()
7: .UseContentRoot(contentRoot)
8: .UseKestrel()
9: .Configure(app => app
10: .UseStaticFiles()
11: .UseStaticFiles(new StaticFileOptions {
12: FileProvider = new PhysicalFileProvider(Path.Combine(contentRoot, "doc")),
13: RequestPath = "/documents"
14: }))
15: .Build()
16: .Run();
17: }
18: }
按照上面這段程序指定的映射關系,對於存儲在“~/doc/”目錄下的這個PDF文件(“checklist.pdf”),發布在Web上的URL為“http://localhost:5000/documents/checklist.pdf”。當我們在浏覽器上請求這個地址時,該PDF文件的內容將會按照如下圖所示的形式顯示在浏覽器上。
注冊的StaticFileMiddleware中間件只會處理針對某個具體靜態文件的額請求,如果我們向針對某個目錄的URL發送HTTP請求(比如“http://localhost:5000/img/”),得到的將是一個狀態為404的響應。不過我們可以通過注冊另一個名為DirectoryBrowserMiddleware的中間件來顯示請求目錄的內容。具體來說,這個中間件會返回一個HTML頁面,請求目錄下的所有文件將以表格的形式包含在這個頁面中。對於我們演示的這個應用來說,我們可以按照如下的方式調用UseDirectoryBrowser方法來注冊這個DirectoryBrowserMiddleware中間件。
1: public class Program
2: {
3: public static void Main()
4: {
5: string contentRoot = Directory.GetCurrentDirectory();
6: IFileProvider fileProvider = new PhysicalFileProvider(
7: Path.Combine(contentRoot, "doc"));
8: new WebHostBuilder()
9: .UseContentRoot(contentRoot)
10: .UseKestrel()
11: .Configure(app => app
12: .UseStaticFiles()
13: .UseStaticFiles(new StaticFileOptions {
14: FileProvider = fileProvider,
15: RequestPath = "/documents"
16: })
17: .UseDirectoryBrowser()
18: .UseDirectoryBrowser(new DirectoryBrowserOptions {
19: FileProvider = fileProvider,
20: RequestPath = "/documents"
21: }))
22: .Build()
23: .Run();
24: }
25: }
當上面這個應用啟動之後,如果我們利用浏覽器向針對某個目錄的URL(比如“http://localhost:5000/”或者“http://localhost:5000/img/”),目標目錄的內容(包括子目錄和文件)將會以下圖所示的形式顯示在一個表格中。不僅僅如此,子目錄和文件均會顯示為鏈接,指向目標目錄或者文件的URL。
從安全的角度來講,利用注冊的UseDirectoryBrowser中間件顯示一個目錄浏覽頁面會將整個目標目錄的接口和所有文件全部暴露出來,所以這個中間件需要根據自身的安全策略謹慎使用。對於針對目錄的請求,另一種更為常用的響應策略就是顯示一個保存在這個目錄下的默認頁面。按照約定,作為默認頁面的文件一般采用如下四種命名方式:default.htm、default.html、index.htm或者index.html。針對目標目錄下默認頁面的呈現實現在一個名為DefaultFilesMiddleware的中間件中,我們演示的這個應用可以按照如下的方式調用UseDefaultFiles方法來注冊這個中間件。
1: public class Program
2: {
3: public static void Main()
4: {
5: string contentRoot = Directory.GetCurrentDirectory();
6: IFileProvider fileProvider = new PhysicalFileProvider(Path.Combine(contentRoot, "doc"));
7:
8: new WebHostBuilder()
9: .UseContentRoot(contentRoot)
10: .UseKestrel()
11: .Configure(app => app
12: .UseDefaultFiles()
13: .UseDefaultFiles(new DefaultFilesOptions{
14: RequestPath = "/documents",
15: FileProvider = fileProvider,
16: })
17: .UseStaticFiles()
18: .UseStaticFiles(new StaticFileOptions
19: {
20: FileProvider = fileProvider,
21: RequestPath = "/documents"
22: })
23: .UseDirectoryBrowser()
24: .UseDirectoryBrowser(new DirectoryBrowserOptions
25: {
26: FileProvider = fileProvider,
27: RequestPath = "/documents"
28: }))
29: .Build()
30: .Run();
31: }
32: }
現在我們在“~/wwwroot/img/”目錄下創建一個名為index.htm的默認頁面,現在利用浏覽器訪問這個目錄對應的URL(“http://localhost:5000/img/”),顯示就時這個頁面的內容。
我們必須在注冊StaticFileMiddleware和DirectoryBrowserMiddleware之前注冊DefaultFilesMiddleware,否則它起不了任何作用。由於DirectoryBrowserMiddleware和DefaultFilesMiddleware這兩個中間件處理的均是針對目錄的請求,如果DirectoryBrowserMiddleware先被注冊,那麼顯示的總是目錄的內容。若DefaultFilesMiddleware先被注冊,在默認頁面不存在情況下回顯示目錄的內容。至於為什麼要先於StaticFileMiddleware之前注冊DefaultFilesMiddleware,則是因為後者是通過采用URL重寫的方式實現的,也就是說這個中間件會將針對目錄的請求改寫成針對默認頁面的請求,而最終針對默認頁面的請求還得依賴StaticFileMiddleware完成。
DefaultFilesMiddleware中間件在默認情況下總是以約定的名稱(default.htm、default.html、index.htm或者index.html)在當前請求的目錄下定位默認頁面。如果我們希望作為默認頁面的文件不能按照這樣的約定命名(比如readme.htm),我們需要按照如下的方式顯式指定默認頁面的文件名。
1: public class Program
2: {
3: public static void Main()
4: {
5: string contentRoot = Directory.GetCurrentDirectory();
6: IFileProvider fileProvider = new PhysicalFileProvider(Path.Combine(contentRoot, "doc"));
7:
8: DefaultFilesOptions options1 = new DefaultFilesOptions();
9: DefaultFilesOptions options2 = new DefaultFilesOptions{
10: RequestPath = "/documents",
11: FileProvider = fileProvider
12: };
13: options1.DefaultFileNames.Add("readme.htm");
14: options2.DefaultFileNames.Add("readme.htm");
15:
16: new WebHostBuilder()
17: .UseContentRoot(contentRoot)
18: .UseKestrel()
19: .Configure(app => app
20: .UseDefaultFiles(options1)
21: .UseDefaultFiles(options2)
22: .UseStaticFiles()
23: .UseStaticFiles(new StaticFileOptions{
24: FileProvider = fileProvider,
25: RequestPath = "/documents"
26: })
27: .UseDirectoryBrowser()
28: .UseDirectoryBrowser(new DirectoryBrowserOptions{
29: FileProvider = fileProvider,
30: RequestPath = "/documents"
31: }))
32: .Build()
33: .Run();
34: }
35: }
通過上面演示的實例可以看出,浏覽器能夠正確的將請求的目標文件的內容正常的呈現出來。對HTTP協議具有基本了解的人都應該知道,響應的文件能夠在支持的浏覽器上呈現具有一個基本的前提,那就是響應消息通過Content-Type報頭攜帶的媒體類型必須與內容一致。我們的實例演示了針對兩種類型文件的請求,一種是JPG文件,另一種是PDF文件,對應的媒體類型分別是“image/jpg”和“application/pdf”,那麼StaticFileMiddleware是如何正確解析出正確的媒體類型的呢?
StaticFileMiddleware針對媒體類型的解析是通過一個名為ContentTypeProvider的對象來實現的,而默認使用的則是一個FileExtensionContentTypeProvider對象。顧名思義,FileExtensionContentTypeProvider是根據文件的擴展命名來解析媒體類型的。FileExtensionContentTypeProvider內部預定了數百種常用文件擴展名與對應媒體類型之間的映射關系,所以如果我們發布的靜態文件具有標准的擴展名,StaticFileMiddleware就能為對應的響應賦予正確的媒體類型。
那麼如果某個文件的擴展名沒有在這個預定義的映射之中,或者我們需要某個預定義的擴展名匹配不同的媒體類型,我們應該如何解決呢?還是針對我們演示的這個實例,想在我將“~/wwwroot/img/ dophin1.jpg”這個文件的擴展名改成“.img”,毫無疑問StaticFileMiddleware將能為針對該文件的請求解析出正確媒體類型。這個問題具有若干不同的解決方案,第一種方案就是讓StaticFileMiddleware支持不能識別的文件類型,並為它們設置一個默認的媒體類型,如下所示了具體采用的編程方式。
1: public class Program
2: {
3: public static void Main()
4: {
5: new WebHostBuilder()
6: .UseContentRoot(Directory.GetCurrentDirectory();)
7: .UseKestrel()
8: .Configure(app => app.UseStaticFiles(new StaticFileOptions {
9: ServeUnknownFileTypes = true,
10: DefaultContentType = "image/jpg"
11: }))
12: .Build()
13: .Run();
14: }
15: }
上述這種解決方案只能設置一種默認媒體類型,如果具有多種需要映射成不同媒體類型的非識別文件類型,采用這種方案就無能為力了,所以最根本的解決方案還是需要將不能識別的文件類型和對應的媒體類型進行映射。由於StaticFileMiddleware使用的ContentTypeProvider是可以定制的,我們可以按照如下的方式顯式地為StaticFileMiddleware指定一個FileExtensionContentTypeProvider對象作為它的ContentTypeProvider,然後將取缺失的映射添加到這個FileExtensionContentTypeProvider對象上。
1: public class Program
2: {
3: public static void Main()
4: {
5: FileExtensionContentTypeProvider contentTypeProvider = new FileExtensionContentTypeProvider();
6: contentTypeProvider.Mappings.Add(".img", "image/jpg");
7:
8: new WebHostBuilder()
9: .UseContentRoot(Directory.GetCurrentDirectory())
10: .UseKestrel()
11: .Configure(app => app.UseStaticFiles(new StaticFileOptions{
12: ContentTypeProvider = contentTypeProvider
13: }))
14: .Build()
15: .Run();
16: }
17: }