系列文章
- 基于.NetCore開發部落格項目 StarBlog - (1) 為什麼需要自己寫一個部落格?
- 基于.NetCore開發部落格項目 StarBlog - (2) 環境準備和建立項目
- 基于.NetCore開發部落格項目 StarBlog - (3) 模型設計
- 基于.NetCore開發部落格項目 StarBlog - (4) markdown部落格批量導入
- 基于.NetCore開發部落格項目 StarBlog - (5) 開始搭建Web項目
- 基于.NetCore開發部落格項目 StarBlog - (6) 頁面開發之部落格文章清單
- 基于.NetCore開發部落格項目 StarBlog - (7) 頁面開發之文章詳情頁面
- 基于.NetCore開發部落格項目 StarBlog - (8) 分類層級結構展示
- 基于.NetCore開發部落格項目 StarBlog - (9) 圖檔批量導入
- 基于.NetCore開發部落格項目 StarBlog - (10) 圖檔瀑布流
- 基于.NetCore開發部落格項目 StarBlog - (11) 實作通路統計
- 基于.NetCore開發部落格項目 StarBlog - (12) Razor頁面動态編譯
- 基于.NetCore開發部落格項目 StarBlog - (13) 加入友情連結功能
- 基于.NetCore開發部落格項目 StarBlog - (14) 實作主題切換功能
- 基于.NetCore開發部落格項目 StarBlog - (15) 生成随機尺寸圖檔
- 基于.NetCore開發部落格項目 StarBlog - (16) 一些新功能 (監控/統計/配置/初始化)
- 基于.NetCore開發部落格項目 StarBlog - (17) 自動下載下傳文章裡的外部圖檔
前言
好久沒更新部落格了,上個月底更新了一篇關于StarBlog部落格開發的文章之後,就因為線下教育訓練、詩詞大會之類的雜七雜八的事浪費了很多時間,有段時間一直在忙這些事情都沒空寫代碼……
PS:我在詩詞大會上分享了這首詩:讀白居易的《禽蟲十二章》
然後最近買了楊中科大佬新出的《AspNetCore技術内幕》,看得津津有味,花了一個多星期的時間,把書裡的内容大緻看了一遍,DDD(領域驅動設計)我早就想學了,不過一直沒找到好的入門資料,大佬的這本書就很不錯,很好懂,盡管如此,DDD還是一個相對複雜的方法,需要通過不斷的實踐來掌握。
雖然最近做了這麼多事,但同時工作也很忙,有個項目需要在九月前上線,本來我打算來實踐一下DDD的,不過寫着寫着發現還是把握不住,隻好先用我之前的DjangoStarter架構,後面再慢慢把我的StarBlog部落格用DDD思想進行改造~
對了,這麼久沒更新部落格的原因,還有一點是我在使用過程中對目前的管理背景非常不滿(使用Vue2+ElementUI開發),使用者體驗極差,是以我同時在構思用何種技術對管理背景前端項目進行重構,目前有幾個備選項:
- blazor(使用C#開發前端,很酷)
- react(相對其他的來說,我最喜歡的前端技術棧)
- 仍然vue,但重寫現有架構(工作量較小)
還沒拿定主意,在重構完成之前,隻能先捏着鼻子用現有的管理背景,同時大機率也不會在現有的前端項目中增加新功能了。
回到正題
OK,說回本文的内容。在部落格的使用過程中,有時候我會從其他網站複制一些markdown片段,或者是從我在其他平台的部落格上複制markdown内容(部落格園、掘金之類的),這時候複制過來的markdown内容裡面可能會有一些圖檔,如果不做處理,可能會産生某些問題,如因圖檔防盜鍊功能導緻網絡圖檔在StarBlog部落格中無法顯示、網站營運商關閉導緻圖檔丢失等,對于資料,還是牢牢掌握在自己的手中比較放心。
于是,我就做了這個功能:将markdown文章中的網絡圖檔下載下傳下來,并且替換markdown中的連結。
原理很簡單,掃描markdown,把圖檔連結拿出來下載下傳,同時把圖檔連結替換成StarBlog上的位址。下面一步步介紹如何在代碼中實作。
下載下傳圖檔
首先是下載下傳圖檔的功能,C#中通路網絡,可以使用HttpClient這個标準庫
最簡單的用法是這樣:
var client = new HttpClient();
await client.GetAsync("圖檔位址");
不過官方文檔中并不推薦這種用法,最佳實踐是一個程式中隻維護一個HttpClient的對象
在AspNetCore中,我們可以利用依賴注入
IHttpClientFactory
來管理HttpClient對象。
在
Program.cs
中注冊服務
builder.Services.AddHttpClient();
在需要的地方注入
IHttpClientFactory
,比如在本項目中,我們建立一個
CommonService.cs
來放下載下傳檔案的代碼,考慮到這個功能以後别的地方也可能用到,是以做成通用的,不和
PostService
耦合在一起。
代碼如下:
public class CommonService {
private readonly ILogger<CommonService> _logger;
private readonly IHttpClientFactory _httpClientFactory;
public CommonService(ILogger<CommonService> logger, IHttpClientFactory httpClientFactory) {
_logger = logger;
_httpClientFactory = httpClientFactory;
}
public async Task<string?> DownloadFileAsync(string url, string savePath) {
var httpClient = _httpClientFactory.CreateClient();
try {
var resp = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
// 生成随機檔案名
var fileName = GuidUtils.GuidTo16String() + Path.GetExtension(url);
var filePath = Path.Combine(savePath, WebUtility.UrlEncode(fileName));
await using var fs = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.Write);
await resp.Content.CopyToAsync(fs);
return fileName;
}
catch (Exception ex) {
_logger.LogError("下載下傳檔案出錯,資訊:{Error}", ex);
return null;
}
}
}
分析一下部分代碼:
- 第13行代碼使用HttpClient的
方法下載下傳資料,添加了個GetAsync
參數,這樣我們不必等全部資訊加載到記憶體中後再進行流讀取之類的操作,而是在請求頭傳回的時候就可以進入下一步處理。避免因為要下載下傳的檔案太大而導緻HttpCompletionOption.ResponseHeadersRead
,這對下載下傳檔案的程式來說很重要!OutOfMemoryException
- 第16行,使用封裝好的Guid工具生成16位的GUID,直接用
也行,這是32位的。Guid.NewGuid().ToString()
- 第18-19行,将Http響應内容寫入檔案流
搞定,下載下傳檔案代碼比較簡單,涉及到IO操作這種容易出錯的地方,細節要處理好,才能保證程式的穩定性。
PS:别忘了注冊服務!builder.Services.AddSingleton<CommonService>();
處理Markdown
下載下傳圖檔的功能搞定了之後,我們繼續來做markdown處理的部分
關于C#處理Markdown,之前已經有過多次探索了,可以說是輕車熟路了hhh~
附上之前關于Markdown處理的文章:
- C#解析Markdown文檔,實作替換圖檔連結操作
- 基于.NetCore開發部落格項目 StarBlog - (4) markdown部落格批量導入
依然是用Markdig這個庫(貌似.NetCore處理markdown上也沒其他選擇)
在
PostService.cs
中增加代碼
/// <summary>
/// Markdown中外部圖檔下載下傳
/// <para>如果Markdown中包含外部圖檔URL,則下載下傳到本地且進行URL替換</para>
/// </summary>
private async Task<string> MdExternalUrlDownloadAsync(Post post) {
if (post.Content == null) return string.Empty;
// 得先初始化目錄
InitPostMediaDir(post);
var document = Markdown.Parse(post.Content);
foreach (var node in document.AsEnumerable()) {
if (node is not ParagraphBlock {Inline: { }} paragraphBlock) continue;
foreach (var inline in paragraphBlock.Inline) {
if (inline is not LinkInline {IsImage: true} linkInline) continue;
var imgUrl = linkInline.Url;
// 跳過空連結
if (imgUrl == null) continue;
// 跳過本站位址的圖檔
if (imgUrl.StartsWith(Host)) continue;
// 下載下傳圖檔
_logger.LogDebug("文章:{Title},下載下傳圖檔:{Url}", post.Title, imgUrl);
var savePath = Path.Combine(_environment.WebRootPath, "media", "blog", post.Id!);
var fileName = await _commonService.DownloadFileAsync(imgUrl, savePath);
linkInline.Url = fileName;
}
}
await using var writer = new StringWriter();
var render = new NormalizeRenderer(writer);
render.Render(document);
return writer.ToString();
}
代碼說明:
- 第9行的初始化目錄就是檢查這篇文章有沒有對應的目錄,沒有就先建立,很簡單就不貼代碼了。可以在github項目裡看到完整代碼
- 第12行開始的兩層循環通過周遊markdown文檔樹,把圖檔連結找出來
- 第22行檢查圖檔是站外還是站内的,站内圖檔不用下載下傳
這樣就完成了markdown裡站外圖檔的下載下傳和連結替換~
修改文章儲存邏輯
接下來修改一下文章的儲存邏輯
還是在這個
PostService.cs
裡,儲存和新增文章共享一個方法:
InsertOrUpdateAsync
直接上代碼
public async Task<Post> InsertOrUpdateAsync(Post post) {
// 是新文章的話,先儲存到資料庫
if (await _postRepo.Where(a => a.Id == post.Id).CountAsync() == 0) {
post = await _postRepo.InsertAsync(post);
}
// 檢查文章中的外部圖檔,下載下傳并進行替換
post.Content = await MdExternalUrlDownloadAsync(post);
// 修改文章時,将markdown中的圖檔位址替換成相對路徑再儲存
post.Content = MdImageLinkConvert(post, false);
// 處理完内容再更新一次
await _postRepo.UpdateAsync(post);
return post;
}
代碼說明:
- 新文章的話,會先儲存一次,作為草稿。
- 先下載下傳外部圖檔,再替換本地圖檔連結(關于圖檔連結替換的,可以參考本系列第4篇文章,上面有連結)
- 完成這些之後再儲存,注意這時文章還是草稿狀态,需要通過另一個方法将文章的
屬性設定為true,不過與本文關系不大,這裡先不貼代碼,後續在RESTFul接口開發部分的文章裡會詳細介紹這個流程。IsPublish
到這裡就搞定啦~
參考資料
- 官方文檔:https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient?view=net-6.0
- c#:HttpClient使用詳解:https://blog.csdn.net/u010476739/article/details/119782562
- C#中HttpClient的使用小結:https://zhuanlan.zhihu.com/p/89106847
微信公衆号:「程式設計實驗室」
專注于網際網路熱門新技術探索與團隊靈活開發實踐,包括架構設計、機器學習與資料分析算法、移動端開發、Linux、Web前後端開發等,歡迎一起探讨技術,分享學習實踐經驗。