前言
本文記錄一下 StarBlog 專案的當前狀態與接下來 v2 版本的開發規劃。
StarBlog 專案從 2022 年開始至今已經 2 年多了,本來早就該給第一期做個小結的,但這種部落格型別的專案,一旦穩定能用之後,我就沒多大的動力去更新了 😂
部落格地址是: blog.deali.cn
另外還有個用 NextJS 重構的預覽版
PS:對了,還有個 StarBlog 的 Vue 前端系列,我之前已經寫好了,一直沒有發出來,接下來會連載更新,感興趣的同學可以關注一下。
內容比較簡單(當時我是邊學 Vue 邊做的),不過從這個角度來說也挺適合剛入門的同學閱讀,畢竟以初學者的角度寫的。
當時開發這個專案的本意是邊學邊做,作為熟悉 AspNetCore 的練手專案,現在說實話也無法投入很多時間去開發這類部落格專案了……畢竟這種型別的專案太基礎了。
還記得我年初規劃的幾個新專案,到現在的進度都還很有限,所以接下來還是得把時間放在這些新專案上。
這裡來回顧一下新專案開發計劃:
- StarSSO - 單點認證服務
- EchoSubs - 影片字幕識別、翻譯服務
- SnapMix - 隨機圖片介面服務
- AIHub 2.0 - AIHub 的升級版
- StarBlogHub - 實現一個去中心化的部落格聚合平臺,不同的個人部落格都可以接入,共享流量
- TodayTV - 看電視,用於代替傳統的電視直播
- Clipify - 基於 Blazor 的影片剪輯工具,已開源
這麼看下來也太多了,攤子鋪太大了,不好收場啊…
不過精力放在其他專案上也不是意味著不管 StarBlog 專案了,事實上我一直有在做小修小改,這個看 Github commit 就知道了。
當前版本的不足之處
從 StarBlog 專案上線至今,我不斷學習關於 AspNetCore 的細節知識,相比起剛剛開發這個專案的時候,對框架的熟悉程度提升了一些,自然也發現了之前程式碼裡的侷限之處:
- 增刪改查的「查」應該使用 patch 方法
- 在 Get 方法介面加上
[HttpHead]
來實現對 Head 方法的支援 - 過濾和搜尋的介面需要對引數進行 trim
- 不應該將介面的返回值都修改為
ApiResponse
型別,應該保留框架的ActionResult
型別,這樣功能更多 - 只統一了介面的返回值,沒有對異常進行包裝,應該使用
app.UseExceptionHandler
中介軟體來實現統一錯誤處理(也可以使用異常過濾器) - 對 markdown 的 toc、公式、程式碼塊、表格巢狀圖片等還是支援不佳
這些問題將是 v2 版本要解決的。
v2 新版規劃
目前規劃了一些新的功能和最佳化,但這肯定不是 v2 版本的全部,各位同學如果有好的建議也可以留言討論一下~
部落格前臺重構
- 使用 Next.js 重構
- 使用 nodejs 技術棧的 markdown 解析
管理後臺重構
- 使用基於 react 的技術棧重構
新的訪問統計功能
- 地理資訊視覺化
- 搜尋引擎收錄分析
- 反爬蟲功能
- 文章閱讀量統計
文章編輯功能
- 使用新的 markdown 編輯器(最好像 wagtail 那樣所見即所得的)
- 支援在文章中加入更多內容(如影片)
文章閱讀體驗最佳化
- 使用新的 markdown 渲染工具(目前使用的是我 fork 魔改的 editor.md,用起來還可以,但這個工具很老了,而且也停更了,我希望找一個維護良好更現代的渲染工具來替代)
文章加密
- 設定固定密碼
- 關注公眾號獲取動態密碼
新版搜尋功能
- 使用全文檢索引擎
- 加入 Embedding
AI 功能
- 知識庫
- 對話功能
- 文章 AI 總結
- 自動評論
AspNetCore 溫故知新
24 年初我又複習了一些 AspNetCore 框架的功能,比較零散不成體系,與 StarBlog 的開發是息息相關的,所以在本文記錄一下吧~
統一錯誤處理
異常過濾器
編寫過濾器
public class ApiExceptionFilter : IExceptionFilter {
public void OnException(ExceptionContext context) {
var response = new ApiResponse {
StatusCode = 500,
Successful = false,
// 可以根據需求修改此處來更詳細地描述錯誤資訊
Message = context.Exception.Message
};
context.Result = new ObjectResult(response) {
StatusCode = 500
};
// 標記異常已處理
context.ExceptionHandled = true;
}
}
註冊
builder.Services.AddControllers(options => {
options.Filters.Add<ApiExceptionFilter>();
options.Filters.Add<ApiResponseFilter>();
});
過濾器可以捕獲在 Action 方法或控制器中丟擲的異常,並允許開發者對其進行處理,然後返回一個統一的響應格式。
但不是在 Action 方法或控制器中丟擲的異常,是捕獲不到的,例如加了 [Authorize]
特性的介面,沒有提供認證資訊的時候訪問報 401 錯誤,這種是捕獲不到的。
中介軟體
如果想要在整個應用程式中處理異常,使用中介軟體可能是更好的選擇。中介軟體可以捕獲在請求處理管道中發生的所有型別的異常。
使用 app.UseExceptionHandler
中介軟體來實現統一錯誤處理
一個簡單的例子,在 Program.cs
裡配置內建的 ExceptionHandler
中介軟體
if (app.Environment.IsDevelopment()) {
app.UseDeveloperExceptionPage();
}
else {
app.UseExceptionHandler(applicationBuilder => {
applicationBuilder.Run(async context => {
// 記錄日誌啥的
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
await context.Response.WriteAsJsonAsync(new { message = "Unexpected error!" });
});
});
}
自己手寫中介軟體
public class ErrorHandlingMiddleware {
private readonly RequestDelegate _next;
public ErrorHandlingMiddleware(RequestDelegate next) {
_next = next;
}
public async Task Invoke(HttpContext context) {
try {
await _next(context);
}
catch (Exception ex) {
await HandleExceptionAsync(context, ex);
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception exception) {
var response = new ApiResponse {
StatusCode = 500,
Successful = false,
Message = exception.Message
};
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
return context.Response.WriteAsync(JsonConvert.SerializeObject(response));
}
}
使用中介軟體
app.UseMiddleware<ErrorHandlingMiddleware>();
自定義認證授權相關的返回值
在 ASP.NET Core 中,當使用 app.UseAuthentication()
和 app.UseAuthorization()
中介軟體處理認證和授權邏輯時,如果認證或授權失敗,這些中介軟體會直接修改響應,返回 HTTP 狀態碼如 401(未認證)或 403(未授權)。
這些響應並不是透過異常機制處理的,因此常規的異常處理中介軟體或 UseExceptionHandler
無法捕獲和修改這些特定的錯誤響應。
要自定義這些錯誤響應,需要配置認證中介軟體以使用特定的事件來修改響應。
這通常涉及到在認證方案的配置中新增事件處理邏輯。下面以 JWT 認證為例說明如何自定義 401 和 403 的響應:
配置 JWT 認證以自定義 401 和 403 響應
在 services.AddAuthentication().AddJwtBearer(options => { ... })
裡面新增事件配置
options.Events = new JwtBearerEvents {
OnChallenge = async context => {
if (!context.Response.HasStarted) {
context.HandleResponse(); // 阻止預設的401響應
var response = new ApiResponse {
StatusCode = 401,
Successful = false,
Message = "Authentication failed. You are not authorized."
};
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(JsonConvert.SerializeObject(response));
}
},
OnForbidden = async context => {
var response = new ApiResponse {
StatusCode = 403,
Successful = false,
Message = "You do not have permission to access this resource."
};
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(JsonConvert.SerializeObject(response));
}
};
在 JWT 認證流程中,JwtBearerEvents
類提供了多個事件來處理不同的認證相關情景:
- OnChallenge - 這個事件是在認證失敗時觸發的,通常是因為請求中沒有提供有效的 JWT 令牌。例如,如果請求沒有包含令牌,或者令牌不符合預期的格式,或者令牌已過期等情況,都會觸發此事件。
OnChallenge
事件是處理返回 401 未認證響應的正確位置。 - OnAuthenticationFailed - 這個事件在認證過程中出現異常時觸發。這通常涉及到令牌解析或驗證中出現的錯誤,比如令牌被篡改。在此事件中,你可以記錄異常或修改認證失敗時的處理邏輯。
- OnForbidden - 當使用者透過了認證但是不符合特定的授權條件時觸發。例如,使用者的角色或許可權不足以訪問某個資源。在此事件中,你可以自定義返回 403 禁止訪問的響應。
自定義模型繫結
實現 IModelBinder
介面可以自定義介面的 model bind 行為,這種叫做 Custom Model Binder
,建議放在 Helpers
目錄下
例子:輸入 guid1,guid2,guid3,guid4
形式的引數,獲取公司列表
[HttpGet("{ids}")]
public async Task<IActionResult> GetCompanyCollection([FromRoute] IEnumerable<Guid> ids) {
}
新增 Helpers/ArrayModelBinder.cs
檔案
public class ArrayModelBinder : IModelBinder {
public Task BindModelAsync(ModelBindingContext bindingContext) {
if (!bindingContext.ModelMetadata.IsEnumerableType) {
bindingContext.Result = ModelBindingResult.Failed();
return Task.CompletedTask;
}
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).ToString();
if (string.IsNullOrWhiteSpace(value)) {
bindingContext.Result = ModelBindingResult.Success(null);
return Task.CompletedTask;
}
// 使用反射獲取引數型別
var elementType = bindingContext.ModelType.GetTypeInfo().GenericTypeParameters[0];
// 根據引數型別,新建一個型別轉換器
var converter = TypeDescriptor.GetConverter(elementType);
// 按照 , 分割字串,
// 指定 `StringSplitOptions.RemoveEmptyEntries` 引數,用以清除分割後的空值,比如 `1,,3,4` -> `["1", "3", "4"]`
var values = value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)
.Select(x => converter.ConvertFromString(x.Trim()))
.ToArray();
var typedValues = Array.CreateInstance(elementType, values.Length);
values.CopyTo(typedValues, 0);
bindingContext.Model = typedValues;
bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
return Task.CompletedTask;
}
}
最後在介面簽名裡指定使用的 ModelBinder
[HttpGet("{ids}")]
public async Task<IActionResult> GetCompanyCollection(
[FromRoute]
[ModelBinder(BinderType = typeof(ArrayModelBinder))]
IEnumerable<Guid> ids) {
}
PS:為了更直觀表示出這個 route parameter 是支援傳入多個值,可以在引數上加個括號。
[HttpGet("({ids})")]
public async Task<IActionResult> GetCompanyCollection(
小結
不知道說啥,現在很少寫 C#了,最近.Net9 好像出來了,最佳化了 GC 演算法,等有空來試試~