惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Security @ Cisco Blogs
The Last Watchdog
The Last Watchdog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
aimingoo的专栏
aimingoo的专栏
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
PCI Perspectives
PCI Perspectives
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
月光博客
月光博客
V
Visual Studio Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
T
Tailwind CSS Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
L
LangChain Blog
B
Blog RSS Feed
小众软件
小众软件
N
News | PayPal Newsroom
Attack and Defense Labs
Attack and Defense Labs
Microsoft Azure Blog
Microsoft Azure Blog
V
Vulnerabilities – Threatpost
The Hacker News
The Hacker News
T
Tor Project blog
A
Arctic Wolf
Jina AI
Jina AI
Hacker News: Ask HN
Hacker News: Ask HN
F
Fortinet All Blogs
Cloudbric
Cloudbric
S
Secure Thoughts
L
LINUX DO - 热门话题
博客园 - 司徒正美
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
S
Security Affairs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
J
Java Code Geeks
P
Privacy International News Feed
AWS News Blog
AWS News Blog
S
Securelist
TaoSecurity Blog
TaoSecurity Blog
AI
AI
O
OpenAI News
C
Cyber Attacks, Cyber Crime and Cyber Security
K
Kaspersky official blog
T
The Blog of Author Tim Ferriss
大猫的无限游戏
大猫的无限游戏
Google DeepMind News
Google DeepMind News
Know Your Adversary
Know Your Adversary
P
Palo Alto Networks Blog
T
Tenable Blog
Last Week in AI
Last Week in AI
WordPress大学
WordPress大学
S
SegmentFault 最新的问题

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角 .NET Core 原生 DI 扩展之基于名称的注入实现 - 元视角
ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角
飞鸿踏雪 · 2021-04-08 · via 元视角

上一篇 博客中,博主和大家分享了如何在 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表示当前页对应的记录。

{
  "totalCount": 2,
  "items": [
    {
      "name": "Admin",
      "isDefault": false,
      "isStatic": true,
      "isPublic": true,
      "concurrencyStamp": "cb53f2d7-159e-452d-9d9c-021629b500e0",
      "id": "39fb19e8-fb34-dfbd-3c70-181f604fd5ff",
      "extraProperties": {}
    },
    {
      "name": "Manager",
      "isDefault": false,
      "isStatic": false,
      "isPublic": false,
      "concurrencyStamp": "145ec550-7fe7-4c80-85e3-f317a168e6b6",
      "id": "39fb6216-2803-20c6-7211-76f8fe38b90e",
      "extraProperties": {}
    }
  ]
}

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

public async Task<PagedResultDto<DataDictionaryQueryDto>> GetCategories(
    GetDataDictionaryRequestInput input
)
{
  var totalCount = (await _dataDictRepository.GetQueryableAsync())
    .WhereIf(!string.IsNullOrEmpty(input.Name), x => x.Name.Contains(input.Name) || x.Name == input.Name)
    .WhereIf(!string.IsNullOrEmpty(input.Description), x => x.Description.Contains(input.Description) || x.Description == input.Description)
    .Count();

  var items = (await _dataDictRepository.GetQueryableAsync())
    .WhereIf(!string.IsNullOrEmpty(input.Name), x => x.Name.Contains(input.Name) || x.Name == input.Name)
    .WhereIf(!string.IsNullOrEmpty(input.Description), x => x.Description.Contains(input.Description) || x.Description == input.Description)
    .Skip(input.SkipCount)
    .Take(input.MaxResultCount)
    .ToList();

    return new PagedResultDto<DataDictionaryQueryDto>()
    {
      TotalCount = totalCount,
      Items = ObjectMapper.Map<List<DataDictionary>, List<DataDictionaryQueryDto>>(items)
    };
}

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

STable组件中的分页查询

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

<template>
  <s-table
    ref="table"
    size="default"
    :rowKey="(record) => record.data.id"
    :columns="columns"
    :data="loadData"
    :rowSelection="{ selectedRowKeys: selectedRowKeys, onChange: onSelectChange }"
  >
  </s-table>
</template>

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

