系列文章
- 基于.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哈哈,同时对本项目是比较感兴趣的,先感谢大家的支持
本文一不小心就写得比较长了,本来是想以那种每篇文章比较短的形式做一个连载,这样读起来不会有太大的压力,没想到稍微一展开讲就涉及到很多内容,接下来的文章我得优化优化~
最近一段时间,公众号后台、微信都有收到朋友的催更,或者是抱怨我更新得太慢,实在是抱歉,最近被工作上的事情搞得有点晕头转向的,下班回家后晚上就只想看会书或者玩一下游戏放松,懈怠了,看到有这么多大佬在关注我的项目,顿时又充满动力了!冲冲冲,接下来争取每两天更新一篇,欢迎继续关注~