之前折腾过一个中后台项目,列表页写到最后有点绷不住了。
一个页面,要加载数据、要分页、要筛选、要刷新,我图省事全堆在 setup 里。刚开始还行,需求一加,几百行下去,改一处动全身。盯着屏幕发了会儿呆,决定还是得抽。
这篇记一下我把它抽成组合式函数(composable)的过程。不是什么高深玩意,但踩的坑挺真实,写出来给自己提个醒,也希望能帮到同样乱过的人。
一、先看看乱成什么样
大概长这样(简化过):
<script setup>
import { ref, onMounted } from 'vue'
import { getUserList } from '@/api'
const list = ref([])
const loading = ref(false)
const page = ref(1)
const size = ref(10)
const total = ref(0)
const keyword = ref('')
const load = async () => {
loading.value = true
try {
const res = await getUserList({ page: page.value, size: size.value, keyword: keyword.value })
list.value = res.data.list
total.value = res.data.total
} finally {
loading.value = false
}
}
const onSearch = () => { page.value = 1; load() }
const onPageChange = (p) => { page.value = p; load() }
const onReset = () => { keyword.value = ''; page.value = 1; load() }
onMounted(load)
</script>
一个页面这样也就算了。问题是我这个项目里,这种列表页有十几个,每个都差不多,又都有点小差别。复制粘贴吧,改一个 bug 要改十几个地方;不复制吧,每个都得重写一遍。
二、思路:把"列表"这件事抽出去
组合式函数说白了,就是把一堆相关的响应式状态和操作,从组件里挪进一个 useXxx 函数。组件只管用,不管它内部怎么实现。
我抽了一个 useList:
// useList.js
import { ref } from 'vue'
export function useList(fetchFn) {
const list = ref([])
const loading = ref(false)
const page = ref(1)
const size = ref(10)
const total = ref(0)
const keyword = ref('')
const load = async () => {
loading.value = true
try {
const res = await fetchFn({ page: page.value, size: size.value, keyword: keyword.value })
list.value = res.data.list
total.value = res.data.total
} finally {
loading.value = false
}
}
const search = () => { page.value = 1; load() }
const reset = () => { keyword.value = ''; page.value = 1; load() }
const pageChange = (p) => { page.value = p; load() }
return { list, loading, page, size, total, keyword, load, search, reset, pageChange }
}
组件里就清爽多了:
<script setup>
import { onMounted } from 'vue'
import { getUserList } from '@/api'
import { useList } from './useList'
const { list, loading, total, keyword, search, reset, pageChange } = useList(getUserList)
onMounted(search)
</script>
数据怎么拿、分页怎么算,组件根本不用管,调一句 useList(getUserList) 就行。十几个列表页,每个只要一行。
三、踩过的几个坑
看着简单,真抽的时候还是翻了几个车。
1. 解构把响应式弄丢了
这是最容易踩的。如果你 return 的是一个 reactive 对象,组件里一解构,响应式就没了,页面怎么都不更新,你还以为是接口没返回。
我的解法是:组合式函数里单个值全用 ref,return 出去让组件自己解构。ref 解构出来还是响应式的,基本不会出错。
这块我其实也不是 100% 摸透了所有边界,但「ref 优先」这个习惯养成了,至今没再翻过车。
2. 参数传 ref 还是传值
fetchFn 如果依赖外部条件(比如要带上路由里的 id),你直接传个字符串进去,useList 里是不会跟着变的。要么把 ref/computed 传进去,要么干脆把这个条件也交给 useList 管。
我图省事,一般把查询条件都塞进 useList 的状态里,组件只管调方法。
3. loading 别忘了放 finally
loading.value = false 要是放在 try 末尾,接口一报错,loading 就卡死在那转圈。这个坑我踩过——页面一直 loading,我还以为是接口挂了,查了半天才发现是状态没复位。放进 finally 里就稳了。
四、不是所有逻辑都得抽
抽爽了容易上头,见啥都想抽。后来我给自己立了条规矩:两个地方以上要用,或者一块逻辑明显能独立成"一件事",才抽。就用一次的、跟当前页面强绑定的,留在 setup 里也挺好,没必要为了抽而抽。
这套 useList 我现在也还在调——要不要内置防抖、要不要缓存上一页数据,都没完全定。先这么用着,踩到新坑再说。
就这些。如果你也在 setup 里堆过几百行,希望这篇能给你个下手的思路。

1991

被折叠的 条评论
为什么被折叠?



