前言
前面我们已经实现了文章发布与文章管理功能,今天我们来实现首页的文章流列表以及查看文章详情的功能。
往期文章
仓库地址
- 切图仔做全栈:React&Nest.js 社区平台(一)——基础架构与邮箱注册、JWT 登录实现
- 切图仔做全栈:React&Nest.js社区平台(二)——👋手把手实现优雅的鉴权机制
- React&Nest.js全栈社区平台(三)——🐘对象存储是什么?为什么要用它?
- React&Nest.js社区平台(四)——✏️文章发布与管理实战
后端实现
对于文章列表来说,无论前端以什么样的形式去展现,表格也好,滚动刷新的列表也好,它本质上是一个分页的需求。
对于分页的需求相信每个前端都不陌生,你平时对接的时候把页码跟每一页的条数提供给后端后,后端会根据这些信息取出对应的条数返回给前端。
比如下面的 sql
语句,它指的是在 users
表中,从 第10条
开始,取 10条
数据,大多数的分页场景都是基于这个 sql
语句。
SELECT * FROM users offset 10 limit 10;
这一期我们会封装一个通用的分页 service
,接受任意一个 entity
对象,去查询分页信息。
通用分页Service封装
首先先定义一下分页 service
的返回值:
export class PaginationResult<T> { total: number; pageSize: number; currentPage: number; totalPage: number; isEnd: boolean; list: T[]; }
它接收一个范型参数 T
,对应的是传入的 entity
对象,其他的参数解释如下:
total
:总条数pageSize
:每页条数currentPage
:当前页totalPage
:总共有多少页isEnd
:是否还有下一页list
:查询出来的列表对象
整个分页 service
实现如下:
export class PaginationService<T> { constructor(private repository: Repository<T>) {} async paginate(params: { page: number; pageSize: number; options?: FindOneOptions<T>; }): Promise<PaginationResult<T>> { const { page, pageSize, options = {} } = params; const [result, total] = await this.repository.findAndCount({ take: pageSize, skip: pageSize * (page - 1), ...options, }); const paginationResult = new PaginationResult<T>(); paginationResult.list = result; paginationResult.total = total; paginationResult.pageSize = pageSize; paginationResult.currentPage = page; paginationResult.totalPage = Math.ceil(total / pageSize); paginationResult.isEnd = paginationResult.totalPage === page; return paginationResult; } }
解释一下上面的代码:
- 整个类接收一个范型对象
T
,它对应的是需要查询的entity
对象 - 构造函数中接收一个
repository
,它是范型T(entity对象)
所对应的repository
- 首先接收页码参数
page
,每一页的条数pageSize
,以及一个拓展查询条件options
。options
的类型是TypeORM
中的FindOneOptions
- 使用
findAndCount
查询出当前条件的条数以及结果,其中take
对应原生sql
语句的limit
,skip
对应原生sql
语句的offset
- 最后组装一下参数返回给调用方
文章列表接口
有了上面这个通用的分页器之后,我们可以在 artice.service
中实现一下获取分页文章列表的方法。
async getArticles(params: { page: number; pageSize: number }) { const paginationService = new PaginationService<ArticleEntity>( this.articleRepository, ); const res = await paginationService.paginate({ ...params, options: { select: ['id', 'categoryId', 'introduction', 'title', 'creatorName'], where: { status: 1, isDeleted: 0 }, }, }); return res; }
使用 ArticleEntity
跟注入的 articleRepository
来 new
一个分页 service
,然后调用它的 paginate
方法。
这里把前端传过来的页码和每页的条数传进去,别忘了只能把已发布的文章 (status=1)
和未删除的文章查出来 (is_deleted=0)
值得一提的是,因为我们使用了范型以及对 options
定义了 FindOneOptions
这个类型,所以在开发的过程中, ts
的类型推断可以帮我们自动提示一些东西。
比如说我要选择某一些字段,在 select
数组中书写时,会对应提示 ArticleEntity
里有的字段,十分的方便。
整个接口的返回值如下图所示:
前端实现
前端方面使用 react-infinite-scroll-component
这个无限滚动组件配合 antd
的列表组件来实现一个滚动加载的分页列表。
<div id="scrollableDiv" className={styles.list}> <InfiniteScroll dataLength={article.total} next={() => setPageNo(pageNo + 1)} hasMore={!article.isEnd} loader={<Skeleton avatar paragraph={{ rows: 1 }} active />} endMessage={<Divider plain>到底了~</Divider>} scrollableTarget="scrollableDiv" > <List dataSource={article.list} renderItem={(item: any) => ( <List.Item className={styles.listItem} onClick={() => navigate(`/detail?id=${item.id}`)} key={item.id} > <List.Item.Meta title={item.title} description={ <> <div style={{ margin: "8px 0" }}>{item.introduction}</div> <Tag>{categoryMap[item.categoryId]}</Tag> </> } /> </List.Item> )} /> </InfiniteScroll> </div>
当页面滚动到底部时,会去拉取下一页的列表,同时根据我们上面 service
中返回的 isEnd
字段判断列表是否已经完全加载完毕。
有了文章列表之后必不可少的当然是文章详情,文章详情的接口就是根据文章 id
去查询一条记录,代码比较简单,这里就不放出来了。
前端要做的事情就是解析数据库里面存储的 markdown
内容为 html
,然后渲染到页面上,这里我使用的是 react-markdown
这个库,当然你可以搭配别的选择,或者自研一个。
至于样式什么的,就看你自己自由发挥了。
<div className={styles.content}> <ReactMarkdown children={article.content} /> </div>
最后
以上就是本文的全部内容,我们一起封装了一个通用的分页类,它可以方便的在各个 Entity
中实现分页的需求,然后我们还实现了文章流和文章详情,到这里,我们的社区平台基本上也算是做到了管理文章+查看文章的功能。
如果你觉得有意思的话,点点关注点点赞吧~欢迎在评论区交流