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来查询数据库得来的:
到这里,前端部分和服务端部分实现就完成了。
总结
今天我们围绕运营物料的后台管理,实现了物料的数据的“增加”“修改”和“查询”能力,这是我们的课程项目首次完整的一次数据管理操作。
不知道你有没有发现?我们实现了数据的“增改查”,但没有“删除”。这一点之前也提到过,因为实际企业项目不会真的删除数据库信息,都是“假删除”或者“软删除”,以后碰到类似的全栈场景,要注意这个知识点。
分析了物料管理的功能,我们重点学习了在全栈项目中,如何从前端到服务端,全栈化地实现列表分页的功能,这也是企业工作中,常见的功能需求点。可以分为两个部分来实现。
- 前端部分,核心是实现“页码选择器”,设计实现的重点就是多种分页数据的计算和显示。
- 服务端部分,核心是MySQL数据库分页查询SQL语句的实现,重点是要理清分页查询的“起始数据位置”和“查询数据量”。
思考题
在今天的技术方案设计中,物料组件版本号只能升级不能降级,这是为什么呢?
欢迎在留言区分享你的思考,如果对今天的内容有疑问也欢迎留言,下一讲见。
完整的代码在这里
- ifelse 👍(1) 💬(0)
学习打卡
2024-09-24