基於.NetCore開發部落格專案 StarBlog - (32) 第一期完結

程序设计实验室發表於2024-12-02

前言

本文記錄一下 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 類提供了多個事件來處理不同的認證相關情景:

  1. OnChallenge - 這個事件是在認證失敗時觸發的,通常是因為請求中沒有提供有效的 JWT 令牌。例如,如果請求沒有包含令牌,或者令牌不符合預期的格式,或者令牌已過期等情況,都會觸發此事件。OnChallenge 事件是處理返回 401 未認證響應的正確位置。
  2. OnAuthenticationFailed - 這個事件在認證過程中出現異常時觸發。這通常涉及到令牌解析或驗證中出現的錯誤,比如令牌被篡改。在此事件中,你可以記錄異常或修改認證失敗時的處理邏輯。
  3. 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 演算法,等有空來試試~

相關文章