Vue 模板内复用技巧:createReusableTemplate 模式
1. 核心问题
在同一个 Vue 组件中,某一段模板代码需要在多处重复使用。
传统解决方案的局限性:
- 封装新组件 (
.vue文件): 过于繁琐,为了小段代码创建新文件增加了维护成本。 - 使用 TSX/JSX: 需要改变开发范式,并非所有团队都适用。
2. 核心思想:定义与使用分离
利用 Vue 的插槽 (Slot) 和函数式组件,在组件内部实现模板的“定义”与“使用”分离。
- 定义模板 (
DefineTemplate): 创建一个组件,它的唯一作用是捕获其插槽内容,并将其存为一个变量,但不渲染任何内容。 - 使用模板 (
UseTemplate): 创建另一个组件,它的作用是执行之前保存的插槽函数,从而在需要的地方将模板渲染出来。
3. 实现步骤
步骤一:创建可复用的 createReusableTemplate 函数
将“定义”和“使用”组件的逻辑封装成一个通用的组合式函数。
javascript
// composables/useReusableTemplate.js
import { defineComponent } from 'vue'
export function createReusableTemplate() {
let render
// 1. 定义模板的组件 (DefineTemplate)
// 它通过 setup 作用域捕获插槽,但不渲染任何东西
const DefineTemplate = defineComponent({
setup(props, { slots }) {
// 将默认插槽函数保存到外部变量 render
render = slots.default
// 返回 undefined,使其不渲染任何 DOM
return () => {}
}
})
// 2. 使用模板的组件 (UseTemplate)
// 一个简单的函数式组件,负责执行 render 函数
const UseTemplate = defineComponent((props) => {
// 执行 render 函数并传入 props,以支持作用域插槽
return () => render?.(props)
})
// 3. 返回两个组件,通常用数组方便解构
return [DefineTemplate, UseTemplate]
}步骤二:在组件中使用
在需要复用模板的组件中,导入并使用 createReusableTemplate。
vue
<script setup>
import { createReusableTemplate } from './composables/useReusableTemplate.js'
// 1. 创建模板定义和使用组件
const [DefineTemplate, UseTemplate] = createReusableTemplate()
const handleClick = () => {
console.log('事件从 setup 中触发了')
}
</script>
<template>
<DefineTemplate>
<div v-slot="{ title, onFoo }">
<h2>{{ title }}</h2>
<div @click="onFoo">这里是可复用的内容...</div>
<button @click="handleClick">点击触发 setup 事件</button>
</div>
</DefineTemplate>
<UseTemplate :title="'Header'" @foo="() => console.log('第一个实例被点击')" />
<hr />
<UseTemplate :title="'Main Content'" />
<hr />
<UseTemplate :title="'Footer'" />
</template>4. 进阶功能:传递 Props 和事件
传递 Props (属性)
- 传递: 在使用
<UseTemplate />时,像普通组件一样绑定props(如:title="'Header'"). - 接收:
UseTemplate组件通过props参数接收数据。- 它在调用
render(props)时将整个props对象传递给插槽。 - 在
<DefineTemplate>的插槽中,使用v-slot(作用域插槽) 来接收。- 标准写法:
<template v-slot="{ title }"> - 默认插槽简化写法:可以直接在组件标签上写
v-slot="{ title }"。
- 标准写法:
传递 Events (事件)
- 传递: 在使用
<UseTemplate />时,像普通组件一样绑定事件 (如@foo="...")。 - 接收:
- 事件监听器会作为
props的一部分被传递,格式为on<EventName>(如onFoo)。 - 同样通过
v-slot="{ onFoo }"在插槽中接收。 - 在模板中直接绑定即可,如
@click="onFoo"。
- 事件监听器会作为
5. ⚠️ 注意事项
- 定义必须在使用之前:
<DefineTemplate>组件的声明必须在模板中出现在任何<UseTemplate>之前。- 原因: Vue 模板的编译和执行是顺序的。如果
<UseTemplate>先执行,此时render函数尚未被赋值(仍为undefined),会导致运行时报错。
- 原因: Vue 模板的编译和执行是顺序的。如果
DefineTemplate不渲染: 该组件的setup返回一个空渲染函数() => {}或undefined,确保它只捕获插槽而不产生任何实际的 DOM 输出。- 作用域: 在
<DefineTemplate>插槽内定义的模板,既可以访问来自<UseTemplate>通过v-slot传递的props,也可以直接访问其所在父组件setup中定义的响应式数据和方法(如handleClick)。