自定义 Hooks 必须是纯函数,只封装可复用的状态与副作用,不参与模板解析;useListRequest需适配UniApp生命周期,useLocalStorage需处理跨端JSON序列化异常,useUploader需按平台差异化实现上传逻辑。

不能在 setup 里直接写逻辑,也不能把组件当返回值传出去——自定义 Hooks 必须是纯函数,只封装可复用的状态与副作用,不参与模板解析。
useListRequest:封装列表请求最常踩的坑
很多人照搬 Web 端 useSWR 或 react-query 思路,在 UniApp 里直接返回 data 和 refetch,结果发现下拉刷新不触发、上拉加载没反应。根本原因是 UniApp 的页面生命周期(onPullDownRefresh、onReachBottom)和 Vue 的响应式系统不是自动桥接的。
- 必须显式调用
onPullDownRefresh和onReachBottom,并在 Hook 内部用uni.startPullDownRefresh()/uni.stopPullDownRefresh()配合管理状态 -
fetchFn参数不能硬编码分页字段,得支持动态透传(比如 H5 用page/size,小程序可能用offset/limit) - 错误重试不能只靠
try/catch,要结合uni.showToast和uni.hideToast控制 UI 反馈,否则 H5 和小程序表现不一致
示例关键片段:
export function useListRequest(fetchFn, options = {}) {
const { pageKey = 'page', sizeKey = 'size', initialPage = 1, pageSize = 10 } = options
const state = reactive({
list: [],
loading: false,
error: null,
hasMore: true,
})
const fetchData = async (params = {}) => {
state.loading = true
try {
const res = await fetchFn({
[pageKey]: params[pageKey] ?? state.page,
[sizeKey]: params[sizeKey] ?? pageSize,
...params,
})
state.list = params[pageKey] === initialPage ? res.data : [...state.list, ...res.data]
state.hasMore = res.data.length >= pageSize
} catch (e) {
state.error = e
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
state.loading = false
}
}
// 绑定到页面生命周期(需在 setup 中手动调用)
const bindLifetimes = () => {
uni.$on('pullDownRefresh', () => fetchData({ [pageKey]: initialPage }))
uni.$on('reachBottom', () => {
if (state.hasMore && !state.loading) {
fetchData({ [pageKey]: state.page + 1 })
}
})
}
return { ...toRefs(state), fetchData, bindLifetimes }
}
useLocalStorage:跨端存储必须处理 JSON 序列化异常
UniApp 的 uni.setStorageSync 和 Web 端 localStorage.setItem 行为不完全一致:H5 环境能存任意对象,但小程序和 App 端对非字符串值会静默失败或报错 setStorage:fail parameter error。
- 必须对写入值做
JSON.stringify安全包裹,读取时加try/catch捕获解析失败 - 不能直接返回
ref,得用computed包一层,否则修改响应式变量后不会触发uni.setStorageSync - 多端缓存键名建议加前缀(如
uni_app_theme),避免和其它 SDK 冲突
示例:
export function useLocalStorage(key, initialValue) {
const storageKey = `uni_app_${key}`
const stored = ref()
try {
const raw = uni.getStorageSync(storageKey)
stored.value = raw ? JSON.parse(raw) : initialValue
} catch {
stored.value = initialValue
}
const value = computed({
get() { return stored.value },
set(v) {
try {
uni.setStorageSync(storageKey, JSON.stringify(v))
stored.value = v
} catch (e) {
console.warn(`Failed to save ${storageKey}`, e)
}
}
})
return value
}
useUploader:文件上传必须区分平台能力边界
UniApp 的 uni.chooseImage、uni.chooseFile 在不同平台返回结构差异极大:H5 返回 File 对象,小程序返回临时路径,App 端还可能需要额外权限声明。直接传给 uni.uploadFile 会报 uploadFile:fail tempFilePath is not exist 或空文件。
- 必须在 Hook 内部做平台判断:
uni.getSystemInfoSync().platform,H5 走FormData+fetch,其余走uni.uploadFile - 多文件上传不能简单
for循环调用uploadFile,得用Promise.allSettled并统一收集成功/失败项 - 进度监听只在
uploadFile中有效,H5 需要自己模拟(比如按 chunk 分片 +XMLHttpRequest.upload.onprogress)
关键判断逻辑:
const platform = uni.getSystemInfoSync().platform
if (platform === 'h5') {
// 构造 FormData,用 fetch 上传
} else {
// 走 uni.uploadFile,filePath 来自 chooseImage/chooseFile 返回值
}
真正难的不是写函数,而是每一步都要问:这个 API 在微信小程序里有没有?在 App 端会不会被拦截?H5 下用户关了 JS 还能不能兜底?跨端 Hooks 的健壮性,就藏在这些条件分支里。










