AgileConfig的UI使用react重寫快完成了。上次搞定了基于jwt的登入模式(AntDesign Pro + .NET Core 實作基于JWT的登入認證),但是還有點問題。現在使用react重寫後,agileconfig成了個确确實實的前後端分離項目。那麼其實部署的話要分2個站點部署,把前端build完的靜态内容部署在一個網站,把server端也部署在一個站點。然後修改前端的baseURL讓spa的api請求都指向server的網站。
這樣做也不是不行,但是這不符合AgileConfig的精神,那就是簡單。asp.net core程式本身其實就是一個http伺服器,是以完全可以把spa網站使用它來承載。這樣隻需要部署一個站點就可以同時跑spa跟後端server了。
其實最簡單的辦法就是把build完的檔案全部丢wwwroot檔案夾下面。然後通路:
http://localhost:5000/index.html
但是這樣我們的入口是index.html,這樣看起來比較别扭,不夠友好。而且這些檔案直接丢在wwwroot的根目錄下,會跟網站其他js、css等内容混合在一起,也很混亂。
那麼下面我們就要解決這兩個檔案,我們要達到的目的有2個:
- spa的入口path友好,比如http://localhost:5000/ui
- spa靜态檔案存放的目錄獨立,比如存放在wwwroot/ui檔案夾下,或者别的什麼目錄下。
要實作以上内容隻需要一個自定義中間件就可以了。
wwwroot\ui
wwwroot\ui

我們把build完的靜态檔案全部複制到wwwroot\ui檔案夾内,以跟其他靜态資源進行區分。當然你也可以放在任意目錄下,隻要是能讀取到就可以。
ReactUIMiddleware
namespace AgileConfig.Server.Apisite.UIExtension
{
public class ReactUIMiddleware
{
private static Dictionary<string, string> _contentTypes = new Dictionary<string, string>
{
{".html", "text/html; charset=utf-8"},
{".css", "text/css; charset=utf-8"},
{".js", "application/javascript"},
{".png", "image/png"},
{".svg", "image/svg+xml"},
{ ".json","application/json;charset=utf-8"},
{ ".ico","image/x-icon"}
};
private static ConcurrentDictionary<string, byte[]> _staticFilesCache = new ConcurrentDictionary<string, byte[]>();
private readonly RequestDelegate _next;
private readonly ILogger _logger;
public ReactUIMiddleware(
RequestDelegate next,
ILoggerFactory loggerFactory
)
{
_next = next;
_logger = loggerFactory.
CreateLogger<ReactUIMiddleware>();
}
private bool ShouldHandleUIRequest(HttpContext context)
{
return context.Request.Path.HasValue && context.Request.Path.Value.Equals("/ui", StringComparison.OrdinalIgnoreCase);
}
private bool ShouldHandleUIStaticFilesRequest(HttpContext context)
{
//請求的的Referer為 0.0.0.0/ui ,以此為依據判斷是否是reactui需要的靜态檔案
if (context.Request.Path.HasValue && context.Request.Path.Value.Contains("."))
{
context.Request.Headers.TryGetValue("Referer", out StringValues refererValues);
if (refererValues.Any())
{
var refererValue = refererValues.First();
if (refererValue.EndsWith("/ui", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
}
return false;
}
public async Task Invoke(HttpContext context)
{
const string uiDirectory = "wwwroot/ui";
//handle /ui request
var filePath = "";
if (ShouldHandleUIRequest(context))
{
filePath = uiDirectory + "/index.html";
}
//handle static files that Referer = xxx/ui
if (ShouldHandleUIStaticFilesRequest(context))
{
filePath = uiDirectory + context.Request.Path;
}
if (string.IsNullOrEmpty(filePath))
{
await _next(context);
}
else
{
//output the file bytes
if (!File.Exists(filePath))
{
context.Response.StatusCode = 404;
return;
}
context.Response.OnStarting(() =>
{
var extType = Path.GetExtension(filePath);
if (_contentTypes.TryGetValue(extType, out string contentType))
{
context.Response.ContentType = contentType;
}
return Task.CompletedTask;
});
await context.Response.StartAsync();
byte[] fileData = null;
if (_staticFilesCache.TryGetValue(filePath, out byte[] outfileData))
{
fileData = outfileData;
}
else
{
fileData = await File.ReadAllBytesAsync(filePath);
_staticFilesCache.TryAdd(filePath, fileData);
}
await context.Response.BodyWriter.WriteAsync(fileData);
return;
}
}
}
}
大概解釋下這個中間件的思路。這個中間件的邏輯大概是分量部分。
1.攔截請求的路徑為/ui的請求,直接從ui檔案夾讀取index.html靜态檔案的内容然後輸出出去,這就相當于直接通路/index.html。但是這樣的路徑形式看起來更加友好。
2.攔截react spa需要的靜态資源檔案,比如css檔案,js檔案等。這裡比較麻煩,因為spa拉靜态檔案的時候path是直接從網站root開始的,比如http://localhost:5000/xxx.js,那麼怎麼區分出來這個檔案是react spa需要的呢?我們判斷一下請求的Referer頭部,如果Referer的path是/ui,那麼就說明是react spa需要的靜态資源,同樣從ui檔案夾去讀取。
這裡還需要給每個response設定指定的contentType不然浏覽器無法準确識别資源。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseMiddleware<ExceptionHandlerMiddleware>();
}
app.UseMiddleware<ReactUIMiddleware>();
...
...
}
在Startup類的Configure方法内使用這個中間件。這樣我們的改造就差不多了。
運作一下
通路下http://localhost:5000/ui 可以看到spa成功加載進來了。
總結
為了能讓asp.net core承載react spa應用,我們使用一個中間件進行攔截。當通路對應path的時候從本地檔案夾内讀取靜态資源傳回給浏覽器,進而完成spa所需要資源的加載。這次使用react spa來示範,其實換成任何spa應用都是一樣的操作。
代碼在這:ReactUIMiddleware