Skip to content

Vue 模板内复用技巧:createReusableTemplate 模式

1. 核心问题

在同一个 Vue 组件中,某一段模板代码需要在多处重复使用。

传统解决方案的局限性:

  • 封装新组件 (.vue 文件): 过于繁琐,为了小段代码创建新文件增加了维护成本。
  • 使用 TSX/JSX: 需要改变开发范式,并非所有团队都适用。

2. 核心思想:定义与使用分离

利用 Vue 的插槽 (Slot)函数式组件,在组件内部实现模板的“定义”与“使用”分离。

  1. 定义模板 (DefineTemplate): 创建一个组件,它的唯一作用是捕获其插槽内容,并将其存为一个变量,但不渲染任何内容。
  2. 使用模板 (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 (属性)

  1. 传递: 在使用 <UseTemplate /> 时,像普通组件一样绑定 props (如 :title="'Header'").
  2. 接收:
    • UseTemplate 组件通过 props 参数接收数据。
    • 它在调用 render(props) 时将整个 props 对象传递给插槽。
    • <DefineTemplate> 的插槽中,使用 v-slot (作用域插槽) 来接收。
      • 标准写法: <template v-slot="{ title }">
      • 默认插槽简化写法:可以直接在组件标签上写 v-slot="{ title }"

传递 Events (事件)

  1. 传递: 在使用 <UseTemplate /> 时,像普通组件一样绑定事件 (如 @foo="...")。
  2. 接收:
    • 事件监听器会作为 props 的一部分被传递,格式为 on<EventName> (如 onFoo)。
    • 同样通过 v-slot="{ onFoo }" 在插槽中接收。
    • 在模板中直接绑定即可,如 @click="onFoo"

5. ⚠️ 注意事项

  • 定义必须在使用之前: <DefineTemplate> 组件的声明必须在模板中出现在任何 <UseTemplate> 之前。
    • 原因: Vue 模板的编译和执行是顺序的。如果 <UseTemplate> 先执行,此时 render 函数尚未被赋值(仍为 undefined),会导致运行时报错。
  • DefineTemplate 不渲染: 该组件的 setup 返回一个空渲染函数 () => {}undefined,确保它只捕获插槽而不产生任何实际的 DOM 输出。
  • 作用域: 在 <DefineTemplate> 插槽内定义的模板,既可以访问来自 <UseTemplate> 通过 v-slot 传递的 props,也可以直接访问其所在父组件 setup 中定义的响应式数据和方法(如 handleClick)。