React&Nest.js全栈社区平台(五)——👋封装通用分页Service实现文章流与详情

2 阅读4分钟

公众号:【可乐前端】,每天3分钟学习一个优秀的开源项目,分享web面试与实战知识。

前言

前面我们已经实现了文章发布与文章管理功能,今天我们来实现首页的文章流列表以及查看文章详情的功能。

往期文章

仓库地址

后端实现

对于文章列表来说,无论前端以什么样的形式去展现,表格也好,滚动刷新的列表也好,它本质上是一个分页的需求。

对于分页的需求相信每个前端都不陌生,你平时对接的时候把页码跟每一页的条数提供给后端后,后端会根据这些信息取出对应的条数返回给前端。

比如下面的 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 ,以及一个拓展查询条件 optionsoptions 的类型是 TypeORM 中的 FindOneOptions
  • 使用 findAndCount 查询出当前条件的条数以及结果,其中 take 对应原生 sql 语句的 limitskip 对应原生 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 跟注入的 articleRepositorynew 一个分页 service ,然后调用它的 paginate 方法。

这里把前端传过来的页码和每页的条数传进去,别忘了只能把已发布的文章 (status=1) 和未删除的文章查出来 (is_deleted=0)

值得一提的是,因为我们使用了范型以及对 options 定义了 FindOneOptions 这个类型,所以在开发的过程中, ts 的类型推断可以帮我们自动提示一些东西。

比如说我要选择某一些字段,在 select 数组中书写时,会对应提示 ArticleEntity 里有的字段,十分的方便。

image.png

整个接口的返回值如下图所示:

image.png

前端实现

前端方面使用 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 字段判断列表是否已经完全加载完毕。

image.png

image.png

有了文章列表之后必不可少的当然是文章详情,文章详情的接口就是根据文章 id 去查询一条记录,代码比较简单,这里就不放出来了。

前端要做的事情就是解析数据库里面存储的 markdown 内容为 html ,然后渲染到页面上,这里我使用的是 react-markdown 这个库,当然你可以搭配别的选择,或者自研一个。

至于样式什么的,就看你自己自由发挥了。

<div className={styles.content}>
    <ReactMarkdown children={article.content} />
</div>

image.png

最后

以上就是本文的全部内容,我们一起封装了一个通用的分页类,它可以方便的在各个 Entity 中实现分页的需求,然后我们还实现了文章流和文章详情,到这里,我们的社区平台基本上也算是做到了管理文章+查看文章的功能。

如果你觉得有意思的话,点点关注点点赞吧~欢迎在评论区交流