1. 为什么需要将Vue组件封装为Hook函数
在Vue 3的Composition API推出后,Hook函数已经成为组织逻辑的主流方式。将传统组件封装为Hook函数的核心价值在于逻辑复用和解耦。想象一下,当你发现多个组件都在使用相似的逻辑时,与其在每个组件中重复编写setup()里的代码,不如把这些逻辑抽离成可复用的Hook。
Hook函数相比组件有几个显著优势:
- 更细粒度的逻辑复用(可以只使用Hook中的部分功能)
- 更好的类型推断(TypeScript支持更完善)
- 更清晰的依赖关系(所有响应式依赖都显式声明)
- 更灵活的测试方式(可以单独测试Hook而不需要渲染组件)
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础封装方法与核心步骤
2.1 组件逻辑分析
以一个常见的计数器组件为例,原始组件可能长这样:
vue复制<template>
<div>
<button @click="decrement">-</button>
<span>{{ count }}</span>
<button @click="increment">+</button>
</div>
</template>
<script>
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
},
decrement() {
this.count--
}
}
}
</script>
2.2 转换为Hook函数
将其转换为Hook的基本步骤:
- 创建useCounter.js文件
- 将data转换为ref/reactive
- 将methods转换为普通函数
- 返回需要暴露的变量和方法
javascript复制import { ref } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const increment = () => count.value++
const decrement = () => count.value--
return {
count,
increment,
decrement
}
}
2.3 在组件中使用Hook
vue复制<template>
<div>
<button @click="decrement">-</button>
<span>{{ count }}</span>
<button @click="increment">+</button>
</div>
</template>
<script setup>
import { useCounter } from './useCounter'
const { count, increment, decrement } = useCounter()
</script>
3. 高级封装技巧与实战
3.1 处理组件生命周期
当需要处理生命周期时,直接在Hook中使用生命周期钩子:
javascript复制import { onMounted, onUnmounted } from 'vue'
export function useEventListener(target, event, callback) {
onMounted(() => {
target.addEventListener(event, callback)
})
onUnmounted(() => {
target.removeEventListener(event, callback)
})
}
3.2 组合多个Hook
Hook的强大之处在于可以组合使用:
javascript复制import { useCounter } from './useCounter'
import { useLocalStorage } from './useLocalStorage'
export function usePersistentCounter(key, initialValue) {
const { count, increment, decrement } = useCounter(initialValue)
const { save, load } = useLocalStorage(key)
load().then(val => {
if(val !== null) count.value = val
})
const persistentIncrement = () => {
increment()
save(count.value)
}
const persistentDecrement = () => {
decrement()
save(count.value)
}
return {
count,
increment: persistentIncrement,
decrement: persistentDecrement
}
}
3.3 处理异步操作
对于异步操作,使用async/await配合ref:
javascript复制import { ref } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
const fetchData = async () => {
loading.value = true
try {
const response = await fetch(url)
data.value = await response.json()
} catch (err) {
error.value = err
} finally {
loading.value = false
}
}
fetchData()
return {
data,
error,
loading,
retry: fetchData
}
}
4. 性能优化与最佳实践
4.1 减少不必要的响应式
不是所有值都需要响应式,使用普通变量可以提升性能:
javascript复制export function useHeavyCalculation(initialValue) {
// 这个值不需要响应式
const config = {
factor: 2,
maxIterations: 1000
}
const result = ref(initialValue)
// ...计算逻辑
return { result }
}
4.2 使用computed优化派生状态
javascript复制import { ref, computed } from 'vue'
export function useCart() {
const items = ref([])
const total = computed(() =>
items.value.reduce((sum, item) => sum + item.price, 0)
)
const addItem = (item) => {
items.value.push(item)
}
return {
items,
total,
addItem
}
}
4.3 使用watchEffect处理副作用
javascript复制import { ref, watchEffect } from 'vue'
export function useWindowSize() {
const width = ref(window.innerWidth)
const height = ref(window.innerHeight)
const update = () => {
width.value = window.innerWidth
height.value = window.innerHeight
}
watchEffect((onCleanup) => {
window.addEventListener('resize', update)
onCleanup(() => {
window.removeEventListener('resize', update)
})
})
return { width, height }
}
5. 常见问题与解决方案
5.1 Hook之间的状态共享
如果需要共享状态,可以在Hook外部创建ref:
javascript复制// sharedState.js
import { ref } from 'vue'
export const sharedCount = ref(0)
// useCounterA.js
import { sharedCount } from './sharedState'
export function useCounterA() {
const increment = () => sharedCount.value++
return { count: sharedCount, increment }
}
// useCounterB.js
import { sharedCount } from './sharedState'
export function useCounterB() {
const decrement = () => sharedCount.value--
return { count: sharedCount, decrement }
}
5.2 处理SSR环境
在服务端渲染时,需要避免浏览器API的直接调用:
javascript复制import { ref, onMounted } from 'vue'
export function useMediaQuery(query) {
const matches = ref(false)
if(typeof window !== 'undefined') {
const mediaQuery = window.matchMedia(query)
matches.value = mediaQuery.matches
const handler = (e) => {
matches.value = e.matches
}
onMounted(() => {
mediaQuery.addListener(handler)
})
onUnmounted(() => {
mediaQuery.removeListener(handler)
})
}
return matches
}
5.3 TypeScript支持
为Hook添加类型定义可以显著提升开发体验:
typescript复制import { ref, Ref } from 'vue'
interface UseCounterReturn {
count: Ref<number>
increment: () => void
decrement: () => void
reset: () => void
}
export function useCounter(initialValue: number = 0): UseCounterReturn {
const count = ref(initialValue)
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => count.value = initialValue
return {
count,
increment,
decrement,
reset
}
}
6. 实战案例:封装一个复杂的表单Hook
让我们封装一个处理表单验证、提交和状态管理的完整Hook:
javascript复制import { ref, computed } from 'vue'
export function useForm({ initialValues, validate, onSubmit }) {
const values = ref({ ...initialValues })
const errors = ref({})
const isSubmitting = ref(false)
const setFieldValue = (name, value) => {
values.value[name] = value
}
const setFieldError = (name, error) => {
errors.value[name] = error
}
const validateForm = async () => {
try {
errors.value = await validate(values.value)
return Object.keys(errors.value).length === 0
} catch (err) {
console.error('Validation error:', err)
return false
}
}
const handleSubmit = async () => {
const isValid = await validateForm()
if(!isValid) return
isSubmitting.value = true
try {
await onSubmit(values.value)
} catch (err) {
console.error('Submit error:', err)
} finally {
isSubmitting.value = false
}
}
const isDirty = computed(() => {
return Object.keys(initialValues).some(
key => initialValues[key] !== values.value[key]
)
})
return {
values,
errors,
isSubmitting,
isDirty,
setFieldValue,
setFieldError,
handleSubmit
}
}
使用示例:
vue复制<script setup>
import { useForm } from './useForm'
const { values, errors, handleSubmit } = useForm({
initialValues: {
email: '',
password: ''
},
validate: (values) => {
const errors = {}
if(!values.email) errors.email = 'Email is required'
if(!values.password) errors.password = 'Password is required'
return errors
},
onSubmit: async (values) => {
await login(values)
}
})
</script>
7. 测试Hook函数的最佳实践
测试Hook函数比测试组件更简单直接。使用@vue/test-utils可以这样测试:
javascript复制import { renderHook } from '@testing-library/vue'
import { useCounter } from './useCounter'
test('should increment counter', async () => {
const { result } = renderHook(() => useCounter())
expect(result.count.value).toBe(0)
await result.increment()
expect(result.count.value).toBe(1)
})
对于异步Hook的测试:
javascript复制import { renderHook, flushPromises } from '@testing-library/vue'
import { useFetch } from './useFetch'
test('should handle fetch data', async () => {
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ data: 'test' })
})
)
const { result } = renderHook(() => useFetch('/api/data'))
expect(result.loading.value).toBe(true)
await flushPromises()
expect(result.loading.value).toBe(false)
expect(result.data.value).toEqual({ data: 'test' })
})
8. 从JSX组件到Hook的转换
如果你的组件使用JSX,转换思路也类似。例如一个渲染列表的JSX组件:
jsx复制export default {
data() {
return {
items: []
}
},
async created() {
this.items = await fetchItems()
},
render() {
return (
<ul>
{this.items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)
}
}
转换为Hook后:
javascript复制import { ref } from 'vue'
export function useItemList(fetchItems) {
const items = ref([])
const loadItems = async () => {
items.value = await fetchItems()
}
loadItems()
return { items, loadItems }
}
然后在JSX组件中使用:
jsx复制import { defineComponent } from 'vue'
import { useItemList } from './useItemList'
export default defineComponent({
setup() {
const { items } = useItemList(() =>
fetch('/api/items').then(r => r.json())
)
return () => (
<ul>
{items.value.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)
}
})
9. 发布可复用的Hook库
当你有多个有用的Hook时,可以考虑发布为库:
- 创建index.js作为入口文件:
javascript复制export * from './useCounter'
export * from './useFetch'
export * from './useForm'
// 其他Hook...
- 配置package.json:
json复制{
"name": "vue-hooks-library",
"version": "1.0.0",
"main": "index.js",
"peerDependencies": {
"vue": "^3.0.0"
}
}
- 添加TypeScript类型定义(可选):
typescript复制// types.d.ts
declare module 'vue-hooks-library' {
export * from './useCounter'
// 其他类型导出...
}
- 发布到npm:
bash复制npm login
npm publish
10. 与其他状态管理库集成
Hook函数可以与Pinia、Vuex等状态管理库配合使用:
10.1 与Pinia集成
javascript复制import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const increment = () => count.value++
const decrement = () => count.value--
return { count, increment, decrement }
})
// 在Hook中使用store
export function usePersistentCounter() {
const store = useCounterStore()
const { save, load } = useLocalStorage('counter')
load().then(val => {
if(val !== null) store.count = val
})
const increment = () => {
store.increment()
save(store.count)
}
return {
count: computed(() => store.count),
increment
}
}
10.2 与Vuex集成
javascript复制import { useStore } from 'vuex'
export function useVuexCounter() {
const store = useStore()
const count = computed(() => store.state.count)
const increment = () => store.commit('increment')
const decrement = () => store.commit('decrement')
return { count, increment, decrement }
}
11. 性能监控Hook示例
创建一个用于监控组件性能的Hook:
javascript复制import { onMounted, onUnmounted } from 'vue'
export function usePerformanceMonitor(componentName) {
const startTime = performance.now()
let lifecycleDuration = 0
onMounted(() => {
const mountTime = performance.now() - startTime
lifecycleDuration = mountTime
console.log(`${componentName} mounted in ${mountTime}ms`)
const observer = new PerformanceObserver((list) => {
for(const entry of list.getEntries()) {
if(entry.name === componentName) {
console.log(`${componentName} render time:`, entry.duration)
}
}
})
observer.observe({ entryTypes: ['measure'] })
onUnmounted(() => {
observer.disconnect()
const totalTime = performance.now() - startTime
console.log(`${componentName} unmounted after ${totalTime}ms`)
})
})
return {
markRenderStart: () => performance.mark(`${componentName}-render-start`),
markRenderEnd: () => {
performance.mark(`${componentName}-render-end`)
performance.measure(
componentName,
`${componentName}-render-start`,
`${componentName}-render-end`
)
}
}
}
使用示例:
vue复制<script setup>
import { usePerformanceMonitor } from './usePerformanceMonitor'
const { markRenderStart, markRenderEnd } = usePerformanceMonitor('MyComponent')
markRenderStart()
// 渲染逻辑...
markRenderEnd()
</script>
12. 响应式本地存储Hook
创建一个自动同步到localStorage的Hook:
javascript复制import { ref, watch } from 'vue'
export function useLocalStorage(key, initialValue) {
const storedValue = localStorage.getItem(key)
const value = ref(storedValue ? JSON.parse(storedValue) : initialValue)
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
}, { deep: true })
const update = (newValue) => {
value.value = newValue
}
const remove = () => {
localStorage.removeItem(key)
value.value = null
}
return {
value,
update,
remove
}
}
增强版,支持过期时间:
javascript复制import { ref, watch } from 'vue'
export function useExpirableStorage(key, initialValue, ttl = null) {
const item = localStorage.getItem(key)
let parsedValue = initialValue
let expired = false
if(item) {
try {
const { value, expires } = JSON.parse(item)
if(!expires || Date.now() < expires) {
parsedValue = value
} else {
expired = true
}
} catch {
// 忽略解析错误
}
}
const value = ref(parsedValue)
const update = (newValue) => {
const expires = ttl ? Date.now() + ttl : null
localStorage.setItem(key, JSON.stringify({ value: newValue, expires }))
value.value = newValue
}
if(expired) {
localStorage.removeItem(key)
}
watch(value, update, { deep: true })
return {
value,
update
}
}
13. 拖拽功能Hook实现
创建一个处理拖拽逻辑的通用Hook:
javascript复制import { ref, onMounted, onUnmounted } from 'vue'
export function useDrag(containerRef, options = {}) {
const isDragging = ref(false)
const position = ref({ x: 0, y: 0 })
const startPosition = ref({ x: 0, y: 0 })
const onMouseDown = (e) => {
isDragging.value = true
startPosition.value = {
x: e.clientX - position.value.x,
y: e.clientY - position.value.y
}
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp)
}
const onMouseMove = (e) => {
if(!isDragging.value) return
position.value = {
x: e.clientX - startPosition.value.x,
y: e.clientY - startPosition.value.y
}
if(options.onDrag) {
options.onDrag(position.value)
}
}
const onMouseUp = () => {
isDragging.value = false
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
if(options.onDragEnd) {
options.onDragEnd(position.value)
}
}
onMounted(() => {
const el = containerRef.value
el.addEventListener('mousedown', onMouseDown)
onUnmounted(() => {
el.removeEventListener('mousedown', onMouseDown)
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
})
})
return {
position,
isDragging
}
}
使用示例:
vue复制<template>
<div
ref="draggable"
:style="{
position: 'absolute',
left: `${position.x}px`,
top: `${position.y}px`,
cursor: isDragging ? 'grabbing' : 'grab'
}"
>
拖拽我
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useDrag } from './useDrag'
const draggable = ref(null)
const { position, isDragging } = useDrag(draggable, {
onDragEnd: (pos) => {
console.log('拖拽结束位置:', pos)
}
})
</script>
14. 无限滚动Hook实现
创建一个处理无限滚动加载的Hook:
javascript复制import { ref, onMounted, onUnmounted } from 'vue'
export function useInfiniteScroll(options) {
const isLoading = ref(false)
const page = ref(1)
const hasMore = ref(true)
const loadMore = async () => {
if(isLoading.value || !hasMore.value) return
isLoading.value = true
try {
const data = await options.loader(page.value)
options.onLoad(data)
if(data.length < options.pageSize) {
hasMore.value = false
} else {
page.value++
}
} catch (err) {
console.error('加载失败:', err)
} finally {
isLoading.value = false
}
}
const handleScroll = () => {
const { scrollTop, clientHeight, scrollHeight } = document.documentElement
const threshold = options.threshold || 100
if(scrollTop + clientHeight >= scrollHeight - threshold) {
loadMore()
}
}
onMounted(() => {
window.addEventListener('scroll', handleScroll)
// 初始加载
loadMore()
})
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll)
})
return {
isLoading,
hasMore,
reset: () => {
page.value = 1
hasMore.value = true
}
}
}
使用示例:
vue复制<script setup>
import { useInfiniteScroll } from './useInfiniteScroll'
const items = ref([])
const { isLoading } = useInfiniteScroll({
pageSize: 10,
loader: async (page) => {
const res = await fetch(`/api/items?page=${page}`)
return res.json()
},
onLoad: (data) => {
items.value = [...items.value, ...data]
}
})
</script>
15. 路由感知Hook实现
创建一个响应路由变化的Hook:
javascript复制import { watch } from 'vue'
import { useRoute } from 'vue-router'
export function useRouteWatcher(callback) {
const route = useRoute()
watch(() => route.path, (newPath, oldPath) => {
callback(newPath, oldPath, route)
}, { immediate: true })
return {
unwatch: () => stop()
}
}
增强版,支持特定路由监听:
javascript复制import { watch } from 'vue'
import { useRoute } from 'vue-router'
export function useRouteChange(options) {
const route = useRoute()
const unwatch = watch(() => route.path, (newPath, oldPath) => {
if(!options.routes || options.routes.includes(newPath)) {
options.handler(newPath, oldPath, route)
}
}, { immediate: options.immediate })
return {
unwatch
}
}
使用示例:
vue复制<script setup>
import { useRouteChange } from './useRouteChange'
useRouteChange({
routes: ['/dashboard', '/profile'],
handler: (newPath) => {
console.log('路由变化到:', newPath)
// 执行特定逻辑
}
})
</script>
16. 动画控制Hook实现
创建一个控制CSS动画的Hook:
javascript复制import { ref } from 'vue'
export function useAnimation(elRef, options = {}) {
const isPlaying = ref(false)
const play = () => {
const el = elRef.value
if(!el) return
el.style.animation = `${options.name || 'fade'} ${options.duration || '1s'} ${options.timing || 'ease'}`
el.style.animationIterationCount = options.iteration || '1'
isPlaying.value = true
const onAnimationEnd = () => {
isPlaying.value = false
el.removeEventListener('animationend', onAnimationEnd)
if(options.onEnd) {
options.onEnd()
}
}
el.addEventListener('animationend', onAnimationEnd)
}
return {
isPlaying,
play
}
}
使用示例:
vue复制<template>
<div ref="box" class="box"></div>
<button @click="play">播放动画</button>
</template>
<script setup>
import { ref } from 'vue'
import { useAnimation } from './useAnimation'
const box = ref(null)
const { isPlaying, play } = useAnimation(box, {
name: 'bounce',
duration: '0.5s',
onEnd: () => {
console.log('动画结束')
}
})
</script>
17. 设备特性检测Hook
创建一个检测设备特性的Hook:
javascript复制import { ref, onMounted } from 'vue'
export function useDevice() {
const isMobile = ref(false)
const isTablet = ref(false)
const isDesktop = ref(true)
const orientation = ref('landscape')
const isOnline = ref(navigator.onLine)
const checkDevice = () => {
const width = window.innerWidth
isMobile.value = width < 768
isTablet.value = width >= 768 && width < 1024
isDesktop.value = width >= 1024
orientation.value =
window.innerHeight > window.innerWidth ? 'portrait' : 'landscape'
}
onMounted(() => {
checkDevice()
window.addEventListener('resize', checkDevice)
window.addEventListener('online', () => isOnline.value = true)
window.addEventListener('offline', () => isOnline.value = false)
})
return {
isMobile,
isTablet,
isDesktop,
orientation,
isOnline
}
}
增强版,支持更多特性检测:
javascript复制import { ref, onMounted } from 'vue'
export function useDeviceCapabilities() {
const capabilities = ref({
touch: 'ontouchstart' in window,
geolocation: 'geolocation' in navigator,
camera: !!navigator.mediaDevices?.getUserMedia,
notifications: 'Notification' in window,
vibration: 'vibrate' in navigator,
bluetooth: 'bluetooth' in navigator
})
const checkBattery = async () => {
if('getBattery' in navigator) {
try {
const battery = await navigator.getBattery()
return {
level: battery.level,
charging: battery.charging
}
} catch {
return null
}
}
return null
}
return {
capabilities,
checkBattery
}
}
18. WebSocket连接Hook
创建一个管理WebSocket连接的Hook:
javascript复制import { ref, onUnmounted } from 'vue'
export function useWebSocket(url, options = {}) {
const socket = ref(null)
const isConnected = ref(false)
const messages = ref([])
const error = ref(null)
const connect = () => {
socket.value = new WebSocket(url)
socket.value.onopen = () => {
isConnected.value = true
if(options.onOpen) options.onOpen()
}
socket.value.onmessage = (event) => {
const data = options.parse ? options.parse(event.data) : event.data
messages.value.push(data)
if(options.onMessage) options.onMessage(data)
}
socket.value.onerror = (err) => {
error.value = err
if(options.onError) options.onError(err)
}
socket.value.onclose = () => {
isConnected.value = false
if(options.onClose) options.onClose()
}
}
const send = (data) => {
if(isConnected.value && socket.value) {
const payload = options.stringify ? options.stringify(data) : data
socket.value.send(payload)
}
}
const disconnect = () => {
if(socket.value) {
socket.value.close()
}
}
onUnmounted(() => {
disconnect()
})
return {
socket,
isConnected,
messages,
error,
connect,
send,
disconnect
}
}
使用示例:
vue复制<script setup>
import { useWebSocket } from './useWebSocket'
const { messages, send, isConnected } = useWebSocket('wss://echo.websocket.org', {
onMessage: (data) => {
console.log('收到消息:', data)
}
})
const sendMessage = () => {
send('Hello WebSocket!')
}
</script>
19. 权限请求Hook实现
创建一个处理浏览器权限请求的Hook:
javascript复制import { ref } from 'vue'
export function usePermission(permissionName) {
const status = ref('prompt') // 'granted', 'denied', 'prompt'
const isSupported = ref('permissions' in navigator)
const check = async () => {
if(!isSupported.value) {
status.value = 'unsupported'
return
}
try {
const result = await navigator.permissions.query({ name: permissionName })
status.value = result.state
result.onchange = () => {
status.value = result.state
}
} catch (err) {
console.error('检查权限失败:', err)
status.value = 'error'
}
}
const request = async () => {
try {
let granted = false
switch(permissionName) {
case 'camera':
granted = await navigator.mediaDevices.getUserMedia({ video: true })
.then(() => true)
.catch(() => false)
break
case 'geolocation':
granted = await new Promise(resolve =>
navigator.geolocation.getCurrentPosition(
() => resolve(true),
() => resolve(false)
)
)
break
case 'notifications':
granted = await Notification.requestPermission()
.then(perm => perm === 'granted')
break
default:
console.warn(`不支持请求的权限类型: ${permissionName}`)
}
if(granted) {
status.value = 'granted'
return true
} else {
status.value = 'denied'
return false
}
} catch (err) {
console.error('请求权限失败:', err)
status.value = 'error'
return false
}
}
return {
isSupported,
status,
check,
request
}
}
使用示例:
vue复制<script setup>
import { usePermission } from './usePermission'
const { status, request } = usePermission('geolocation')
const requestLocation = async () => {
const granted = await request()
if(granted) {
console.log('已获得位置权限')
}
}
</script>
20. 高级模式:Hook工厂函数
对于需要高度可定制的场景,可以创建Hook工厂:
javascript复制export function createPaginatedHook(fetcher, options = {}) {
return function usePaginatedData(initialParams = {}) {
const data = ref([])
const isLoading = ref(false)
const error = ref(null)
const page = ref(1)
const hasMore = ref(true)
const fetchData = async (params = {}) => {
if(isLoading.value || !hasMore.value) return
isLoading.value = true
error.value = null
try {
const result = await fetcher({
page: page.value,
pageSize: options.pageSize || 10,
...initialParams,
...params
})
if(options.onSuccess) {
options.onSuccess(result)
}
if(result.length < (options.pageSize || 10)) {
hasMore.value = false
} else {
page.value++
}
data.value = [...data.value, ...result]
} catch (err) {
error.value = err
if(options.onError) {
options.onError(err)
}
} finally {
isLoading.value = false
}
}
const reset = () => {
data.value = []
page.value = 1
hasMore.value = true
error.value = null
}
return {
data,
isLoading,
error,
fetchData,
reset,
hasMore
}
}
}
使用示例:
javascript复制// 创建特定Hook
const useProductList = createPaginatedList(
async ({ page, pageSize }) => {
const res = await fetch(`/api/products?page=${page}&size=${pageSize}`)
return res.json()
},
{
pageSize: 20,
onSuccess: (data) => {
console.log('加载了新产品:', data)
}
}
)
// 在组件中使用
const { data, fetchData } = useProductList({ category: 'electronics' })
