Vue 3.5 重磅新特性:useTemplateRef 让模板引用更优雅、更高效
·
Vue 3.5 重磅新特性:useTemplateRef 让模板引用更优雅、更高效
文章目录
1. 概述
Vue 3.5 引入了 useTemplateRef API,提供了一种更类型安全的方式来引用模板中的 DOM 元素和组件实例。
2. 传统模板引用方式
2.1 基本用法
<template>
<input ref="inputRef" />
<button @click="focusInput">聚焦输入框</button>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
// 传统方式 - 类型为 any 或需要手动断言
const inputRef = ref<HTMLInputElement | null>(null)
const focusInput = () => {
if (inputRef.value) {
inputRef.value.focus()
}
}
onMounted(() => {
// 组件挂载后可以访问
console.log(inputRef.value)
})
</script>
2.2 引用组件实例
<template>
<ChildComponent ref="childRef" />
<button @click="callChildMethod">调用子组件方法</button>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
// 需要定义组件实例类型
interface ChildComponentInstance {
someMethod: () => void
someValue: number
}
const childRef = ref<ChildComponentInstance | null>(null)
const callChildMethod = () => {
if (childRef.value) {
childRef.value.someMethod()
}
}
</script>
3. useTemplateRef 新 API
3.1 基本用法
<template>
<input ref="inputRef" />
<button @click="focusInput">聚焦输入框</button>
</template>
<script setup lang="ts">
import { useTemplateRef } from 'vue'
// 自动推断为 HTMLInputElement | null
const inputRef = useTemplateRef<HTMLInputElement>('inputRef')
const focusInput = () => {
// 类型安全,自动补全 input 元素的方法和属性
inputRef.value?.focus()
}
</script>
3.2 多个引用
<template>
<input ref="inputRef" />
<textarea ref="textareaRef" />
<button ref="buttonRef">按钮</button>
</template>
<script setup lang="ts">
import { useTemplateRef } from 'vue'
const inputRef = useTemplateRef<HTMLInputElement>('inputRef')
const textareaRef = useTemplateRef<HTMLTextAreaElement>('textareaRef')
const buttonRef = useTemplateRef<HTMLButtonElement>('buttonRef')
</script>
4. 组件引用
4.1 引用子组件实例
<!-- ChildComponent.vue -->
<template>
<div>
<p>计数: {{ count }}</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
const increment = () => {
count.value++
}
const getCount = (): number => {
return count.value
}
// 暴露给父组件的方法和属性
defineExpose({
count,
increment,
getCount
})
</script>
<!-- ParentComponent.vue -->
<template>
<ChildComponent ref="childRef" />
<button @click="handleClick">操作子组件</button>
</template>
<script setup lang="ts">
import { useTemplateRef } from 'vue'
import ChildComponent from './ChildComponent.vue'
// 定义子组件实例类型
interface ChildComponentExposed {
count: number
increment: () => void
getCount: () => number
}
const childRef = useTemplateRef<ChildComponentExposed>('childRef')
const handleClick = () => {
if (childRef.value) {
// 类型安全的方法调用
childRef.value.increment()
console.log('当前计数:', childRef.value.getCount())
}
}
</script>
5. 动态引用
5.1 v-for 中的引用
<template>
<div>
<div
v-for="(item, index) in items"
:key="item.id"
:ref="setItemRef"
>
{{ item.name }}
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
interface Item {
id: number
name: string
}
const items = ref<Item[]>([
{ id: 1, name: '项目1' },
{ id: 2, name: '项目2' },
{ id: 3, name: '项目3' }
])
const itemRefs = ref<HTMLElement[]>([])
const setItemRef = (el: any) => {
if (el) {
itemRefs.value.push(el)
}
}
onMounted(() => {
console.log('所有元素引用:', itemRefs.value)
})
</script>
5.2 使用 useTemplateRef 处理动态引用
<template>
<div>
<div
v-for="(item, index) in items"
:key="item.id"
:ref="(el) => setDynamicRef(index, el)"
>
{{ item.name }}
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
interface Item {
id: number
name: string
}
const items = ref<Item[]>([
{ id: 1, name: '项目1' },
{ id: 2, name: '项目2' },
{ id: 3, name: '项目3' }
])
const dynamicRefs = ref<Map<number, HTMLElement>>(new Map())
const setDynamicRef = (index: number, el: HTMLElement | null) => {
if (el) {
dynamicRefs.value.set(index, el)
} else {
dynamicRefs.value.delete(index)
}
}
</script>
6. 响应式引用处理
6.1 监听引用变化
<template>
<div>
<input
v-if="showInput"
ref="inputRef"
placeholder="动态显示的输入框"
/>
<button @click="toggleInput">切换输入框</button>
</div>
</template>
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue'
import { useTemplateRef } from 'vue'
const showInput = ref(true)
const inputRef = useTemplateRef<HTMLInputElement>('inputRef')
// 监听引用变化
watch(inputRef, (newRef, oldRef) => {
console.log('引用变化:', { oldRef, newRef })
if (newRef) {
newRef.focus()
}
})
const toggleInput = async () => {
showInput.value = !showInput.value
await nextTick()
// 切换后自动聚焦
inputRef.value?.focus()
}
</script>
7. 实际应用场景
7.1 表单自动聚焦
<template>
<form @submit="handleSubmit">
<input
ref="firstInputRef"
v-model="form.name"
placeholder="姓名"
/>
<input
ref="emailInputRef"
v-model="form.email"
placeholder="邮箱"
/>
<button type="submit">提交</button>
</form>
</template>
<script setup lang="ts">
import { reactive, onMounted } from 'vue'
import { useTemplateRef } from 'vue'
const firstInputRef = useTemplateRef<HTMLInputElement>('firstInputRef')
const emailInputRef = useTemplateRef<HTMLInputElement>('emailInputRef')
const form = reactive({
name: '',
email: ''
})
onMounted(() => {
// 页面加载后自动聚焦到第一个输入框
firstInputRef.value?.focus()
})
const handleSubmit = (e: Event) => {
e.preventDefault()
if (!form.name) {
firstInputRef.value?.focus()
return
}
if (!form.email) {
emailInputRef.value?.focus()
return
}
console.log('表单提交:', form)
}
</script>
7.2 集成第三方库
<template>
<div ref="chartRef" style="width: 100%; height: 400px;"></div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
import { useTemplateRef } from 'vue'
import * as echarts from 'echarts'
const chartRef = useTemplateRef<HTMLDivElement>('chartRef')
let chart: echarts.ECharts | null = null
onMounted(() => {
if (chartRef.value) {
chart = echarts.init(chartRef.value)
const option = {
title: {
text: '示例图表'
},
tooltip: {},
xAxis: {
data: ['A', 'B', 'C', 'D', 'E']
},
yAxis: {},
series: [{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10]
}]
}
chart.setOption(option)
}
})
onUnmounted(() => {
chart?.dispose()
})
</script>
7.3 组件通信和协作
<!-- Modal.vue -->
<template>
<div v-if="isOpen" class="modal">
<div class="modal-content">
<div ref="headerRef" class="modal-header">
<slot name="header">
<h2>默认标题</h2>
</slot>
</div>
<div class="modal-body">
<slot></slot>
</div>
<div ref="footerRef" class="modal-footer">
<slot name="footer">
<button @click="close">关闭</button>
</slot>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useTemplateRef } from 'vue'
interface Props {
isOpen: boolean
}
defineProps<Props>()
const emit = defineEmits<{
close: []
}>()
const headerRef = useTemplateRef<HTMLDivElement>('headerRef')
const footerRef = useTemplateRef<HTMLDivElement>('footerRef')
const close = () => {
emit('close')
}
// 暴露方法给父组件
defineExpose({
focusHeader: () => headerRef.value?.focus(),
focusFooter: () => footerRef.value?.focus()
})
</script>
<!-- ParentComponent.vue -->
<template>
<button @click="openModal">打开模态框</button>
<Modal ref="modalRef" :is-open="isModalOpen" @close="closeModal">
<template #header>
<h2>自定义标题</h2>
</template>
<p>模态框内容</p>
</Modal>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useTemplateRef } from 'vue'
import Modal from './Modal.vue'
interface ModalExposed {
focusHeader: () => void
focusFooter: () => void
}
const modalRef = useTemplateRef<ModalExposed>('modalRef')
const isModalOpen = ref(false)
const openModal = () => {
isModalOpen.value = true
// 可以调用子组件暴露的方法
setTimeout(() => {
modalRef.value?.focusHeader()
}, 100)
}
const closeModal = () => {
isModalOpen.value = false
}
</script>
8. 类型安全和最佳实践
8.1 严格的类型定义
<template>
<canvas ref="canvasRef" width="800" height="600"></canvas>
</template>
<script setup lang="ts">
import { useTemplateRef } from 'vue'
// 明确的类型定义
const canvasRef = useTemplateRef<HTMLCanvasElement>('canvasRef')
const drawOnCanvas = () => {
const canvas = canvasRef.value
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
// 类型安全的 Canvas 操作
ctx.fillStyle = 'red'
ctx.fillRect(10, 10, 100, 100)
}
</script>
8.2 自定义 Hook 封装
// composables/useAutoFocus.ts
import { useTemplateRef } from 'vue'
export function useAutoFocus(refName: string) {
const elementRef = useTemplateRef<HTMLElement>(refName)
const focus = () => {
elementRef.value?.focus()
}
const blur = () => {
elementRef.value?.blur()
}
const select = () => {
if (elementRef.value instanceof HTMLInputElement) {
elementRef.value.select()
}
}
return {
elementRef,
focus,
blur,
select
}
}
<template>
<input ref="searchInputRef" placeholder="搜索..." />
<button @click="focusSearch">聚焦搜索框</button>
</template>
<script setup lang="ts">
import { useAutoFocus } from '@/composables/useAutoFocus'
const { elementRef: searchInputRef, focus: focusSearch } = useAutoFocus('searchInputRef')
</script>
9. 与传统方式的对比
| 特性 | 传统 ref() |
useTemplateRef() |
|---|---|---|
| 类型安全 | 需要手动类型断言 | 自动类型推断 |
| 代码简洁性 | 需要泛型参数 | 更简洁的语法 |
| 模板一致性 | 需要匹配 ref 名称 | 自动名称匹配 |
| TypeScript 支持 | 良好 | 优秀 |
| 动态引用 | 相同 | 相同 |
10. 总结
useTemplateRef 的优势:
- 更好的类型推断:自动推断正确的元素或组件类型
- 更简洁的语法:减少模板代码和类型声明
- 改进的开发体验:更好的 IDE 支持和自动补全
- 类型安全:编译时类型检查,减少运行时错误
使用建议:
- 新项目优先使用
useTemplateRef - 现有项目可以逐步迁移
- 复杂场景结合传统
ref()使用 - 充分利用 TypeScript 的类型系统
Vue 3.5 的 useTemplateRef 为模板引用提供了更现代化、类型安全的解决方案,是 Vue 开发生态的重要进步。
更多推荐
所有评论(0)