基於.NetCore開發部落格專案 StarBlog - (6) 頁面開發之部落格文章列表

程式設計實驗室發表於2022-05-18

系列文章

前言

前一篇文章把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,可以看到文章列表如下,很簡單(簡陋),而且全部文章都顯示出來了,頁面很長,這很明顯並不是我們想要的最終效果。

image

不急,接下來慢慢來優化。

分頁

首先是頁面把全部文章都顯示出來的問題,我們需要引入分頁功能

分頁可以自己實現,也可以用第三方元件,我們用的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&amp;categoryId=0">2</a></li>
        <li class="page-item"><a class="page-link" href="/Blog/List?page=3&amp;categoryId=0">3</a></li>
        <li class="page-item"><a class="page-link" href="/Blog/List?page=4&amp;categoryId=0">4</a></li>
        <li class="page-item"><a class="page-link" href="/Blog/List?page=5&amp;categoryId=0">5</a></li>
        <li class="page-item"><a class="page-link" href="/Blog/List?page=6&amp;categoryId=0">6</a></li>
        <li class="page-item"><a class="page-link" href="/Blog/List?page=7&amp;categoryId=0">7</a></li>
        <li class="page-item"><a class="page-link" href="/Blog/List?page=8&amp;categoryId=0">8</a></li>
        <li class="page-item"><a class="page-link" href="/Blog/List?page=9&amp;categoryId=0">9</a></li>
        <li class="page-item"><a class="page-link" href="/Blog/List?page=10&amp;categoryId=0">10</a></li>
        <li class="PagedList-ellipses page-item"><a class="PagedList-skipToNext page-link" href="/Blog/List?page=11&amp;categoryId=0" rel="next">…</a></li>
        <li class="PagedList-skipToNext page-item"><a class="page-link" href="/Blog/List?page=2&amp;categoryId=0" rel="next">&gt;</a></li>
        <li class="PagedList-skipToLast page-item"><a class="page-link" href="/Blog/List?page=64&amp;categoryId=0">&gt;&gt;</a></li>
    </ul>
</div>

顯示效果:

image

請求引數封裝

前面介紹的分頁需要在訪問頁面時傳入請求引數,這樣我們Controller的Action方法就需要加上pageNumberpageSize這兩個引數,後面還要加文章分類篩選和搜尋排序什麼的,這樣引數太多了,全都寫在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.WebViewModels目錄下,新建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

最終效果

截了個長圖,最終的頁面效果就是這樣了~

image

小結

如果你看到了這裡,說明你是個有耐心的人 O(∩_∩)O哈哈,同時對本專案是比較感興趣的,先感謝大家的支援

本文一不小心就寫得比較長了,本來是想以那種每篇文章比較短的形式做一個連載,這樣讀起來不會有太大的壓力,沒想到稍微一展開講就涉及到很多內容,接下來的文章我得優化優化~

最近一段時間,公眾號後臺、微信都有收到朋友的催更,或者是抱怨我更新得太慢,實在是抱歉,最近被工作上的事情搞得有點暈頭轉向的,下班回家後晚上就只想看會書或者玩一下游戲放鬆,懈怠了,看到有這麼多大佬在關注我的專案,頓時又充滿動力了!沖沖衝,接下來爭取每兩天更新一篇,歡迎繼續關注~

相關文章