在前端开发中,我们经常会遇到需要在不同位置复用相同内容的场景,如抽屉组件(Drawer)。下面将通过一个具体的例子,对比分析createReusableTemp

83 阅读4分钟

示例场景分析

假设我们有一个ReuseTabTemplate组件,需要在两个地方使用:

  1. 在页面中正常渲染

  2. 在全屏抽屉中渲染(点击按钮弹出)

两种场景共享相同的内容和状态,但样式和位置不同。

方案对比实现

1. 使用 createReusableTemplate 实现

typescript

// useTabTemplate.ts
import { createReusableTemplate, ref, computed } from '@vueuse/core'

export const useTabTemplate = createReusableTemplate((props) => {
  // 共享状态
  const activeTab = ref('tab1')
  const tabData = ref([
    { id: 'tab1', title: '标签1', content: '标签1内容' },
    { id: 'tab2', title: '标签2', content: '标签2内容' }
  ])
  
  const tabHeight = computed(() => {
    return props.tabHeight || '500px'
  })
  
  const setActiveTab = (tabId: string) => {
    activeTab.value = tabId
  }
  
  // 渲染函数
  const renderTabs = () => {
    return h('div', { class: 'tab-container', style: `height: ${tabHeight.value}` }, [
      h('div', { class: 'tab-header' },
        tabData.value.map(tab => 
          h('button', {
            class: { 'active': tab.id === activeTab.value },
            onClick: () => setActiveTab(tab.id)
          }, tab.title)
        )
      ),
      h('div', { class: 'tab-content' },
        tabData.value.find(tab => tab.id === activeTab.value)?.content
      )
    ])
  }
  
  return {
    renderTabs,
    activeTab,
    tabData
  }
})

vue

<!-- 使用页面 -->
<template>
  <div>
    <button @click="drawerVisible = true">打开抽屉</button>
    
    <!-- 正常渲染 -->
    <div class="normal-container">
      <component :is="tabTemplate.renderTabs" />
    </div>
    
    <!-- 抽屉渲染 -->
    <el-drawer
      size="100%"
      v-model="drawerVisible"
      :show-close="false"
      :append-to-body="true"
      title=""
      direction="btt"
      class="demo-drawer"
    >
      <component :is="tabTemplate.renderTabs" />
    </el-drawer>
  </div>
</template>

<script>
import { useTabTemplate } from './useTabTemplate'

export default {
  setup() {
    const tabTemplate = useTabTemplate({ tabHeight: '80%' })
    const drawerVisible = ref(false)
    
    return {
      tabTemplate,
      drawerVisible
    }
  }
}
</script>
2. 传统渲染函数实现

vue

<!-- 页面组件 -->
<template>
  <div>
    <button @click="drawerVisible = true">打开抽屉</button>
    
    <!-- 正常渲染 -->
    <div class="normal-container">
      <div class="tab-container" :style="{ height: tabHeight }">
        <div class="tab-header">
          <button 
            v-for="tab in tabData" 
            :key="tab.id" 
            :class="{ active: activeTab === tab.id }"
            @click="activeTab = tab.id"
          >{{ tab.title }}</button>
        </div>
        <div class="tab-content">
          {{ tabData.find(tab => tab.id === activeTab)?.content }}
        </div>
      </div>
    </div>
    
    <!-- 抽屉渲染 -->
    <el-drawer
      size="100%"
      v-model="drawerVisible"
      :show-close="false"
      :append-to-body="true"
      title=""
      direction="btt"
      class="demo-drawer"
    >
      <div class="tab-container" :style="{ height: drawerHeight }">
        <div class="tab-header">
          <button 
            v-for="tab in tabData" 
            :key="tab.id" 
            :class="{ active: activeTab === tab.id }"
            @click="activeTab = tab.id"
          >{{ tab.title }}</button>
        </div>
        <div class="tab-content">
          {{ tabData.find(tab => tab.id === activeTab)?.content }}
        </div>
      </div>
    </el-drawer>
  </div>
</template>

<script>
export default {
  data() {
    return {
      drawerVisible: false,
      activeTab: 'tab1',
      tabData: [
        { id: 'tab1', title: '标签1', content: '标签1内容' },
        { id: 'tab2', title: '标签2', content: '标签2内容' }
      ],
      tabHeight: '500px',
      drawerHeight: '80%'
    }
  }
}
</script>
3. React JSX 实现

jsx

// TabComponent.jsx
import React, { useState } from 'react'
import { Drawer } from 'element-react'

