跳转至

23 运营物料的后台管理:如何全栈化实现列表分页的功能?

你好,我是杨文坚。

物料功能维度,主要功能链路是物料资源管理和物料信息管理。上节课我们学习了如何管理物料产物,主要是静态资源的管理,我们把Vue.js组件编译成多种JavaScript模块化格式,然后把多个格式的JavaScript文件和CSS文件,存放到CDN服务或者私有NPM站点管理。

那今天我们就进入物料信息管理,也就是物料的数据管理,因为功能链路都集中在后台层面,我们会围绕“运营物料的后台管理”来学习。

按照之前实现用户功能的套路,我们先做功能逻辑设计,再做技术方案设计,最后实现代码。那么如何设计物料管理的功能逻辑呢?

如何设计物料管理的功能逻辑?

先梳理一下物料管理的整个功能链路需要哪些“功能点”。

按照课程运营搭建平台的设计,我们基于“按需开发”的原则,先实现核心够用的功能,后续根据实际情况再按需扩展。所以,物料管理的最基本功能,我分成了四个基础功能点。

  • 物料注册
  • 物料编辑
  • 物料列表管理
  • 物料快照列表管理

物料管理,首先要有数据才能管理。所以,物料管理第一个功能点就是“物料注册”,也就是创造物料数据

物料注册,就是根据CDN已经存在的物料产物,把名称和版本号等基础信息进行注册登记,方便后续搭建页面的时候使用。功能链路图就像这样:

注册物料的功能逻辑有三个步骤点。

  • 第一步,用户输入物料名称和版本号等信息,提交给服务端。
  • 第二步,服务端对物料名称和版本进行判断处理。
  • 第三步,服务端返回结果,浏览器显示结果。

在第二步中,服务端要根据物料名称,查询物料是否已经被注册,然后根据物料名称,查找远程CDN是否存在对应物料产物。如果存在物料产物,就判断CDN是否存在对应的版本产物,如果注册成功,就记录首次物料快照数据。

接下来看第二个功能点“物料编辑”的功能逻辑设计。

我把物料编辑功能逻辑分成四步。

  • 第一步,用户进入的物料编辑页面,渲染物料当前完整数据。
  • 第二步,用户在当前数据上编辑修改后,提交新版本号给服务端。
  • 第三步,服务端对物料新版本进行校验。
  • 第四步,浏览器处理对应的返回结果。

其中最重要是第三步,服务端要先查询物料是否已经存在数据库,判断是否可以进行编辑。然后再查询CDN是否存在对应的版本产物,再判断新版本号是否高于旧版本号。如果这些判断逻辑通过且成功修改物料数据,就记录编辑物料快照数据。

“物料注册”和“物料编辑”的功能有了,就等于有了数据积累,那么数据积累后如何显示呢?

我们基本靠列表进行显示处理,所以接下来的基本功能就是“物料列表管理”和“物料快照列表管理”。这两个的功能逻辑十分类似,我们一起分析,其中,主要的功能逻辑就是列表显示数据和分页的页码选择操作。

分页列表具体的功能逻辑有三步。

  • 第一步,进入列表页面时,默认渲染第一页的列表数据。
  • 第二步,渲染分页的页码选择器。
  • 第三步,点击页码选择器的按钮,渲染对应页码的列表数据。

比较重要的是第二、三步,第二步要根据当前页码、每页数据和数据总量,来换算和渲染分页选择器,并且提供点击操作事件。第三步,点击页码后,要触发新的列表数据请求,然后重新渲染列表,重新计算页码选择器的内容。

物料后台管理功能的逻辑设计我们就做好了,按照实现套路,接下来就是技术方案的设计了。

如何设计物料管理的技术方案?

技术方案都是基于功能逻辑的设计来定制的。

物料注册的技术方案

根据上面功能逻辑的设计描述,我先画出物料注册的技术方案图。

物料注册操作的技术方案分成四步。

