1. 理解uniapp页面列表请求hook的核心需求
在uni-app开发中,页面列表数据请求是最常见的场景之一。传统的开发方式往往在每个页面的onLoad或onShow生命周期中直接调用API请求方法,这种方式会导致大量重复代码,且难以统一管理请求逻辑。通过hook技术对列表请求进行封装,可以实现以下核心价值:
- 代码复用性:将通用的列表请求逻辑抽离为可复用的hook,避免每个页面重复编写相似代码
- 状态统一管理:集中处理加载状态、分页参数、错误处理等通用逻辑
- 维护便捷性:当需要修改请求逻辑时,只需调整hook实现,所有使用该hook的页面自动更新
- 性能优化:可以在hook层统一实现防抖、缓存等优化策略
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础hook实现方案
2.1 创建useListRequest hook
首先在项目中创建hooks目录,新建useListRequest.js文件:
javascript复制import { ref } from 'vue'
export default function useListRequest(apiFunction, options = {}) {
const {
immediate = true,
pageSize = 10,
initialParams = {}
} = options
const loading = ref(false)
const error = ref(null)
const data = ref([])
const pagination = ref({
page: 1,
pageSize,
total: 0
})
const fetchData = async (params = {}) => {
try {
loading.value = true
error.value = null
const res = await apiFunction({
...initialParams,
...params,
page: pagination.value.page,
pageSize: pagination.value.pageSize
})
data.value = res.data.list || []
pagination.value.total = res.data.total || 0
} catch (err) {
error.value = err
console.error('列表请求失败:', err)
} finally {
loading.value = false
}
}
if (immediate) {
fetchData()
}
return {
loading,
error,
data,
pagination,
fetchData
}
}
2.2 在页面中使用hook
在页面组件中使用这个hook:
javascript复制import { onReachBottom } from '@dcloudio/uni-app'
import useListRequest from '@/hooks/useListRequest'
import { getArticleList } from '@/api/article'
export default {
setup() {
const {
loading,
error,
data: articleList,
pagination,
fetchData
} = useListRequest(getArticleList, {
pageSize: 15,
initialParams: { category: 'tech' }
})
// 上拉加载更多
onReachBottom(() => {
if (!loading.value &&
articleList.value.length < pagination.value.total) {
pagination.value.page++
fetchData()
}
})
return {
loading,
error,
articleList
}
}
}
3. 高级功能扩展实现
3.1 请求防抖处理
列表请求特别是上拉加载时,需要防止用户快速滚动导致的重复请求:
javascript复制import { ref } from 'vue'
import { debounce } from 'lodash-es'
export default function useListRequest(apiFunction, options = {}) {
// ...其他代码
const fetchData = debounce(async (params = {}) => {
// 原有实现
}, 300, { leading: true, trailing: false })
// ...其他代码
}
3.2 数据缓存策略
实现简单的内存缓存,避免重复请求相同数据:
javascript复制const cache = new Map()
export default function useListRequest(apiFunction, options = {}) {
// ...其他代码
const fetchData = async (params = {}) => {
const cacheKey = JSON.stringify({
...params,
page: pagination.value.page,
pageSize: pagination.value.pageSize
})
if (cache.has(cacheKey)) {
const cached = cache.get(cacheKey)
data.value = cached.data
pagination.value.total = cached.total
return
}
try {
// 原有请求逻辑...
// 缓存结果
cache.set(cacheKey, {
data: data.value,
total: pagination.value.total
})
} catch (err) {
// 错误处理...
}
}
// ...其他代码
}
3.3 错误重试机制
为网络不稳定的场景添加自动重试功能:
javascript复制export default function useListRequest(apiFunction, options = {}) {
const {
retryTimes = 3,
retryInterval = 1000
} = options
const fetchData = async (params = {}, retryCount = 0) => {
try {
// 原有请求逻辑...
} catch (err) {
if (retryCount < retryTimes) {
await new Promise(resolve => setTimeout(resolve, retryInterval))
return fetchData(params, retryCount + 1)
}
error.value = err
}
}
// ...其他代码
}
4. 性能优化实践
4.1 列表项唯一键优化
确保列表渲染时使用稳定的唯一key,提高虚拟列表性能:
javascript复制// 在hook返回数据前处理
data.value = (res.data.list || []).map(item => ({
...item,
_id: item.id || Math.random().toString(36).substr(2, 9)
}))
// 模板中使用
<template v-for="item in articleList" :key="item._id">
<!-- 列表项内容 -->
</template>
4.2 分页预加载策略
提前加载下一页数据,提升用户体验:
javascript复制onReachBottom(() => {
if (!loading.value &&
articleList.value.length < pagination.value.total) {
// 距离底部还有20%高度时提前加载
const preloadThreshold = 0.8
if (scrollRatio < preloadThreshold) {
pagination.value.page++
fetchData()
}
}
})
4.3 大数据量分片渲染
对于超长列表,采用分片渲染策略:
javascript复制const visibleData = computed(() => {
return articleList.value.slice(0, visibleCount.value)
})
const loadMore = () => {
visibleCount.value += chunkSize
}
// 初始只渲染20条,滚动时每次加载10条
const chunkSize = 10
const visibleCount = ref(20)
5. 常见问题与解决方案
5.1 页面跳转返回列表位置保持
使用scroll-top属性记录并恢复滚动位置:
javascript复制// 在hook中添加
const scrollTop = ref(0)
const saveScrollPosition = () => {
scrollTop.value = document.documentElement.scrollTop ||
document.body.scrollTop
}
// 页面激活时恢复位置
onPageShow(() => {
if (scrollTop.value) {
uni.pageScrollTo({
scrollTop: scrollTop.value,
duration: 0
})
}
})
return {
// ...其他返回
saveScrollPosition
}
// 在列表页面使用
<scroll-view
:scroll-top="scrollTop"
@scroll="saveScrollPosition"
>
<!-- 列表内容 -->
</scroll-view>
5.2 多标签列表状态冲突
为不同标签的列表维护独立状态:
javascript复制const tabData = ref({})
const fetchData = async (tab, params = {}) => {
if (!tabData.value[tab]) {
tabData.value[tab] = {
loading: false,
data: [],
pagination: { page: 1, pageSize, total: 0 }
}
}
const currentTab = tabData.value[tab]
try {
currentTab.loading = true
const res = await apiFunction({
tab,
...params,
page: currentTab.pagination.page,
pageSize: currentTab.pagination.pageSize
})
currentTab.data = res.data.list || []
currentTab.pagination.total = res.data.total || 0
} catch (err) {
console.error(err)
} finally {
currentTab.loading = false
}
}
5.3 请求取消与竞态处理
使用AbortController避免过时请求覆盖最新结果:
javascript复制let abortController = null
const fetchData = async (params = {}) => {
// 取消上一个未完成的请求
if (abortController) {
abortController.abort()
}
abortController = new AbortController()
try {
loading.value = true
const res = await apiFunction({
...params,
signal: abortController.signal
})
// 处理结果...
} catch (err) {
if (err.name !== 'AbortError') {
// 非取消错误才处理
error.value = err
}
} finally {
abortController = null
loading.value = false
}
}
6. 与uniapp生态深度集成
6.1 支持uniapp的pull-down-refresh
javascript复制import { onPullDownRefresh } from '@dcloudio/uni-app'
// 在hook返回中添加
const refresh = () => {
pagination.value.page = 1
return fetchData()
}
// 在页面中使用
onPullDownRefresh(async () => {
await refresh()
uni.stopPullDownRefresh()
})
return {
// ...其他返回
refresh
}
6.2 适配uniapp的页面生命周期
javascript复制import { onShow, onHide } from '@dcloudio/uni-app'
// 在hook中添加
const isActive = ref(true)
onShow(() => {
isActive.value = true
// 页面显示时刷新数据
if (options.refreshOnShow) {
fetchData()
}
})
onHide(() => {
isActive.value = false
})
// 在请求处理中检查页面状态
const fetchData = async () => {
if (!isActive.value) return
// ...请求逻辑
}
6.3 集成uniapp的网络状态监测
javascript复制import { onNetworkStatusChange } from '@dcloudio/uni-app'
// 在hook中添加
const networkConnected = ref(true)
onNetworkStatusChange((res) => {
networkConnected.value = res.isConnected
if (res.isConnected && options.retryOnReconnect) {
fetchData()
}
})
7. TypeScript增强实现
为hook添加完整的类型定义:
typescript复制import type { Ref } from 'vue'
interface ListRequestOptions<TParams, TItem> {
immediate?: boolean
pageSize?: number
initialParams?: TParams
formatData?: (data: any) => TItem[]
onSuccess?: (data: TItem[]) => void
onError?: (error: Error) => void
}
interface Pagination {
page: number
pageSize: number
total: number
}
export default function useListRequest<TParams extends object, TItem>(
apiFunction: (params: any) => Promise<any>,
options: ListRequestOptions<TParams, TItem> = {}
) {
const loading: Ref<boolean> = ref(false)
const error: Ref<Error | null> = ref(null)
const data: Ref<TItem[]> = ref([])
const pagination: Ref<Pagination> = ref({
page: 1,
pageSize: options.pageSize || 10,
total: 0
})
// ...其余实现
}
8. 测试与调试策略
8.1 单元测试示例
使用vitest编写hook测试:
javascript复制import { test, expect, vi } from 'vitest'
import { ref } from 'vue'
import useListRequest from './useListRequest'
test('should fetch data correctly', async () => {
const mockApi = vi.fn().mockResolvedValue({
data: {
list: [{ id: 1, name: 'test' }],
total: 1
}
})
const {
data,
loading,
error,
fetchData
} = useListRequest(mockApi)
expect(loading.value).toBe(true)
await fetchData()
expect(loading.value).toBe(false)
expect(data.value.length).toBe(1)
expect(error.value).toBeNull()
})
8.2 调试技巧
在uniapp中调试hook的建议:
- 使用console.log输出关键状态变化
- 在vue devtools中检查hook暴露的ref状态
- 为error状态添加监听,自动弹出错误提示:
javascript复制watch(error, (err) => {
if (err) {
uni.showToast({
title: err.message,
icon: 'none'
})
}
})
9. 生产环境最佳实践
9.1 性能监控集成
javascript复制const fetchData = async () => {
const startTime = Date.now()
try {
// ...请求逻辑
const duration = Date.now() - startTime
if (duration > 1000) {
reportSlowRequest(duration)
}
} catch (err) {
reportError(err)
}
}
9.2 敏感数据处理
javascript复制const sanitizeData = (list) => {
return list.map(item => {
const sanitized = {...item}
// 移除敏感字段
delete sanitized.password
delete sanitized.token
return sanitized
})
}
data.value = sanitizeData(res.data.list)
9.3 多环境配置适配
javascript复制const getApiBaseUrl = () => {
if (process.env.NODE_ENV === 'development') {
return 'http://dev.api.example.com'
}
return 'https://api.example.com'
}
const apiFunction = (params) => {
return uni.request({
url: `${getApiBaseUrl()}/list`,
data: params
})
}
10. 扩展思考与进阶方向
10.1 服务端渲染(SSR)适配
javascript复制// 在hook中添加服务端渲染支持
if (process.env.UNI_PLATFORM === 'h5' &&
typeof window === 'undefined') {
// 服务端环境特殊处理
data.value = []
} else {
// 客户端正常逻辑
fetchData()
}
10.2 WebSocket实时更新
javascript复制const setupWebSocket = () => {
const ws = new WebSocket('wss://api.example.com/realtime')
ws.onmessage = (event) => {
const message = JSON.parse(event.data)
if (message.type === 'list_update') {
// 合并更新到现有数据
data.value = mergeUpdates(data.value, message.data)
}
}
onUnmounted(() => {
ws.close()
})
}
// 在hook初始化时调用
setupWebSocket()
10.3 离线缓存策略
javascript复制const getCachedData = async () => {
try {
const cached = await uni.getStorage({ key: 'list_cache' })
if (cached && cached.data) {
data.value = cached.data
return true
}
} catch (err) {
console.warn('读取缓存失败:', err)
}
return false
}
const fetchData = async () => {
if (options.useCache && await getCachedData()) {
// 先显示缓存数据
if (options.showCacheWhileLoading) {
loading.value = false
}
}
// 继续网络请求...
try {
const res = await apiFunction(/* ... */)
// 保存新数据到缓存
await uni.setStorage({
key: 'list_cache',
data: res.data.list
})
} catch (err) {
// 网络错误时可以使用缓存数据
if (!data.value.length && options.fallbackToCache) {
await getCachedData()
}
}
}
