返回

ABP vNext 对接 Ant Design Vue 实现分页查询

上一篇 博客中,博主和大家分享了如何在 EF Core 中实现多租户架构。在这一过程中,博主主要参考了 ABP vNext 这个框架。从上个月开始,我个人发起了一个项目,基于 ABP vNextAnt Design Vue 来实现一个通用的后台管理系统,希望以此来推进 DDDVue 的学习,努力打通前端与后端的“任督二脉”。因此,接下来的这段时间内,我写作的主题将会围绕 ABP vNext 和 Ant Design Vue。而在今天的这篇博客中,我们来说说 ABP vNext 对接 Ant Design Vue 实现分页查询的问题,希望能让大家在面对类似问题时有所帮助。我不打算写一个系列教程,更多的是从我个人的关注点出发,如果大家有更多想要交流的话题,欢迎大家通过评论或者邮件来留言,谢谢大家!

ABP vNext中的分页查询

OK,当大家接触过 ABP vNext 以后,就会了解到这样一件事情,即,ABP vNext 中默认提供的分页查询接口,在大多数情况下,通常都会是下面这样的风格。这里以角色查询的接口为例,它对应的请求地址是:/api/identity/roles?SkipCount=0&MaxResultCount=10。此时,我们可以注意到,返回的数据结构中含有totalCountitems两个属性。其中,totalCount表示记录的总数目,items表示当前页对应的记录。

 1{
 2  "totalCount": 2,
 3  "items": [
 4    {
 5      "name": "Admin",
 6      "isDefault": false,
 7      "isStatic": true,
 8      "isPublic": true,
 9      "concurrencyStamp": "cb53f2d7-159e-452d-9d9c-021629b500e0",
10      "id": "39fb19e8-fb34-dfbd-3c70-181f604fd5ff",
11      "extraProperties": {}
12    },
13    {
14      "name": "Manager",
15      "isDefault": false,
16      "isStatic": false,
17      "isPublic": false,
18      "concurrencyStamp": "145ec550-7fe7-4c80-85e3-f317a168e6b6",
19      "id": "39fb6216-2803-20c6-7211-76f8fe38b90e",
20      "extraProperties": {}
21    }
22  ]
23}

事实上,ABP vNext 中自带的分页查询,主要是通过SkipCountMaxResultCount两个参数来实现。假设MaxResultCount,即分页大小为m,则第n页对应的SkipCount应该为(n-1) * m。如果大家对于LINQ非常熟悉的话,应该可以自然而然地联想到Skip()Take()两个方法,这是一个非常自然的联想,因为 ABP vNext 就是这样实现分页查询的。这里以博主的“数据字典”分页查询接口为例:

 1public async Task<PagedResultDto<DataDictionaryQueryDto>> GetCategories(
 2    GetDataDictionaryRequestInput input
 3)
 4{
 5  var totalCount = (await _dataDictRepository.GetQueryableAsync())
 6    .WhereIf(!string.IsNullOrEmpty(input.Name), x => x.Name.Contains(input.Name) || x.Name == input.Name)
 7    .WhereIf(!string.IsNullOrEmpty(input.Description), x => x.Description.Contains(input.Description) || x.Description == input.Description)
 8    .Count();
 9
10  var items = (await _dataDictRepository.GetQueryableAsync())
11    .WhereIf(!string.IsNullOrEmpty(input.Name), x => x.Name.Contains(input.Name) || x.Name == input.Name)
12    .WhereIf(!string.IsNullOrEmpty(input.Description), x => x.Description.Contains(input.Description) || x.Description == input.Description)
13    .Skip(input.SkipCount)
14    .Take(input.MaxResultCount)
15    .ToList();
16
17    return new PagedResultDto<DataDictionaryQueryDto>()
18    {
19      TotalCount = totalCount,
20      Items = ObjectMapper.Map<List<DataDictionary>, List<DataDictionaryQueryDto>>(items)
21    };
22}

可以注意到,在 ABP vNext 中我们只需要构造好TotalCountItems这两个属性即可。

STable组件中的分页查询

接下来,在 Ant Design Vue 的 Pro 版本中,我们使用STable组件来展示列表类的数据,关于这个组件的使用方法,大家可以参考 官方文档。按照最小化可行产品(MVP)的理念,一个最简单的STable组件的使用,如下面所示:

 1<template>
 2  <s-table
 3    ref="table"
 4    size="default"
 5    :rowKey="(record) => record.data.id"
 6    :columns="columns"
 7    :data="loadData"
 8    :rowSelection="{ selectedRowKeys: selectedRowKeys, onChange: onSelectChange }"
 9  >