第一步,前端提交“待注册”的物料名称和物料版本号。这里的物料名称,就是物料对应的Vue.js组件名称,是托管在CDN或私有NPM站点上的物料产物。在课程的代码案例中,我把物料静态资源放在mock-cdn子项目中。

在浏览器里,前端用动态表单的方式,来实现物料注册的页面交互功能。这里用动态表单,也能方便后续注册信息扩展。表单处理好数据后,就以表单形式发起POST请求给服务端。

第二步,服务端接收HTTP请求。这里会在路由层Router先做统一的用户登录态校验,如果不存在登录态就拦截,直接返回失败信息。如果存在登录态,就进入控制层Controller进入服务端其它操作逻辑。

第三步,服务端处理物料注册数据。这一步中,控制层Controller解析出当前用户信息,解析出HTTP表单中的物料信息。然后,进入业务层Service中处理数据。

业务层里,先用物料名称查询在数据库里是否存在已注册物料,接着用物料名称查询目前CDN服务,或者私有NPM站点上物料的所有版本号。如果版本号都为空,就是证明物料不存在,如果所有版本号存在,就判断当前注册版本是否存在。

如果物料和版本都存在,就进行注册操作,写入数据到物料信息表里,同时,再写一份数据到物料快照表里,记录一次物料操作变更。最后,封装对应结果返回。

第四步,浏览器处理HTTP响应结果。如果物料注册成功,就跳转到物料列表页面。如果物料注册失败,就提示失败原因。

完整的物料注册流程技术方案,我们就梳理好了,有些人会觉得注册信息太少,就只有物料名称和版本号?

其实,功能实现是“由简到难”一步步来,目前的技术方案中,我们前端用动态表单,服务端做好登录态和关键数据的校验,整个物料注册链路走通,前后端结合,把核心能力实现好,就方便我们后面做详细信息的扩展操作了。

物料编辑的技术方案

接下来设计物料编辑流程的技术方案。我画了一张图:

物料编辑比物料注册复杂一些,因为涉及数据的来回请求操作,具体的技术方案分成六个步骤。

第一步,渲染编辑物料页面。物料编辑页面的URL,携带了物料的uuid数据,通过uuid发起GET请求到服务端,查找详细的物料数据。

第二步,服务端接收请求,查询物料信息。服务端接收到HTTP请求,进行登录态校验,校验成功后才能进行下一个操作环节。下一个环节的操作,就是去查询数据库物料信息表,根据uuid查找物料信息。最后,返回物料信息给前端。

第三步,渲染待编辑的完整信息。如果前端拿到完整物料信息,就填充到动态表单,渲染完整的表单信息,方便给用户进行编辑,否则提示对应失败原因。

第四步,提交“已编辑”的物料信息,用表单格式发起POST请求,发送给服务端。

第五步,服务端接收HTTP请求,并处理已编辑信息。这一步比较关键,要分成多个环节。

首先,路由层进行登录态校验,校验成功后才能进行解析HTTP表单,把“已编辑”的物料信息提出来进行数据处理。然后,用物料名称查询在数据库里是否为已注册物料,如果是则进入下一环节校验。最后,校验新版本是否大于旧版版本号,同时判断编辑后的新版本,是否存在CDN静态资源,如果存在就进行更新物料信息数据,同时记录物料新数据到物料快照表里,记录一次物料操作变更。

第六步,浏览器处理HTTP响应结果,如果物料编辑成功,就跳转到物料列表页面。如果物料编辑失败,就提示失败原因。

物料注册和编辑的技术方案,我们就设计好了。从物料注册到编辑,你有没有觉得技术实现逻辑越来越繁琐复杂了?别心急,你可以先消化一下,打好基础,我们最后一个列表管理的技术方案设计会更复杂一点。

列表管理的技术方案

物料列表和物料快照列表的技术方案,我们统一用一套技术方案。

列表分页查询的技术方案,分成三步。