const TabComponent = () => {
  const [drawerVisible, setDrawerVisible] = useState(false)
  const [activeTab, setActiveTab] = useState('tab1')
  const tabData = [
    { id: 'tab1', title: '标签1', content: '标签1内容' },
    { id: 'tab2', title: '标签2', content: '标签2内容' }
  ]
  
  const renderTabs = (height) => (
    <div className="tab-container" style={{ height }}>
      <div className="tab-header">
        {tabData.map(tab => (
          <button
            key={tab.id}
            className={activeTab === tab.id ? 'active' : ''}
            onClick={() => setActiveTab(tab.id)}
          >{tab.title}</button>
        ))}
      </div>
      <div className="tab-content">
        {tabData.find(tab => tab.id === activeTab)?.content}
      </div>
    </div>
  )
  
  return (
    <div>
      <button onClick={() => setDrawerVisible(true)}>打开抽屉</button>
      
      {/* 正常渲染 */}
      <div className="normal-container">
        {renderTabs('500px')}
      </div>
      
      {/* 抽屉渲染 */}
      <Drawer
        size="100%"
        visible={drawerVisible}
        onClose={() => setDrawerVisible(false)}
        showClose={false}
        appendToBody={true}
        title=""
        direction="btt"
        className="demo-drawer"
      >
        {renderTabs('80%')}
      </Drawer>
    </div>
  )
}

export default TabComponent
4. Vue Teleport 实现

vue

<template>
  <div>
    <button @click="drawerVisible = true">打开抽屉</button>
    
    <!-- 正常渲染 -->
    <div class="normal-container">
      <ReuseTabTemplate :tabHeight="tabHeight" />
    </div>
    
    <!-- 抽屉渲染 -->
    <el-drawer
      size="100%"
      v-model="drawerVisible"
      :show-close="false"
      :append-to-body="true"
      title=""
      direction="btt"
      class="demo-drawer"
    >
      <ReuseTabTemplate :tabHeight="drawerHeight" />
    </el-drawer>
  </div>
</template>

<script>
import ReuseTabTemplate from './ReuseTabTemplate.vue'

export default {
  components: { ReuseTabTemplate },
  data() {
    return {
      drawerVisible: false,
      tabHeight: '500px',
      drawerHeight: '80%'
    }
  }
}
</script>

vue

<!-- ReuseTabTemplate.vue -->
<template>
  <div class="tab-container" :style="{ height: tabHeight }">
    <div class="tab-header">
      <button 
        v-for="tab in tabData" 
        :key="tab.id" 
        :class="{ active: activeTab === tab.id }"
        @click="activeTab = tab.id"
      >{{ tab.title }}</button>
    </div>
    <div class="tab-content">
      {{ tabData.find(tab => tab.id === activeTab)?.content }}
    </div>
  </div>
</template>

<script>
export default {
  props: {
    tabHeight: {
      type: String,
      default: '500px'
    }
  },
  data() {
    return {
      activeTab: 'tab1',
      tabData: [
        { id: 'tab1', title: '标签1', content: '标签1内容' },
        { id: 'tab2', title: '标签2', content: '标签2内容' }
      ]
    }
  }
}
</script>

方案优缺点对比

方案优点缺点
createReusableTemplate1. 逻辑与渲染完全分离,真正实现代码复用 2. 共享状态简单高效 3. 支持多容器渲染 4. 维护单一源码,内容一致性强1. 学习曲线较陡 2. 调试复杂度较高 3. 依赖 Vueuse 库
传统渲染函数1. 简单直接 2. 无需额外学习成本 3. 适合小型项目1. 代码重复严重 2. 状态管理分散 3. 维护困难,修改需同步多处
React JSX1. 灵活的 JavaScript 控制 2. 组件复用机制成熟 3. 适合 React 生态项目1. 状态管理复杂(需使用 useContext 或 Redux 等) 2. 跨框架不兼容
Vue Teleport1. 解决渲染位置问题 2. 简单易用 3. 适合只需位置变化的场景1. 仍需封装组件 2. 状态管理仍需组件间通信 3. 内容复用有限

性能对比

指标createReusableTemplate传统渲染函数React JSXVue Teleport
内存占用低(共享逻辑)高(重复状态)高(组件实例)中(组件实例)
初始化时间低(一次初始化)
更新渲染效率高(响应式优化)
多实例性能

决策指南

  1. 选择 createReusableTemplate

    • 需要在多个地方复用相同内容和逻辑
    • 希望集中管理共享状态
    • 追求极致的代码复用和维护性
    • 项目使用 Vue 3 和组合式 API
  2. 选择传统渲染函数

    • 项目规模较小
    • 不需要复杂的状态共享
    • 希望快速实现功能
  3. 选择 React JSX

    • 项目基于 React 生态
    • 需要 JavaScript 完全控制
    • 组件层级较深,需要复杂状态管理
  4. 选择 Vue Teleport

    • 只需要解决渲染位置问题
    • 不需要复杂的逻辑复用
    • 内容定制需求较高

总结

对于需要在多个容器中复用相同内容和状态的场景,createReusableTemplate提供了最优雅的解决方案。它通过逻辑与渲染的分离,实现了真正的代码复用,同时保持了良好的性能和可维护性。相比之下,传统方法要么导致代码重复,要么引入不必要的组件封装开销。

如果你的项目使用 Vue 3,并且有复杂的复用需求,createReusableTemplate是首选方案。它代表了前端组件设计从 "模板复用" 到 "逻辑复用" 的重要演进,为现代前端开发提供了更高效的工具。