ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

职位列表页改了三版:搜索防抖 + 上拉加载 + 骨架屏,uni-app 完整封装

职位列表页改了三版:搜索防抖 + 上拉加载 + 骨架屏,uni-app 完整封装 导读小蓝直聘的职位列表页改了三版才消停第一版搜索逻辑写死在页面里输入一个字母发一次请求后端被刷出慢查询第二版拆成组合式函数防抖 上拉加载 骨架屏一次配齐。这篇把 uni-app 列表页的完整封装和踩过的坑写下来直接抄。职位列表页改了三版搜索防抖 上拉加载 骨架屏uni-app 完整封装先说痛点。职位列表页需求很常规顶部搜索框、下面职位卡片列表、上拉加载更多、加载中转圈。第一版代码全写在页面里用户每敲一个字就request一次后端接口被刷得直冒烟下拉加载还经常和滚动冲突一顿操作下来页面又卡又乱。第一版的问题搜索没防抖请求满天飞这是最初的代码监听输入框值直接请求watch(searchText,(val){fetchJobs({keyword:val,page:1})// 每敲一个字发一次请求})searchText一变就发请求v-model每次输入都触发一个Java关键字敲下来发了 4 个请求还是乱序返回的——先发的请求后返回把后发的结果覆盖了。封装useJobList防抖 分页 上拉一次配齐重构后把列表逻辑抽成组合式函数useJobList页面只管渲染// utils/useJobList.jsimport{ref}fromvueimport{debounce}from./debounceexportfunctionuseJobList(fetchApi){constlistref([])constpageref(1)constsizeref(10)consttotalref(0)constloadingref(false)constfinishedref(false)// 是否加载完constkeywordref()// 搜索防抖 500ms重置页码重新加载constonSearchdebounce((){page.value1list.value[]finished.valuefalseload()},500)// 加载一页asyncfunctionload(){if(loading.value||finished.value)returnloading.valuetruetry{constresawaitfetchApi({keyword:keyword.value,page:page.value,size:size.value})list.value.push(...res.records)total.valueres.totalif(list.value.lengthtotal.value){finished.valuetrue}page.value}finally{loading.valuefalse}}// 上拉加载更多onReachBottom 里调用functiononReachBottom(){load()}return{list,loading,finished,keyword,onSearch,onReachBottom}}防抖函数// utils/debounce.jsexportfunctiondebounce(fn,delay500){lettimernullreturnfunction(...args){if(timer)clearTimeout(timer)timersetTimeout((){fn.apply(this,args)},delay)}}页面里一行接入import { useJobList } from /utils/useJobList import { fetchJobs } from /api/job const jobList useJobList(fetchJobs) // 页面生命周期上拉加载 import { onReachBottom } from dcloudio/uni-app onReachBottom(() jobList.onReachBottom())骨架屏uni-app 没有内置用 CSS 简单实现加载首屏时白屏很难看小程序里做骨架屏最省事的方式是灰色占位块 动画.skeleton { padding: 24rpx; } .sk-item { display: flex; margin-bottom: 30rpx; } .sk-avatar { width: 88rpx; height: 88rpx; border-radius: 12rpx; background: linear-gradient(90deg, #f2f3f5 25%, #e8e9eb 37%, #f2f3f5 63%); background-size: 400% 100%; animation: sk-loading 1.4s ease infinite; } .sk-line { height: 28rpx; border-radius: 8rpx; margin-bottom: 16rpx; background: linear-gradient(90deg, #f2f3f5 25%, #e8e9eb 37%, #f2f3f5 63%); background-size: 400% 100%; animation: sk-loading 1.4s ease infinite; } .w60 { width: 60%; } .w80 { width: 80%; } .w40 { width: 40%; } keyframes sk-loading { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }首次加载显示骨架屏数据回来换成列表体验立刻不一样。空状态加载完没数据给个兜底提示列表页最容易漏的是空状态。搜索Java 架构师没结果页面白花花一片用户以为挂了。useJobList 里加个判断页面渲染兜底没有找到相关职位换个关键词试试 已经到底啦空状态、加载中、到底了三个状态都覆盖到列表页才算完整。踩坑一onReachBottom 不触发原来是页面没用原生滚动现象上拉加载死活不触发console.log没输出。排查过程页面用了 scroll-view 组件包列表onReachBottom是页面原生滚动的回调scroll-view 内部滚动它根本感知不到。定位思路看 uni-app 文档onReachBottom只在页面级滚动触发自定义 scroll-view 要用它的scrolltolower事件。最终解决列表页去掉 scroll-view 直接用页面滚动职位列表本来就该整页滚onReachBottom正常触发。如果一定要用 scroll-view就用它的scrolltolower别混着用。踩坑二防抖 setTimeout 在页面卸载后还执行setData 报错现象从列表页切到详情页再返回控制台报setData:fail偶发页面数据错乱。排查过程防抖的setTimeout500ms 后回调里还执行load()此时页面已经卸载list.value.push改了已销毁的响应式数据。定位思路组件/页面卸载时要清理定时器。最终解决onUnload里清掉防抖定时器。给防抖函数加 cancelexportfunctiondebounce(fn,delay500){lettimernullconstwrappedfunction(...args){if(timer)clearTimeout(timer)timersetTimeout(()fn.apply(this,args),delay)}wrapped.cancel(){if(timer)clearTimeout(timer);timernull}returnwrapped}页面卸载时jobList.onSearch.cancel()定时器不残留。可直接复用的要点搜索输入必须防抖500ms 起步防抖函数带上cancel方法卸载时清理。列表逻辑抽useJobListpage/size/loading/finished 状态机页面只管渲染。上拉加载页面级滚动用onReachBottomscroll-view 用scrolltolower别混用。骨架屏用 CSS 动画占位块首屏体验提升明显成本最低。列表接口返回{ records, total }前端用list.length total判断是否加载完。项目源码https://gitee.com/gzqkl/job-uniapp
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进