示例场景分析
假设我们有一个ReuseTabTemplate组件,需要在两个地方使用:
-
在页面中正常渲染
-
在全屏抽屉中渲染(点击按钮弹出)
两种场景共享相同的内容和状态,但样式和位置不同。
方案对比实现
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>
方案优缺点对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| createReusableTemplate | 1. 逻辑与渲染完全分离,真正实现代码复用 2. 共享状态简单高效 3. 支持多容器渲染 4. 维护单一源码,内容一致性强 | 1. 学习曲线较陡 2. 调试复杂度较高 3. 依赖 Vueuse 库 |
| 传统渲染函数 | 1. 简单直接 2. 无需额外学习成本 3. 适合小型项目 | 1. 代码重复严重 2. 状态管理分散 3. 维护困难,修改需同步多处 |
| React JSX | 1. 灵活的 JavaScript 控制 2. 组件复用机制成熟 3. 适合 React 生态项目 | 1. 状态管理复杂(需使用 useContext 或 Redux 等) 2. 跨框架不兼容 |
| Vue Teleport | 1. 解决渲染位置问题 2. 简单易用 3. 适合只需位置变化的场景 | 1. 仍需封装组件 2. 状态管理仍需组件间通信 3. 内容复用有限 |
性能对比
| 指标 | createReusableTemplate | 传统渲染函数 | React JSX | Vue Teleport |
|---|---|---|---|---|
| 内存占用 | 低(共享逻辑) | 高(重复状态) | 高(组件实例) | 中(组件实例) |
| 初始化时间 | 低(一次初始化) | 中 | 中 | 中 |
| 更新渲染效率 | 高(响应式优化) | 中 | 中 | 中 |
| 多实例性能 | 优 | 差 | 差 | 中 |
决策指南
-
选择 createReusableTemplate:
- 需要在多个地方复用相同内容和逻辑
- 希望集中管理共享状态
- 追求极致的代码复用和维护性
- 项目使用 Vue 3 和组合式 API
-
选择传统渲染函数:
- 项目规模较小
- 不需要复杂的状态共享
- 希望快速实现功能
-
选择 React JSX:
- 项目基于 React 生态
- 需要 JavaScript 完全控制
- 组件层级较深,需要复杂状态管理
-
选择 Vue Teleport:
- 只需要解决渲染位置问题
- 不需要复杂的逻辑复用
- 内容定制需求较高
总结
对于需要在多个容器中复用相同内容和状态的场景,createReusableTemplate提供了最优雅的解决方案。它通过逻辑与渲染的分离,实现了真正的代码复用,同时保持了良好的性能和可维护性。相比之下,传统方法要么导致代码重复,要么引入不必要的组件封装开销。
如果你的项目使用 Vue 3,并且有复杂的复用需求,createReusableTemplate是首选方案。它代表了前端组件设计从 "模板复用" 到 "逻辑复用" 的重要演进,为现代前端开发提供了更高效的工具。