10  </s-table>
11</template>

对于这个组件而言,其中最重要的地方当属data属性,它接受一个函数,该函数的返回值为Promise对象,并且有一个参数:

 1<script>
 2  import STable from '@/components'
 3
 4  export default {
 5    components: {
 6      STable
 7    },
 8    data() {
 9      return {
10        // 表格列名
11        columns: [],
12        // 查询条件
13        queryParam: { },
14        // 加载数据方法,必须为 Promise 对象
15        loadData: parameter => {
16          return getRoles(Object.assign({}, this.queryParam, parameter))
17            .then(res => {
18              return res.result
19            })
20        },
21        // ...
22        selectedRowKeys: [],
23        selectedRows: []
24      }
25    }
26  }
27</script>

也许,你会好奇这个parameter到底是个什么东西?可如果我们将其打印出来,就会发现它其实是分页查询相关的参数:Object { pageNo: 1, pageSize: 10 },而更进一步,如果深入到这个组件的源代码中,我们会注意到组件内部有一个loadData()方法:

 1loadData (pagination, filters, sorter) {
 2  this.localLoading = true
 3  const parameter = Object.assign({
 4    pageNo: (pagination && pagination.current) ||
 5      this.showPagination && this.localPagination.current || this.pageNum,
 6    pageSize: (pagination && pagination.pageSize) ||
 7      this.showPagination && this.localPagination.pageSize || this.pageSize
 8    },
 9    (sorter && sorter.field && {
10      sortField: sorter.field
11    }) || {},
12    (sorter && sorter.order && {
13      sortOrder: sorter.order
14    }) || {}, {
15    ...filters
16    }
17  )
18  const result = this.data(parameter)
19  // 对接自己的通用数据接口需要修改下方代码中的 r.pageNo, r.totalCount, r.data

可以注意到,在STable组件内部,它会将分页、排序和过滤三种不同类型的参数,通过Object.assign()方法聚合到一个对象上,这个对象实际上就是我们刚刚打印出来的parameter。为什么这样说呢?因为它接下来就要调用data属性指向的方法啦!还记得这个data是什么吗?不错,它是一个函数,既然是一个函数,当然可以直接调用。到这里,我们可以获得第一个信息,即,ABP vNext 中的表格组件STable,本身封装了分页查询相关的参数,只要将这些参数传递给后端就可以实现分页查询

实现参数转换层

既然,这个参数和 ABP vNext 需要的参数不同,为了不修改已有的接口,我们考虑在这中间加一层转换。为此,我们定义下面的函数:

 1// 默认列表查询条件
 2const baseListQuery = {
 3  page: 1,
 4  limit: 20
 5}
 6
 7// 查询条件转化
 8export function transformAbpListQuery (query) {
 9  query.filter = query.filter === '' ? undefined : query.filter
10
11  if (window.isNaN(query.pageSize)) {
12    query.pageSize = baseListQuery.limit
13  }
14  if (window.isNaN(query.pageNo)) {
15    query.pageNo = baseListQuery.page
16  }
17
18  const abpListQuery = {
19    maxResultCount: query.pageSize,
20    skipCount: (query.pageNo - 1) * query.pageSize,
21    sorting: '',
22    filter: '',
23    ...query
24  }
25
26  if (typeof (query.sortField) !== 'undefined' && query.sortField !== null) {
27    abpListQuery.sorting = query.sortOrder === 'ascend'
28      ? query.sortField
29      : `${query.sortField} Desc`
30  }
31
32  return abpListQuery
33}

代码非常简单,通过transformAbpListQuery函数,我们就实现了从STableABP vNext的参数转换。需要说明的是,这里的排序使用到了 System.Linq.Dynamic.Core 这个库,它可以实现IQueryable级别的、基于字符串的动态表达式构建功能,使用方法如下:

1var resultSingle = queryable.OrderBy<User>("NumberProperty");
2var resultSingleDescending = queryable.OrderBy<User>("NumberProperty DESC");
3var resultMultiple = queryable.OrderBy<User>("NumberProperty, StringProperty");

所以,当它为降序排序时,我们在排序字段的后面添加DESC即可。关于filter参数,我准备做一套通用性更强的方案,所以,这里就暂时留空啦!接下来,如果大家足够细心的话,会发现STable组件对返回值同样有一定的要求,它要求返回值中至少含有pageNototalCount, data三个属性,而这,是我们获得的第二个信息:

 1// 对接自己的通用数据接口需要修改下方代码中的 r.pageNo, r.totalCount, r.data
 2// eslint-disable-next-line
 3if ((typeof result === 'object' || typeof result === 'function') 
 4  && typeof result.then === 'function') {
 5  result.then(r => {
 6    this.localPagination = this.showPagination 
 7    && Object.assign({}, this.localPagination, {
 8      current: r.pageNo, // 返回结果中的当前分页数
 9      total: r.totalCount, // 返回结果中的总记录数
10      showSizeChanger: this.showSizeChanger,
11      pageSize: (pagination && pagination.pageSize) ||
12      this.localPagination.pageSize
13    }) || false
14
15    this.localDataSource = r.data // 返回结果中的数组数据
16    this.localLoading = false
17  })
18}

依样画葫芦,我们继续编写转换层的代码,返回值格式参考了 Ant Design Vue 中Mock接口的返回值格式:

 1// 查询结果转化
 2export function transformAbpQueryResult (data, message, code = 0, headers = {}) {
 3  const responseBody = { }
 4  responseBody.result = data
 5  if (message !== undefined && message !== null) {
 6    responseBody.message = message
 7  }
 8  if (code !== undefined && code !== 0) {
 9    responseBody.code = code
10    responseBody._status = code
11  }
12  if (headers !== null && typeof headers === 'object' 
13    && Object.keys(headers).length > 0) {
14    responseBody._headers = headers
15  }
16  responseBody.timestamp = new Date().getTime()
17  return responseBody
18}
19
20// 分页查询结果转化
21export function buildPagingQueryResult (queryParam, data) {
22  for (const item of data.items) {
23    // Ant Design Vue 中要求每行数据中必须存在字段:key
24    item.key = item.id
25  }
26  const pagedResult = {
27    pageSize: queryParam.pageSize,
28    pageNo: queryParam.pageNo,
29    totalCount: data.totalCount,
30    totalPage: data.totalCount / queryParam.pageSize,
31    data: data.items
32  }
33  return transformAbpQueryResult(pagedResult)
34}

对于分页结果而言,我们会将分页大小、当前页数、总页数、总记录数及其对应的数据,统一封装到一个对象中,然后再将其传递给返回值中的result属性。

最终对接效果

好了,写了这么多,我们到底实现了一个什么效果呢?对于一开始的角色查询接口,我们可以这样封装到前端的服务层:

 1export function getRoles (query) {
 2    const queryParam = transformAbpListQuery(query)
 3    return axios({
 4      url: AppConsts.resourceService.baseUrl + '/api/identity/roles',
 5      method: 'get',
 6      params: queryParam
 7    }).then(data => {
 8        return buildPagingQueryResult(queryParam, data)
 9    })
10}

接下来,我们只需要实现loadData()方法即可:

1import { getRoles, updateRole, createRole, deleteRole } from '@/api/recipe/abp.role'
2
3loadData: parameter => {
4  return getRoles(Object.assign({}, parameter, this.queryParam))
5    .then(res => {
6      return res.result
7    })
8  },

此时,我们可以注意到,ABP vNext 与 Ant Design Vue 完美地集成在一起,并且参数的转换完全符合我们的预期。这样做的好处显而易见,我们只需要遵循 ABP vNext 的规范进行开发即可,考虑到 ABP vNext 可以直接将ApplicationService暴露为 API 接口,这意味着我们写完了接口,就可以立即开始前后端的联调工作,这无疑可以加快我们的研发效率!

ABP vNext 与 Ant Design Vue 完成整合
ABP vNext 与 Ant Design Vue 完成整合

好了,以上就是这篇博客的全部内容啦!这篇博客要实现的功能其实并不复杂,唯一的难点是,需要在前端和后端两个技术栈上频繁地切换上下文,这可能就是全栈开发者面临的最大挑战,因为技术世界浩如烟海,而一个人的精力终究有限,古人云:朝闻道,夕死可矣,人生百年,吾道不孤,还是请你继续努力哦!

Built with Hugo
Theme Stack designed by Jimmy