公众号:【可乐前端】,每天3分钟学习一个优秀的开源项目,分享web面试与实战知识。
前言
前面我们已经实现了文章发布与文章管理功能,今天我们来实现首页的文章流列表以及查看文章详情的功能。
往期文章
- 切图仔做全栈:React&Nest.js 社区平台(一)------基础架构与邮箱注册、JWT 登录实现
- 切图仔做全栈:React&Nest.js社区平台(二)------👋手把手实现优雅的鉴权机制
- React&Nest.js全栈社区平台(三)------🐘对象存储是什么?为什么要用它?
- React&Nest.js社区平台(四)------✏️文章发布与管理实战
后端实现
对于文章列表来说,无论前端以什么样的形式去展现,表格也好,滚动刷新的列表也好,它本质上是一个分页的需求。
对于分页的需求相信每个前端都不陌生,你平时对接的时候把页码跟每一页的条数提供给后端后,后端会根据这些信息取出对应的条数返回给前端。
比如下面的 sql
语句,它指的是在 users
表中,从 第10条
开始,取 10条
数据,大多数的分页场景都是基于这个 sql
语句。
sql
SELECT * FROM users offset 10 limit 10;
这一期我们会封装一个通用的分页 service
,接受任意一个 entity
对象,去查询分页信息。
通用分页Service封装
首先先定义一下分页 service
的返回值:
ts
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
实现如下:
ts
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
中实现一下获取分页文章列表的方法。
ts
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
的列表组件来实现一个滚动加载的分页列表。
ts
<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
这个库,当然你可以搭配别的选择,或者自研一个。
至于样式什么的,就看你自己自由发挥了。
ts
<div className={styles.content}>
<ReactMarkdown children={article.content} />
</div>
最后
以上就是本文的全部内容,我们一起封装了一个通用的分页类,它可以方便的在各个 Entity
中实现分页的需求,然后我们还实现了文章流和文章详情,到这里,我们的社区平台基本上也算是做到了管理文章+查看文章的功能。
如果你觉得有意思的话,点点关注点点赞吧~欢迎在评论区交流