第一步,首次进入列表页的默认分页渲染。这个时候,浏览器会发起第一页列表数据的请求,请求中携带的参数有首页页码、每页数量。

第二步,服务接收到分页列表的请求数据后,首先要在路由层做用户登录态的校验。然后,根据每页“列表数据量”和“选择页码”进行换算,变成SQL查询的数据“查询起始位置”和“查询个数”,来处理数据库分页查询。

查询到数据后,进行部分数据的操作和脱敏,最后返回响应给浏览器。

第三步,前端接收到分页查询后的数据,把数据进行列表渲染和分页选择器渲染。

物料管理的四个基本功能,技术方案我们就设计好了,其中很多技术点,在用户注册和登录那节课(第21讲),你已经掌握如何实现了,接下来我们就重点看看“列表分页功能”如何实现。

如何实现物料列表分页功能?

列表分页功能,分成前端和服务端两部分。

前端实现部分

前端实现部分主要有两个渲染模块,“渲染列表”和“渲染页码选择器”。其中“渲染列表”非常简单,直接基于Vue.js的v-for语法,把数据进行循环渲染出来。“页码选择器”比较复杂,我们看效果图。

从图中可以看出,实现页码选择器需要三个数据,“列表数据量”“选中页码”和“数据总量”。我们可以基于“列表数据量”和“数据总量”计算得出“页数总量”,也就是页码的总数。

知道了页码的“页数总量”,我们就可以知道哪些页码是可以使用的。这个时候,结合当前的“选中页码”进行判断,按需计算出可以显示的页码数据就行了。具体实现方法你可以参考代码。

interface PageItem {
  num: number;
  text: string;
  active: boolean;
  disabled: boolean;
}

function parsePagination(params: {
  size: number;
  total: number;
  pageNum: number;
}) {
  const { size, total, pageNum } = params;
  const pageItemList: PageItem[] = [];
  const pageCount = Math.ceil(total / size);
  const pageStartNum: number = Math.max(1, pageNum - 2);
  const pageEndNum: number = Math.min(pageCount, pageNum + 2);

  for (let i = pageStartNum; i <= pageEndNum; i++) {
    pageItemList.push({
      num: i,
      text: `${i}`,
      active: i === pageNum,
      disabled: false
    });
  }
  pageItemList.unshift({
    num: pageNum - 1,
    text: '上一页',
    active: false,
    disabled: pageNum - 1 < pageStartNum
  });
  pageItemList.push({
    num: pageNum + 1,
    text: '下一页',
    active: false,
    disabled: pageNum + 1 > pageEndNum
  });
  return pageItemList;
}

通过这个方法,我们可以基于“列表数据量”“选中页码”和“数据总量”,换算出可以显示的页码内容,接下来渲染页码,就可以得到页码选择器。

对应的完整功能逻辑的Vue.js 3.x代码。

<template>
  <div class="pagination">
    <div class="page-list">
      <span
        class="page-item"
        :class="{ active: item.active, disabled: item.disabled }"
        v-for="(item, index) in state.pageItemList"
        :key="index"
        @click="onClickPageItem(item)"
        :data-page-num="item.num"
      >
        {{ item.text }}
      </span>
    </div>
    <span class="page-info">
      ( 共{{ state.pageCount }}页, 共{{ props.totalDataCount }}条数据 )
    </span>
  </div>
</template>

<script lang="ts" setup>
import { reactive, onMounted, watch } from 'vue';

interface PageItem {
  num: number;
  text: string;
  active: boolean;
  disabled: boolean;
}

const props = defineProps<{
  pageNum: number;
  pageSize: number;
  totalDataCount: number;
}>();

const emits = defineEmits<{
  (event: 'changePage', e: { pageNum: number }): void;
}>();

const state = reactive<{
  pageItemList: PageItem[];
  pageCount: number;
}>({
  pageItemList: [],
  pageCount: 0
});

// 挂载时候进行页码计算和渲染
onMounted(() => {
  updatePagination({
    size: props.pageSize,
    pageNum: props.pageNum,
    total: props.totalDataCount
  });
});

