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 的优势:

  1. 更好的类型推断:自动推断正确的元素或组件类型
  2. 更简洁的语法:减少模板代码和类型声明
  3. 改进的开发体验:更好的 IDE 支持和自动补全
  4. 类型安全:编译时类型检查,减少运行时错误

使用建议:

  • 新项目优先使用 useTemplateRef
  • 现有项目可以逐步迁移
  • 复杂场景结合传统 ref() 使用
  • 充分利用 TypeScript 的类型系统

Vue 3.5 的 useTemplateRef 为模板引用提供了更现代化、类型安全的解决方案,是 Vue 开发生态的重要进步。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