🙎 "抖音都能做,为什么你们做不了?抖音能做到按权重推荐视频,小红书能把笔记和短视频混排得像彩虹糖!你们能干点啥?"
需求评审会上,新来的产品总监把MacBook重重得合上,会议室温度瞬间骤降10度,公司后端面面相觑,低着头乖得像个鹌鹑。
🙋♂️ 空气凝固的第七秒,后端架构师发话了:"抖音有算法团队,他们的推荐算法需要用户行为数据训练模型......"
🙎 "那我给你想个笨办法,每页给用户推三条视频,两条图片,一条文章,前边的不够了,后边的补上"
🙋♂️ 后端开发终于说话了:"他们的内容都存在一张表..."
🙎 "别和我说技术架构!用户不在乎你有几张数据库表!上周我们的竞品分析报告显示......"
作为旁观者,我暗自窃喜,还好不是让我做。再说了抖音多少开发,我们前后端加起来才两个人,还想鸟枪换大炮?
📄【需求来了】
-
背景:前端展示标签选项,用户选中标签后,筛选该标签下的数据,展示为瀑布流,用户可以上拉加载分页,每页给用户展示视频+图片+文章。
-
要求:
- 每页必须返回6条数据。
- 每页3条视频+2条图片+1条文章,排序优先级:视频 > 图片 > 文章。
- 如果视频不够了,用图片当做视频补上,图片不够了用文章当做图片补上。(比如:视频只有1条了,最终就是:1视频+4图片+1文章)
-
示意图:
😲【后端慌了】
后端听了需求立马慌了,直摇头说做不了,说后端要重构,嘴里还在无限循环他们有多难办:
- 三种数据存在三种表中,每种表中的数据还有不同标签,要筛选出选中的标签数据,再合并起来给前端。
- 第一张表没数据第二张表补,第二张表没数据第三张表补,分页后根本找不到上一页数据最后一条是哪个。
- 没办法保证一页是6条。
- 要做到这个从高到低依次补充数据,很难,速度会很慢。
😅【后端做不了,让前端试试?】
突然,后端和产品经理把目光转向我。
“那个……前端可以搞定吗?”
???
我一个前端切图仔,我能搞定这个?
“你在想屁吃”
我立马开始输出《前端圣经》
“分页?自己遍历全表啊,这不就是你们说的'后端工程化'吗?”
“这个需求做不了是吧,上次数据库删库你们怎么做得比谁都快?”
“Redis不是数据库?数据存Redis不就行了?”
“您不是整天JVM调优呢?我看您是给破船贴创可贴 内存泄漏漏得比你英语词汇量还丰富💧”
😎【前端“笨办法”】
"好,既然你后端做不了,那我就前端做。后端做不了的需求我前端做,后端做得了的我前端更要做,这就是前端!"
方案思路
后端针对视频、图片、文章三种数据,提供三个分页接口,支持用最后一个数据ID和数量分页请求。(这总能做到吧,后端架构师们?)
-
备用数据设计
为了应对最坏情况(比如视频数据为空),提前预请求多条备用数据:
-
视频接口: 请求 3 条数据(目标数量)。
-
图片接口: 请求 5 条数据。原因在于:
- 当视频数据为空时,视频部分需要 3 条图片数据补位;
- 同时图片区域本身还需要 2 条数据;
- 因此图片接口至少需要 3 + 2 = 5 条数据。
-
文章接口: 请求 6 条数据。原因在于:
- 当视频和图片都为空时,文章接口需要同时补充视频(3 条)、图片(2 条)和文章区域(1 条),共计 6 条数据。
-
-
数据补位顺序
按照以下优先级补位,确保各区域的数据数量满足要求:
-
视频部分(目标 3 条):
- 优先使用视频接口返回的数据。
- 如果视频数据不足,则从图片备用数据中补足;
- 如果图片备用数据仍不足,再从文章备用数据中补足。
-
图片部分(目标 2 条):
- 在视频部分消耗了部分图片数据后,剩余的图片备用数据用于图片区域;
- 如果不足,则从文章备用数据中补足。
-
文章部分(目标 1 条):
- 在视频和图片部分都补位后,剩余的文章备用数据中取 1 条数据用于文章区域。
-
-
数据合并
将三个部分的数据按照视频、图片、文章的顺序合并,最终确保前端展示 6 条数据。
async function loadPage(lastVideoId, lastImageId, lastArticleId) {
// 视频接口:请求3条
// 图片接口:最坏情况需要提供视频的3条补位 + 图片区域2条 = 5条
// 文章接口:最坏情况需要提供视频区域3条 + 图片区域2条 + 文章区域1条 = 6条
const [videoData, imageData, articleData] = await Promise.all([
fetchVideos(3, lastVideoId),
fetchImages(5, lastImageId),
fetchArticles(6, lastArticleId),
]);
// 1. 视频部分:目标是 3 条数据
let videoSlot = [...videoData];
let missingVideoCount = 3 - videoSlot.length;
if (missingVideoCount > 0) {
// 优先从图片数据中补充
const substituteFromImages = imageData.splice(0, missingVideoCount);
videoSlot = videoSlot.concat(substituteFromImages);
missingVideoCount = 3 - videoSlot.length;
if (missingVideoCount > 0) {
// 如果图片仍不足,再从文章数据中补充
const substituteFromArticles = articleData.splice(0, missingVideoCount);
videoSlot = videoSlot.concat(substituteFromArticles);
}
}
// 2. 图片部分:目标是 2 条数据
let imageSlot = [];
if (imageData.length >= 2) {
imageSlot = imageData.splice(0, 2);
} else {
imageSlot = imageData.splice(0);
const missingImageCount = 2 - imageSlot.length;
const substituteFromArticles = articleData.splice(0, missingImageCount);
imageSlot = imageSlot.concat(substituteFromArticles);
}
// 3. 文章部分:目标是 1 条数据
let articleSlot = [];
if (articleData.length > 0) {
articleSlot = articleData.splice(0, 1);
}
// 合并顺序:视频、图片、文章,共 6 条数据
const finalPageData = videoSlot.concat(imageSlot, articleSlot);
return finalPageData;
}
// 模拟视频接口请求
const fetchVideos = (count, lastId) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve([]);
}, 1000);
});
};
// 模拟图片接口请求
const fetchImages = (count, lastId) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve([]);
}, 1000);
});
};
// 模拟文章接口请求
const fetchArticles = (count, lastId) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve([]);
}, 1000);
});
};
// 示例调用
loadPage(0, 0, 0).then((data) => {
console.log("最终数据:", data);
});
📝【后记】
三个月后,当我看到后端年终总结PPT写的:设计实现"革命性分页架构"技术方案,笑得我差点把咖啡喷在屏幕上。
茶水间最新传说:某次服务器宕机后,后端Leader在容灾预案里加了一条——"紧急情况下可用用户浏览器当边缘计算节点"。