组合式函数是利用Vue组合式API封装和复用有状态逻辑的函数。
// composables/useLocalStorage.js
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
const storedValue = localStorage.getItem(key)
const data = ref(storedValue ? JSON.parse(storedValue) : defaultValue)
watch(data, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
}, { deep: true })
return data
}
<script setup>
import { useLocalStorage } from '@/composables/useLocalStorage'
const user = useLocalStorage('user', { name: '', age: 0 })
const theme = useLocalStorage('theme', 'light')
</script>
// composables/useDebounce.js
import { ref, watch } from 'vue'
export function useDebounce(value, delay = 300) {
const debouncedValue = ref(value.value)
let timeout
watch(value, (newValue) => {
clearTimeout(timeout)
timeout = setTimeout(() => {
debouncedValue.value = newValue
}, delay)
})
return debouncedValue
}
<script setup>
import { ref } from 'vue'
import { useDebounce } from '@/composables/useDebounce'
const searchText = ref('')
const debouncedSearch = useDebounce(searchText, 500)
watch(debouncedSearch, (value) => {
console.log('搜索:', value)
// 执行搜索请求
})
</script>
// composables/useEventListener.js
import { onMounted, onUnmounted } from 'vue'
export function useEventListener(target, event, callback) {
onMounted(() => {
target.addEventListener(event, callback)
})
onUnmounted(() => {
target.removeEventListener(event, callback)
})
}
<script setup>
import { ref } from 'vue'
import { useEventListener } from '@/composables/useEventListener'
const x = ref(0)
const y = ref(0)
useEventListener(window, 'mousemove', (e) => {
x.value = e.pageX
y.value = e.pageY
})
</script>
// composables/useAsync.js
import { ref } from 'vue'
export function useAsync(asyncFunction) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
async function execute(...args) {
loading.value = true
error.value = null
try {
data.value = await asyncFunction(...args)
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}
return { data, error, loading, execute }
}
<script setup>
import { useAsync } from '@/composables/useAsync'
const fetchUsers = async () => {
const response = await fetch('/api/users')
return response.json()
}
const { data: users, error, loading, execute } = useAsync(fetchUsers)
// 执行请求
execute()
</script>
// composables/useIntersectionObserver.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useIntersectionObserver(target, options = {}) {
const isVisible = ref(false)
let observer
onMounted(() => {
observer = new IntersectionObserver(([entry]) => {
isVisible.value = entry.isIntersecting
}, options)
if (target.value) {
observer.observe(target.value)
}
})
onUnmounted(() => {
if (observer) {
observer.disconnect()
}
})
return isVisible
}
<template>
<div ref="elementRef">
{{ isVisible ? '可见' : '不可见' }}
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useIntersectionObserver } from '@/composables/useIntersectionObserver'
const elementRef = ref(null)
const isVisible = useIntersectionObserver(elementRef)
</script>
自定义指令用于直接操作DOM元素。
// main.js
const app = createApp(App)
app.directive('focus', {
mounted(el) {
el.focus()
}
})
app.mount('#app')
<script setup>
const vFocus = {
mounted: (el) => el.focus()
}
</script>
<template>
<input v-focus />
</template>
const myDirective = {
// 元素插入到DOM前调用
created(el, binding, vnode, prevVnode) {},
// 元素插入到DOM后调用
mounted(el, binding, vnode, prevVnode) {},
// 父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 父组件更新后调用
updated(el, binding, vnode, prevVnode) {},
// 元素卸载前调用
beforeUnmount(el, binding, vnode, prevVnode) {},
// 元素卸载后调用
unmounted(el, binding, vnode, prevVnode) {}
}
app.directive('color', {
mounted(el, binding) {
// binding.value: 指令的值
// binding.arg: 指令的参数
// binding.modifiers: 指令的修饰符对象
el.style.color = binding.value
}
})
<template>
<!-- v-color="'red'" -->
<p v-color="'red'">红色文本</p>
<!-- v-color:background="'blue'" -->
<p v-color:background="'blue'">蓝色背景</p>
<!-- v-color.important="'green'" -->
<p v-color.important="'green'">绿色重要文本</p>
</template>
// directives/loading.js
export const vLoading = {
mounted(el, binding) {
if (binding.value) {
el.classList.add('loading')
el.style.position = 'relative'
const spinner = document.createElement('div')
spinner.className = 'spinner'
spinner.innerHTML = '加载中...'
el.appendChild(spinner)
}
},
updated(el, binding) {
if (binding.value) {
if (!el.querySelector('.spinner')) {
const spinner = document.createElement('div')
spinner.className = 'spinner'
spinner.innerHTML = '加载中...'
el.appendChild(spinner)
}
} else {
const spinner = el.querySelector('.spinner')
if (spinner) {
spinner.remove()
}
}
}
}
<template>
<div v-loading="isLoading">
内容区域
</div>
</template>
// directives/lazy.js
export const vLazy = {
mounted(el, binding) {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
el.src = binding.value
observer.unobserve(el)
}
})
observer.observe(el)
el._observer = observer
},
unmounted(el) {
if (el._observer) {
el._observer.disconnect()
}
}
}
<template>
<img v-lazy="imageUrl" alt="懒加载图片" />
</template>
// directives/permission.js
export const vPermission = {
mounted(el, binding) {
const { value } = binding
const userPermissions = ['read', 'write'] // 从store获取
if (value && !userPermissions.includes(value)) {
el.parentNode?.removeChild(el)
}
}
}
<template>
<button v-permission="'admin'">删除</button>
<button v-permission="'write'">编辑</button>
</template>
// directives/clickOutside.js
export const vClickOutside = {
mounted(el, binding) {
el._clickOutside = (event) => {
if (!el.contains(event.target)) {
binding.value(event)
}
}
document.addEventListener('click', el._clickOutside)
},
unmounted(el) {
document.removeEventListener('click', el._clickOutside)
delete el._clickOutside
}
}
<template>
<div v-click-outside="closeMenu">
菜单内容
</div>
</template>
<script setup>
function closeMenu() {
console.log('点击了外部')
}
</script>
// directives/copy.js
export const vCopy = {
mounted(el, binding) {
el.addEventListener('click', () => {
const text = binding.value
navigator.clipboard.writeText(text).then(() => {
console.log('复制成功:', text)
})
})
}
}
<template>
<button v-copy="'要复制的文本'">点击复制</button>
</template>
// composables/useForm.js
import { reactive, computed } from 'vue'
export function useForm(initialValues, rules) {
const form = reactive({ ...initialValues })
const errors = reactive({})
function validate(field) {
const rule = rules[field]
if (!rule) return true
const value = form[field]
let error = ''
if (rule.required && !value) {
error = rule.message || '此字段必填'
} else if (rule.min && value.length < rule.min) {
error = `最少${rule.min}个字符`
} else if (rule.pattern && !rule.pattern.test(value)) {
error = rule.message || '格式不正确'
}
errors[field] = error
return !error
}
function validateAll() {
let isValid = true
for (const field in rules) {
if (!validate(field)) {
isValid = false
}
}
return isValid
}
const isValid = computed(() => {
return Object.values(errors).every(error => !error)
})
return {
form,
errors,
validate,
validateAll,
isValid
}
}
<script setup>
import { useForm } from '@/composables/useForm'
const { form, errors, validate, validateAll, isValid } = useForm(
{ username: '', email: '', password: '' },
{
username: { required: true, min: 3 },
email: {
required: true,
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: '邮箱格式不正确'
},
password: { required: true, min: 6 }
}
)
function handleSubmit() {
if (validateAll()) {
console.log('提交表单', form)
}
}
</script>