ASP.NET Core 自訂靜態檔案來源 - 以 JSON 為例
| | 0 | |
之前玩過將 html/js/css/png 等靜態檔案轉成內嵌資源讓 Minimal API 程式徹底實現單一檔案。更進一步,有沒有可能自訂其他靜態檔案來源呢?例如像 Sharepoint 把 html、css、js 存進資料庫,但用起來跟一般網站靜態檔案沒有兩樣。能依需求輕易擴充修改,將網站捏成你想要的形狀,是 ASP.NET Core 標榜的一大特色,就來看看要如何實現這個想法吧。
從 ASP.NET 改用 ASP.NET Core 的朋友一定明顯感受到架構的革命性改變,存取服務要靠介面 (ILogger、IConfiguration、IMemoryCache),要走依賴注入,需花點時間熟悉適應,一旦遇到特殊需求,便能體會到新架構看似複雜的設計,就是為了在關鍵時刻換取必要的彈性。
動手之前,需對 ASP.NET Core 靜態檔案有基本了解,推薦這篇 Static files in ASP.NET Core。要使用靜態檔案,Program.cs 要依需求完成設定,例如:
- app.UseStaticFiles()
讓 wwwroot 下的資料夾及檔案可直接透過 URL 讀取,例如:~/css/site.css 對映 wwwroot/css/site.css。 - 若要使用 wwwroot 以外的資料夾保存靜檔案
允許以 ~/StaticFiles/ URL 存取專案 MyStaticFiles 資料夾內的靜態檔案 參考app.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider( Path.Combine(builder.Environment.ContentRootPath, "MyStaticFiles")), RequestPath = "/StaticFiles" });
- app.UseDefaultFiles() 會啟用預設文件,尋找 index.html、index.htm、default.html、default.htm 等檔案作為目錄 URL 的預設網頁
- app.UseFileServer() 整合了 UseStaticFiles、UseDefaultFiles及 UseDirectoryBrowser 功能 (目錄內容瀏覽網頁預設關閉,基於安全不建議開啟) 參考
- UseStaticFiles 與 UseFileServer 可執行多次,由多個來源取得檔案 參考
UseStaticFiles 是依賴實作 IFileProvider 的物件讀取檔案,預設使用指向 wwwroot 實體目錄的 PhysicalFileProvider 物件,之前內嵌資源用的 ManifestEmbeddedFileProvider 是另一顆實作 IFileProvider 介面的物件。換言之,只要寫一顆元件實作 IFileProvider,便能依我們想要的方式儲存及取得靜態檔案內容。這篇先來個簡單練習,我打算把 html、css、gif 塞進 JSON 裡,實作一個由 JSON 取得檔案內容的 IFileProvider :
[
{
"path": "/index.html",
"content": "<html><body><link href=css/site.css rel=stylesheet /> Web in JSON<img src=imgs/logo.gif /></body></html>"
},
{
"path": "/imgs/logo.gif",
"content": "data:image/gif;base64,R0lGODlhSABIAH...略...AGCIABAQA7"
},
{
"path": "/css/site.css",
"content": "body { font-size: 9pt } img { display: block; margin-top: 5px; }"
}
]
我寫了一個精簡版 StaticFileJsonProvider 實作 IFileProvider,主要提供 IDirectoryContents GetDirectoryContents(string subpath)、public IFileInfo GetFileInfo(string subpath) 兩個方法,傳回型別需為 IDirectoryContents、IFileInfo 介面,也要宣告類別實作。大致概念就這樣,邏輯很單純,讀取並解析 JSON 成物件陣列,比對 Path 找到檔案。
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;
namespace Guineapig.AspNetCore.FileProviders
{
public class FileEntry
{
public string Path { get; set; }
public string Content { get; set; }
public byte[] ToByteArray() =>
Content.StartsWith("data:") && Content.Contains(";base64,") ?
Convert.FromBase64String(Content.Substring(Content.IndexOf(',') + 1)) :
System.Text.Encoding.UTF8.GetBytes(Content);
public Stream GetStream() => new MemoryStream(ToByteArray());
public static DateTime LastModified = DateTime.UtcNow;
}
public class StaticFileJsonProvider : IFileProvider
{
FileEntry[] files;
public StaticFileJsonProvider(string jsonPath)
{
FileEntry.LastModified = File.GetLastWriteTimeUtc(jsonPath);
files = JsonSerializer.Deserialize<FileEntry[]>(File.ReadAllText(jsonPath), new JsonSerializerOptions
{
PropertyNamingPolicy = null,
PropertyNameCaseInsensitive = true
})!;
}
public IDirectoryContents GetDirectoryContents(string subpath)
{
Debug.WriteLine($"GetDirectoryContents - {subpath}");
return new DirectoryContents(files.Where(o => o.Path.StartsWith(subpath)));
}
public IFileInfo GetFileInfo(string subpath)
{
Debug.WriteLine($"GetFileInfo - {subpath}");
var find = files.FirstOrDefault(o => o.Path == subpath);
if (find != null) return new FileInfo(find);
return new NotFoundFileInfo(subpath);
}
public IChangeToken Watch(string filter) => NullChangeToken.Singleton;
}
public class DirectoryContents : IDirectoryContents
{
IFileInfo[] files;
public DirectoryContents(IEnumerable<FileEntry> files)
{
this.files = files.Select(o => new FileInfo(o)).ToArray();
}
public bool Exists => true;
public IEnumerator<IFileInfo> GetEnumerator()
{
foreach (var f in files) yield return f;
}
IEnumerator IEnumerable.GetEnumerator() => files.GetEnumerator();
}
public class FileInfo : IFileInfo
{
Stream stream;
public FileInfo(FileEntry file)
{
stream = file.GetStream();
Name = Path.GetFileName(file.Path);
}
public bool Exists => true;
public bool IsDirectory => false;
public DateTimeOffset LastModified => FileEntry.LastModified;
public long Length => stream.Length;
public string Name { get; private set; }
public string PhysicalPath => null!;
public Stream CreateReadStream() => stream;
}
}
Program.cs 部分只需加上一條 UseStaticFiles():
using Guineapig.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseStaticFiles(new StaticFileOptions {
FileProvider = new StaticFileJsonProvider(
Path.Combine(app.Environment.ContentRootPath, "StaticFiles.json")),
RequestPath = "/web-in-json"
});
app.MapGet("/", () => "Hello World!");
app.Run();
補充幾則眉角:
- RequestPath = "/web-in-json" 參數會過濾以 /web-in-json 起首的 URL,傳入 GetDirectoryContents() 或 GetFileInfo() 的 subpath 不包含 /web-in-json,例如,URL 若為 /web-in-json/css/site.css,subpath 參數為 /css/site.css。
- 若要輸入目錄 URL 自動導向 index.html,UseStaticFiles() 可改寫為 UseFileServer();或是在 UseStaticFiles() 前加上 UseDefaultFiles(),但二者的 FileProvider 必須共用。
var fp= new StaticFileJsonProvider( Path.Combine(app.Environment.ContentRootPath, "StaticFiles.json")); app.UseDefaultFiles(new DefaultFilesOptions { FileProvider = fp, RequestPath = "/web-in-json" }); app.UseStaticFiles(new StaticFileOptions { FileProvider = fp, RequestPath = "/web-in-json" });
- 啟用 UseFileServer() 或 UseDefaultFiles(),GetDirectoryContents() 及 GetFileInfo() 呼叫頻率會提高,要留意效能。
實測成功!
範例專案已放上 Github,需要的同學請自取。
A example of custom IFileProvider for ASP.NET Core, it uses JSON file to store static files and shows how IFileProvidere works.
Comments
Be the first to post a comment