基於.NetCore開發部落格專案 StarBlog - (23) 文章列表介面分頁、過濾、搜尋、排序

程式設計實驗室發表於2022-12-19

前言

上一篇留的坑,火速補上。

在之前的第6篇中,已經有初步介紹,本文做一些補充,已經搞定這部分的同學可以快速跳過,基於.NetCore開發部落格專案 StarBlog - (6) 頁面開發之部落格文章列表

對標準的WebApi來說,分頁、過濾、搜尋、排序是很常見的功能,既可以方便使用者檢視資料,又可以提升程式效能。

通用請求引數

定義一個類來作為通用的請求引數

列表介面通用的引數是這幾個:PageSize, Page, Search, SortBy

反映到URL上,就是 Blog/?pageSize=10&page=1&search=關鍵詞 這樣的形式

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; }
}

文章列表請求引數

在通用請求引數 QueryParameters 的基礎上,派生出文章列表的請求引數類 PostQueryParameters

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";
}

在通用請求引數的基礎上,增加文章相關的篩選欄位。

SortBy 欄位使用 new 關鍵詞覆蓋基類屬性,設定為預設排序是最後更新時間,前面加個減號表示倒序。

service

StarBlog.Web/Services/PostService.cs 中封裝獲取分頁列表的方法

程式碼裡有註釋,比較容易,根據 PostQueryParameters 中的各種引數來做過濾篩選

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);
}

搜尋的實現

在上面 service 的程式碼中

可以看到搜尋只是簡單的“關鍵詞過濾”

使用 Title.Contains(param.Search) ,轉換成SQL就是

select * from post where title like '%關鍵詞%'

單純判斷標題字串中是否包含有關鍵詞的子串。

這對於簡單搜尋一下文章是夠用的,如果要像谷歌、百度這類搜尋引擎一樣能搜到文章的內容,需要用上全文檢索。

現在主流的就是 ElasticSearch 和 Solr,後續可以考慮把這個功能加入本專案~

PS:關於全文檢索,我之前寫過一篇文章:全文檢索引擎原理以及Lucene簡單介紹

同時開源了一個玩具級的全文檢索引擎,https://github.com/Deali-Axy/CloverSearch

分頁的實現

本專案使用 X.PagedList 來實現分頁功能

這個元件在結合MVC使用很方便,如果純WebApi的話,用資料庫自帶的分頁是更好的選擇,效能更好。

這個分頁元件是在 IEnumerable<T> 上新增了擴充套件方法 ToPagedList,所以在用的時候要先把資料都讀取出來,再執行分頁,效能不如在資料庫裡做好分頁再讀出來,很多ORM都支援這個功能,FreeSQL也不例外。

用法例子:

var list = fsql.Select<Topic>()
    .Where(a => a.Id > 10)
    .Count(out var total) //總記錄數量
    .Page(1, 20)
    .Tolist();

詳情請檢視FreeSQL官方文件:https://freesql.net/guide/paging.html

用上 X.PagedList 這個元件後,在任意 IEnumerable<T> 物件上執行 ToPagedList 方法,可以得到 IPagedList<T> 物件

這個物件處理當前頁面的列表資料,還有分頁資訊。

為了讓前端可以方便的使用這部分資訊,我又寫了個擴充套件方法。

StarBlog.Web/Extensions/PagedListExt.cs

public static class PagedListExt {
    public static PaginationMetadata ToPaginationMetadata(this IPagedList page) {
        return new PaginationMetadata {
            PageCount = page.PageCount,
            TotalItemCount = page.TotalItemCount,
            PageNumber = page.PageNumber,
            PageSize = page.PageSize,
            HasNextPage = page.HasNextPage,
            HasPreviousPage = page.HasPreviousPage,
            IsFirstPage = page.IsFirstPage,
            IsLastPage = page.IsLastPage,
            FirstItemOnPage = page.FirstItemOnPage,
            LastItemOnPage = page.LastItemOnPage
        };
    }

    public static string ToPaginationMetadataJson(this IPagedList page) {
        return JsonSerializer.Serialize(ToPaginationMetadata(page));
    }
}

這樣就可以在分頁後得到的 IPagedList 物件上執行 ToPaginationMetadata 得到分頁後設資料了。

這個 PaginationMetadata 也是本專案裡定義的 ViewModel,StarBlog.Web/ViewModels/PaginationMetadata.cs

程式碼如下

public class PaginationMetadata {
    public int PageCount { get; set; }
    public int TotalItemCount { get; set; }
    public int PageNumber { get; set; }
    public int PageSize { get; set; }
    public bool HasPreviousPage { get; set; }
    public bool HasNextPage { get; set; }
    public bool IsFirstPage { get; set; }
    public bool IsLastPage { get; set; }
    public int FirstItemOnPage { get; set; }
    public int LastItemOnPage { get; set; }
}

搞定

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()
    };
}

獲取到分頁資料之後,輸出 ApiResponsePaged<T> 型別的返回值

這個也是我封裝的介面返回值型別,下一篇文章會詳細介紹

Data 屬性就是列表資料,Pagination 屬性是分頁的資訊。

請求這個介面返回的效果如下

{
  "pagination": {
    "pageCount": 40,
    "totalItemCount": 394,
    "pageNumber": 1,
    "pageSize": 10,
    "hasPreviousPage": false,
    "hasNextPage": true,
    "isFirstPage": true,
    "isLastPage": false,
    "firstItemOnPage": 1,
    "lastItemOnPage": 10
  },
  "statusCode": 200,
  "successful": true,
  "message": "Get posts list",
  "data": [{...},{...},{...},{...},{...}]
}

系列文章

相關文章