<script>
  import STable from '@/components'

  export default {
    components: {
      STable
    },
    data() {
      return {
        // 表格列名
        columns: [],
        // 查询条件
        queryParam: { },
        // 加载数据方法,必须为 Promise 对象
        loadData: parameter => {
          return getRoles(Object.assign({}, this.queryParam, parameter))
            .then(res => {
              return res.result
            })
        },
        // ...
        selectedRowKeys: [],
        selectedRows: []
      }
    }
  }
</script>

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

loadData (pagination, filters, sorter) {
  this.localLoading = true
  const parameter = Object.assign({
    pageNo: (pagination && pagination.current) ||
      this.showPagination && this.localPagination.current || this.pageNum,
    pageSize: (pagination && pagination.pageSize) ||
      this.showPagination && this.localPagination.pageSize || this.pageSize
    },
    (sorter && sorter.field && {
      sortField: sorter.field
    }) || {},
    (sorter && sorter.order && {
      sortOrder: sorter.order
    }) || {}, {
    ...filters
    }
  )
  const result = this.data(parameter)
  // 对接自己的通用数据接口需要修改下方代码中的 r.pageNo, r.totalCount, r.data

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

实现参数转换层

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

// 默认列表查询条件
const baseListQuery = {
  page: 1,
  limit: 20
}

// 查询条件转化
export function transformAbpListQuery (query) {
  query.filter = query.filter === '' ? undefined : query.filter

  if (window.isNaN(query.pageSize)) {
    query.pageSize = baseListQuery.limit
  }
  if (window.isNaN(query.pageNo)) {
    query.pageNo = baseListQuery.page
  }

  const abpListQuery = {
    maxResultCount: query.pageSize,
    skipCount: (query.pageNo - 1) * query.pageSize,
    sorting: '',
    filter: '',
    ...query
  }

  if (typeof (query.sortField) !== 'undefined' && query.sortField !== null) {
    abpListQuery.sorting = query.sortOrder === 'ascend'
      ? query.sortField
      : `${query.sortField} Desc`
  }

  return abpListQuery
}

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

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

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

// 对接自己的通用数据接口需要修改下方代码中的 r.pageNo, r.totalCount, r.data
// eslint-disable-next-line
if ((typeof result === 'object' || typeof result === 'function') 
  && typeof result.then === 'function') {
  result.then(r => {
    this.localPagination = this.showPagination 
    && Object.assign({}, this.localPagination, {
      current: r.pageNo, // 返回结果中的当前分页数
      total: r.totalCount, // 返回结果中的总记录数
      showSizeChanger: this.showSizeChanger,
      pageSize: (pagination && pagination.pageSize) ||
      this.localPagination.pageSize
    }) || false

    this.localDataSource = r.data // 返回结果中的数组数据
    this.localLoading = false
  })
}

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

// 查询结果转化
export function transformAbpQueryResult (data, message, code = 0, headers = {}) {
  const responseBody = { }
  responseBody.result = data
  if (message !== undefined && message !== null) {
    responseBody.message = message
  }
  if (code !== undefined && code !== 0) {
    responseBody.code = code
    responseBody._status = code
  }
  if (headers !== null && typeof headers === 'object' 
    && Object.keys(headers).length > 0) {
    responseBody._headers = headers
  }
  responseBody.timestamp = new Date().getTime()
  return responseBody
}

// 分页查询结果转化
export function buildPagingQueryResult (queryParam, data) {
  for (const item of data.items) {
    // Ant Design Vue 中要求每行数据中必须存在字段:key
    item.key = item.id
  }
  const pagedResult = {
    pageSize: queryParam.pageSize,
    pageNo: queryParam.pageNo,
    totalCount: data.totalCount,
    totalPage: data.totalCount / queryParam.pageSize,
    data: data.items
  }
  return transformAbpQueryResult(pagedResult)
}

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

最终对接效果

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

export function getRoles (query) {
    const queryParam = transformAbpListQuery(query)
    return axios({
      url: AppConsts.resourceService.baseUrl + '/api/identity/roles',
      method: 'get',
      params: queryParam
    }).then(data => {
        return buildPagingQueryResult(queryParam, data)
    })
}

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

import { getRoles, updateRole, createRole, deleteRole } from '@/api/recipe/abp.role'

loadData: parameter => {
  return getRoles(Object.assign({}, parameter, this.queryParam))
    .then(res => {
      return res.result
    })
  },

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

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

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