watch(
  [() => props.pageSize, () => props.pageNum, () => props.totalDataCount],
  ([pageSize, pageNum, totalDataCount]) => {
    // 监听数据变化时候,进行页码重新计算和渲染
    updatePagination({
      size: pageSize,
      pageNum: pageNum,
      total: totalDataCount
    });
  }
);

// 点击页码的事件
const onClickPageItem = (item: PageItem) => {
  if (item.active || item.disabled) {
    return;
  }
  updatePagination({
    size: props.pageSize,
    pageNum: item.num,
    total: props.totalDataCount
  });
  emits('changePage', { pageNum: item.num });
};

// 页码数据的统一更新方法
function updatePagination(params: {
  size: number;
  pageNum: number;
  total: number;
}) {
  const { total, size } = params;
  const pageItemList = parsePagination(params);
  state.pageItemList = pageItemList;
  state.pageCount = Math.ceil(total / size);
}

// 页码的计算方法
function parsePagination(params: {
  size: number;
  total: number;
  pageNum: number;
}) {
  const { size, total, pageNum } = params;
  const pageItemList: PageItem[] = [];
  const pageCount = Math.ceil(total / size);
  const pageStartNum: number = Math.max(1, pageNum - 2);
  const pageEndNum: number = Math.min(pageCount, pageNum + 2);

  for (let i = pageStartNum; i <= pageEndNum; i++) {
    pageItemList.push({
      num: i,
      text: `${i}`,
      active: i === pageNum,
      disabled: false
    });
  }
  pageItemList.unshift({
    num: pageNum - 1,
    text: '上一页',
    active: false,
    disabled: pageNum - 1 < pageStartNum
  });
  pageItemList.push({
    num: pageNum + 1,
    text: '下一页',
    active: false,
    disabled: pageNum + 1 > pageEndNum
  });
  return pageItemList;
}
</script>

页码选择器最终实现效果看图。

图片

图片

更多分页页码选择器的详细代码,你可以看课程的代码案例。

服务端实现部分

接下来就是服务端部分的实现,我也画了张图。

服务端的实现核心,就是查询数据库的SQL语句的实现

前端发起分页查询请求,把“列表数据量”和“选择页码”传给服务端。在服务端中,根据每页“列表数据量”和“选择页码”进行换算,变成SQL查询的数据“查询起始位置”和“查询个数”,来处理数据库分页查询。

还记得前端部分中,“页码选择器”用到“数据总量”吗?其实也是通过SQL来查询数据库得来的:

SELECT COUNT(*) AS total FROM table_xxxx

到这里,前端部分和服务端部分实现就完成了。

总结

今天我们围绕运营物料的后台管理,实现了物料的数据的“增加”“修改”和“查询”能力,这是我们的课程项目首次完整的一次数据管理操作。

不知道你有没有发现?我们实现了数据的“增改查”,但没有“删除”。这一点之前也提到过,因为实际企业项目不会真的删除数据库信息,都是“假删除”或者“软删除”,以后碰到类似的全栈场景,要注意这个知识点。

分析了物料管理的功能,我们重点学习了在全栈项目中,如何从前端到服务端,全栈化地实现列表分页的功能,这也是企业工作中,常见的功能需求点。可以分为两个部分来实现。

  • 前端部分,核心是实现“页码选择器”,设计实现的重点就是多种分页数据的计算和显示。
  • 服务端部分,核心是MySQL数据库分页查询SQL语句的实现,重点是要理清分页查询的“起始数据位置”和“查询数据量”。

思考题

在今天的技术方案设计中,物料组件版本号只能升级不能降级,这是为什么呢?

欢迎在留言区分享你的思考,如果对今天的内容有疑问也欢迎留言,下一讲见。

完整的代码在这里

精选留言(1)
  • ifelse 👍(1) 💬(0)

    学习打卡

    2024-09-24