系列文章
- 基於.NetCore開發部落格專案 StarBlog - (1) 為什麼需要自己寫一個部落格?
- 基於.NetCore開發部落格專案 StarBlog - (2) 環境準備和建立專案
- 基於.NetCore開發部落格專案 StarBlog - (3) 模型設計
- 基於.NetCore開發部落格專案 StarBlog - (4) markdown部落格批量匯入
- 基於.NetCore開發部落格專案 StarBlog - (5) 開始搭建Web專案
- 基於.NetCore開發部落格專案 StarBlog - (6) 頁面開發之部落格文章列表
- ...
前言
前一篇文章把Web專案搭起來了,現在開始來寫頁面~
本文記錄部落格文章列表的開發,包括引數、分類過濾、分頁、搜尋、排序等內容。
ORM
本專案的ORM使用FreeSQL,前面「部落格批量匯入」的文章中有初步涉及到了,不過沒有介紹太多,這裡再講一下幾個關鍵的地方。
不同於網上比較常見的EF Core,FreeSQL設計完模型之後不需要進行遷移操作,在開發模式下開啟自動結構同步(AutoSyncStructure
)就能自動建立、修改資料表。
還有比較方便的一點是FreeSQL自帶了簡單的倉儲模式,不用再自己封裝一套,可以減少開發時的程式碼量~
不過侷限性也是有的,不封裝倉儲層的話,意味著service層程式碼跟ORM繫結,以後如果切換ORM會帶來額外的重構成本。
開啟StarBlog.Data
專案,我們來寫一個擴充套件方法,新增Extensions
目錄,在裡面新增ConfigureFreeSql.cs
using FreeSql;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace StarBlog.Data.Extensions;
public static class ConfigureFreeSql {
public static void AddFreeSql(this IServiceCollection services, IConfiguration configuration) {
var freeSql = new FreeSqlBuilder()
.UseConnectionString(DataType.Sqlite, configuration.GetConnectionString("SQLite"))
.UseAutoSyncStructure(true)
.Build();
services.AddSingleton(freeSql);
// 倉儲模式支援
services.AddFreeRepository();
}
}
然後編輯StarBlog.Web
專案下的Program.cs
,註冊一下FreeSQL的服務,用我們剛才寫的擴充套件方法。
using StarBlog.Data.Extensions;
builder.Services.AddFreeSql(builder.Configuration);
在要用的地方注入就行了,比如
IBaseRepository<Post> _postRepo;
// 獲取全部文章
_postRepo.Select.ToList()
就很方便了,開箱即用~
Service
因為我們的後端既要渲染頁面,又要做RESTFul介面,所以要把業務邏輯抽象出來放在service層,避免在Controller裡重複。
在StarBlog.Web
專案的Services
目錄裡新增PostService.cs
,我們要在這封裝跟文章有關的邏輯~
首先依賴注入,把需要用到的服務注入進來
public class PostService {
private readonly IBaseRepository<Post> _postRepo;
private readonly IBaseRepository<Category> _categoryRepo;
public PostService(IBaseRepository<Post> postRepo,
IBaseRepository<Category> categoryRepo) {
_postRepo = postRepo;
_categoryRepo = categoryRepo;
}
}
寫一個獲取全部文章的方法
public List<Post> GetAll() {
return _postRepo.Select.ToList();
}
這樣就初步搞定了,接下來要來寫Controller
Controller
在StarBlog.Web
專案的Controllers
目錄下,新增BlogController.cs
,用來實現跟部落格有關的介面。
注入剛剛寫好的 PostService
public class BlogController : Controller {
private readonly PostService _postService;
public BlogController(PostService postService) {
_postService = postService;
}
}
寫文章列表“介面”(MVC也算介面吧)
public IActionResult List() {
return View(_postService.GetAll());
}
View
根據AspNetCore MVC專案的約定,要把網頁模板放在Views
目錄下,按Controller分類
這個文章列表頁面,按照約定的路徑是:Views/Blog/List.cshtml
,建立這個檔案
@model List<Post>
@{
ViewData["Title"] = "部落格列表";
}
<div class="container px-4 py-3">
@foreach (var post in Model) {
<div class="card mb-3">
<div class="card-header">
@Model.Category.Name
</div>
<div class="card-body">
<h5 class="card-title">@Model.Title</h5>
<p class="card-text">
@Model.Summary
</p>
<a class="btn btn-outline-secondary stretched-link"
asp-controller="Blog" asp-action="Post" asp-route-id="@Model.Id">
檢視全文
</a>
</div>
</div>
}
</div>
這樣簡單的文章列表就完成了
試試效果
執行專案,開啟瀏覽器,輸入地址http://127.0.0.1:5038/Blog/List
,可以看到文章列表如下,很簡單(簡陋),而且全部文章都顯示出來了,頁面很長,這很明顯並不是我們想要的最終效果。
不急,接下來慢慢來優化。
分頁
首先是頁面把全部文章都顯示出來的問題,我們需要引入分頁功能
分頁可以自己實現,也可以用第三方元件,我們用的FreeSQL也支援分頁的API,這裡我直接掏出之前做專案用過的X.PagedList
,它封裝了分頁取資料和前端的分頁部件,比較方便。
直接nuget裡安裝這兩個包就行:
- X.PagedList
- X.PagedList.Mvc.Core
使用很簡單,X.PagedList
元件定義了List
型別的擴充套件方法,直接在ORM讀取出來的List
上用就行
_postRepo.Select.ToList().ToPagedList(pageNumber, pageSize);
返回型別是IPagedList<T>
,除了當前頁面的資料,還包含有分頁的資訊(當前頁面、總頁面數量、頁面大小、總資料量等),可以直接當List
用。
然後X.PagedList
元件還封裝了MVC模板上的HTML元件,使用也很簡單:
<nav aria-label="Page navigation example">
@Html.PagedListPager(Model.Posts, page => Url.Action(
RazorHelper.GetCurrentActionName(ViewContext), new {page, categoryId = Model.CurrentCategoryId}),
new PagedListRenderOptions {
LiElementClasses = new[] {"page-item"},
PageClasses = new[] {"page-link"},
UlElementClasses = new[] {"pagination justify-content-center"}
})
</nav>
前端我要使用bootstrap的分頁元件,所以把bootstrap的class傳進去,如果是其他前端元件庫的話,只需要傳對應的class名稱就行。
渲染出來的頁面程式碼是這樣的:
<div class="pagination-container">
<ul class="pagination justify-content-center">
<li class="active page-item"><span class="page-link">1</span></li>
<li class="page-item"><a class="page-link" href="/Blog/List?page=2&categoryId=0">2</a></li>
<li class="page-item"><a class="page-link" href="/Blog/List?page=3&categoryId=0">3</a></li>
<li class="page-item"><a class="page-link" href="/Blog/List?page=4&categoryId=0">4</a></li>
<li class="page-item"><a class="page-link" href="/Blog/List?page=5&categoryId=0">5</a></li>
<li class="page-item"><a class="page-link" href="/Blog/List?page=6&categoryId=0">6</a></li>
<li class="page-item"><a class="page-link" href="/Blog/List?page=7&categoryId=0">7</a></li>
<li class="page-item"><a class="page-link" href="/Blog/List?page=8&categoryId=0">8</a></li>
<li class="page-item"><a class="page-link" href="/Blog/List?page=9&categoryId=0">9</a></li>
<li class="page-item"><a class="page-link" href="/Blog/List?page=10&categoryId=0">10</a></li>
<li class="PagedList-ellipses page-item"><a class="PagedList-skipToNext page-link" href="/Blog/List?page=11&categoryId=0" rel="next">…</a></li>
<li class="PagedList-skipToNext page-item"><a class="page-link" href="/Blog/List?page=2&categoryId=0" rel="next">></a></li>
<li class="PagedList-skipToLast page-item"><a class="page-link" href="/Blog/List?page=64&categoryId=0">>></a></li>
</ul>
</div>
顯示效果:
請求引數封裝
前面介紹的分頁需要在訪問頁面時傳入請求引數,這樣我們Controller的Action方法就需要加上pageNumber
和pageSize
這兩個引數,後面還要加文章分類篩選和搜尋排序什麼的,這樣引數太多了,全都寫在Action方法的引數裡不優雅,好在AspNetCore提供了class作為引數的寫法。
在StarBlog.Web/ViewModels
目錄下新建QueryFilters
目錄,用來存不同介面的請求引數。
有些引數屬於不同介面都有的,合理利用物件導向,先寫個基類:QueryParameters.cs
public class QueryParameters {
/// <summary>
/// 最大頁面條目
/// </summary>
public const int MaxPageSize = 50;
private int _pageSize = 10;
/// <summary>
/// 頁面大小
/// </summary>
public int PageSize {
get => _pageSize;
set => _pageSize = (value > MaxPageSize) ? MaxPageSize : value;
}
/// <summary>
/// 當前頁碼
/// </summary>
public int Page { get; set; } = 1;
/// <summary>
/// 搜尋關鍵詞
/// </summary>
public string? Search { get; set; }
/// <summary>
/// 排序欄位
/// </summary>
public string? SortBy { get; set; }
}
文章請求引數在此基礎上還增加了狀態、分類等,從上面這個基類派生一個新類就好:PostQueryParameters.cs
public class PostQueryParameters : QueryParameters {
/// <summary>
/// 僅請求已釋出文章
/// </summary>
public bool OnlyPublished { get; set; } = false;
/// <summary>
/// 文章狀態
/// </summary>
public string? Status { get; set; }
/// <summary>
/// 分類ID
/// </summary>
public int CategoryId { get; set; } = 0;
/// <summary>
/// 排序欄位
/// </summary>
public new string? SortBy { get; set; } = "-LastUpdateTime";
}
service改造
我們的核心邏輯都是在service中實現的,請求引數肯定也要傳入給service來使用。
依然是先前的GetPagedList
方法,給其加上各種篩選條件之後是這樣:
public IPagedList<Post> GetPagedList(PostQueryParameters param) {
var querySet = _postRepo.Select;
// 是否釋出
if (param.OnlyPublished) {
querySet = _postRepo.Select.Where(a => a.IsPublish);
}
// 狀態過濾
if (!string.IsNullOrEmpty(param.Status)) {
querySet = querySet.Where(a => a.Status == param.Status);
}
// 分類過濾
if (param.CategoryId != 0) {
querySet = querySet.Where(a => a.CategoryId == param.CategoryId);
}
// 關鍵詞過濾
if (!string.IsNullOrEmpty(param.Search)) {
querySet = querySet.Where(a => a.Title.Contains(param.Search));
}
// 排序
if (!string.IsNullOrEmpty(param.SortBy)) {
// 是否升序
var isAscending = !param.SortBy.StartsWith("-");
var orderByProperty = param.SortBy.Trim('-');
querySet = querySet.OrderByPropertyName(orderByProperty, isAscending);
}
return querySet.Include(a => a.Category).ToList()
.ToPagedList(param.Page, param.PageSize);
}
根據傳入的引數,可以實現狀態過濾、分類過濾、關鍵詞過濾、排序和分頁功能。
ViewModel
一個MVC頁面只能指定一個Model,雖然可以用弱型別的ViewBag
或者ViewData
,但是弱型別不好維護,我們來定義一個ViewModel給頁面使用。
先確定要在文章列表頁面顯示哪些內容,例如顯示當前選擇的文章分類、所有分類列表。
在StarBlog.Web
的ViewModels
目錄下,新建BlogListViewModel.cs
,根據我們要展示的內容,定義模型如下
using StarBlog.Data.Models;
using X.PagedList;
namespace StarBlog.Web.ViewModels;
public class BlogListViewModel {
public Category CurrentCategory { get; set; }
public int CurrentCategoryId { get; set; }
public IPagedList<Post> Posts { get; set; }
public List<Category> Categories { get; set; }
}
搞定。
controller改造
經過前面的鋪墊,controller這裡就簡單了,不過還有要注意的地方,本專案是包含後端渲染和RESTFul介面兩部分的,因此controller要寫兩個,service只要一個就行。
RESTFul介面我後面再具體介紹,可以先看看改造後的RESTFul介面controller的程式碼:
[AllowAnonymous]
[HttpGet]
public ApiResponsePaged<Post> GetList([FromQuery] PostQueryParameters param) {
var pagedList = _postService.GetPagedList(param);
return new ApiResponsePaged<Post> {
Message = "Get posts list",
Data = pagedList.ToList(),
Pagination = pagedList.ToPaginationMetadata()
};
}
程式碼很簡單,這個獲取文章列表的介面,就單純只需要給分頁和過濾後的列表資料就行。
而MVC的介面就沒這麼簡單,要顯示在頁面上的東西,全都要在後端做渲染,包括我們在前面說的要顯示當前分類、所有分類列表。
程式碼長這樣:
public IActionResult List(int categoryId = 0, int page = 1, int pageSize = 5) {
var categories = _categoryRepo.Where(a => a.Visible)
.IncludeMany(a => a.Posts).ToList();
categories.Insert(0, new Category { Id = 0, Name = "All", Posts = _postRepo.Select.ToList() });
return View(new BlogListViewModel {
CurrentCategory = categoryId == 0 ? categories[0] : categories.First(a => a.Id == categoryId),
CurrentCategoryId = categoryId,
Categories = categories,
Posts = _postService.GetPagedList(new PostQueryParameters {
CategoryId = categoryId,
Page = page,
PageSize = pageSize,
OnlyPublished = true
})
});
}
傳入引數只需要三個:
- 分類ID
- 當前頁面
- 頁面大小
這個介面要做的事比較多
- 獲取所有分類
- 判斷當前分類
- 獲取文章列表
最終返回我們前面定義的BlogListViewModel
然後在頁面模板裡就可以用了。
View改造
第一件事把model換成BlogListViewModel
然後就是根據ViewModel裡的資料進行頁面渲染,都是Bootstrap提供的頁面元件,程式碼比較長我就不貼了,頁面模板的完整程式碼可以在這看到:https://github.com/Deali-Axy/StarBlog/blob/master/StarBlog.Web/Views/Blog/List.cshtml
最終效果
截了個長圖,最終的頁面效果就是這樣了~
小結
如果你看到了這裡,說明你是個有耐心的人 O(∩_∩)O哈哈,同時對本專案是比較感興趣的,先感謝大家的支援
本文一不小心就寫得比較長了,本來是想以那種每篇文章比較短的形式做一個連載,這樣讀起來不會有太大的壓力,沒想到稍微一展開講就涉及到很多內容,接下來的文章我得優化優化~
最近一段時間,公眾號後臺、微信都有收到朋友的催更,或者是抱怨我更新得太慢,實在是抱歉,最近被工作上的事情搞得有點暈頭轉向的,下班回家後晚上就只想看會書或者玩一下游戲放鬆,懈怠了,看到有這麼多大佬在關注我的專案,頓時又充滿動力了!沖沖衝,接下來爭取每兩天更新一篇,歡迎繼續關注~