一、引言:为什么需要动态组件?
1.1 传统 CRUD 开发的痛点
在传统的中后台开发中,每个业务模块都需要编写类似的代码:
问题分析 :
- ❌ 每个业务模块都要重复编写相同的组件控制逻辑
- ❌ 新增功能需要修改模板和脚本
- ❌ 组件之间耦合度高,难以复用
- ❌ 代码量大,维护成本高
1.2 动态组件的解决思路
核心理念 :将"组件渲染逻辑"从硬编码转为配置驱动
js
<!-- 动态方式:配置驱动 -->
<template>
<div>
<el-table :data="tableData">...</el-table>
<!-- 动态组件:根据配置自动渲染 -->
<component
v-for="(component, key) in components"
:key="key"
:is="ComponentConfig[key]?.component"
:ref="(el) => { if (el) comMapRef[key] = el }"
/>
</div>
</template>
优势 :
- ✅ 业务逻辑通过配置文件描述
- ✅ 新增功能只需添加配置,无需修改组件代码
- ✅ 组件高度复用,降低开发成本
- ✅ 符合开闭原则,易于扩展
二、核心实现原理
2.1 动态组件机制
Vue 的<component :is="...">是动态组件的核心: 工作原理 :
- :is 属性接收一个 组件对象 (不是字符串)
- Vue 在运行时动态解析该组件对象
- 创建并挂载对应的组件实例
与静态组件的区别 :

2.2 配置驱动渲染的完整链路
以"商品管理"为例,完整展示从配置到渲染的过程: 第一步:业务配置(model.js)
js
module.exports = {
model: 'dashboard',
name: '电商系统',
menu: [{
key: 'product',
name: '商品管理',
schemaConfig: {
api: '/api/proj/product',
// 1. Schema 配置:定义字段
schema: {
type: 'object',
properties: {
product_id: {
type: 'string',
label: '商品ID',
tableOptions: { width: 300 },
editFormOptions: { comType: 'input', disabled: true }
},
product_name: {
type: 'string',
label: '商品名称',
tableOptions: { width: 200 },
searchOptions: { comType: 'dynamicSelect', api: '/api/proj/getProductList' },
createFormOptions: { comType: 'input' },
editFormOptions: { comType: 'input' }
},
product_price: {
type: 'number',
label: '商品价格',
tableOptions: { width: 200 },
createFormOptions: { comType: 'inputNumber' },
editFormOptions: { comType: 'inputNumber' }
}
},
required: ['product_name']
},
// 2. 表格配置:定义按钮
tableConfig: {
headerButtons: [{
label: '新增商品',
eventKey: 'showComponent',
eventOptions: { comName: 'createForm' },
type: 'primary'
}],
rowButtons: [{
label: '修改商品',
eventKey: 'showComponent',
eventOptions: { comName: 'editForm' },
type: 'warning'
}, {
label: '删除商品',
eventKey: 'remove',
type: 'danger',
eventOptions: {
params: { product_id: 'schema::product_id' }
}
}]
},
// 3. 组件配置:定义弹窗
componentConfig: {
createForm: {
title: '新增商品',
saveBtnText: '新增商品'
},
editForm: {
mainKey: 'product_id',
title: '修改商品',
saveBtnText: '修改商品'
}
}
}
}]
}
第二步:Schema 构建(schema.js) buildDtoSchema 方法根据场景提取字段:
js
const buildDtoSchema = (_schema, comName) => {
const dotSchema = {
type: 'object',
properties: {}
}
for (const key in _schema.properties) {
const props = _schema.properties[key]
// 只处理有 xxxOptions 的字段
if (props[`${comName}Options`]) {
let dtoProps = {}
// 提取非 Options 属性(type、label、maxLength 等)
for (const pKey in props) {
if (pKey.indexOf('Options') < 0) {
dtoProps[pKey] = props[pKey]
}
}
// 提取当前场景的 Options
dtoProps.options = props[`${comName}Options`]
// 处理 required
const { required } = _schema
if (required && required.find(pk => pk === key)) {
dtoProps.options.required = true
}
dotSchema.properties[key] = dtoProps
}
}
return dotSchema
}
构建结果示例 :
js
// 调用:buildDtoSchema(schema, 'createForm')
// 输出:
{
type: 'object',
properties: {
product_name: {
type: 'string',
label: '商品名称',
options: {
comType: 'input',
required: true // 从 required 数组提取
}
},
product_price: {
type: 'number',
label: '商品价格',
options: {
comType: 'inputNumber'
}
}
}
}
核心设计 :
- 一份配置,多场景复用 : product_name 同时配置了 tableOptions 、 searchOptions 、 createFormOptions 、 editFormOptions
- 按需提取 :构建表格 schema 时只提取 tableOptions ,构建表单 schema 时只提取 createFormOptions
- 去噪处理 :移除其他场景的配置,避免干扰
第三步:动态渲染(schema-view.vue)
js
<template>
<el-row>
<SearchPanel
v-if="searchSchema?.properties && Object.keys(searchSchema.properties).length > 0"
@search="handleSearch"
@reset="handleReset"
/>
<TablePanel @operate="onOperate" />
<!-- 动态组件渲染 -->
<component
v-for="(component, key) in components"
:key="key"
:is="ComponentConfig[key]?.component"
:ref="(el) => { if (el) comMapRef[key] = el }"
@command="handleCommand"
/>
</el-row>
</template>
<script setup>
import ComponentConfig from './components/component-config.js'
import { useSchema } from './hook/schema.js'
import { provide, ref } from 'vue'
const comMapRef = ref({})
const { api, tableConfig, tableSchema, searchSchema, searchConfig, components } = useSchema()
// 通过 provide 下发配置
provide('schemaViewData', {
api, tableConfig, tableSchema,
searchSchema, searchConfig, components
})
// 事件处理
const onOperate = ({ btnConfig, rowData }) => {
const { eventKey } = btnConfig
if (EventHandlerMap[eventKey]) {
EventHandlerMap[eventKey]({ btnConfig, rowData })
}
}
</script>
第四步:组件注册(component-config.js)
js
import CreateForm from './create-form/create-form.vue'
import EditForm from './edit-form/edit-form.vue'
const ComponentConfig = {
createForm: { component: CreateForm },
editForm: { component: EditForm }
}
export default ComponentConfig
渲染流程 :
js
components = {
createForm: { schema: {...}, config: {...} },
editForm: { schema: {...}, config: {...} }
}
↓ v-for 遍历
<component :is="ComponentConfig['createForm']?.component" />
↓ 解析
ComponentConfig['createForm'].component = CreateForm
↓ 渲染
<CreateForm /> 组件实例
三、设计模式深度解析
3.1 策略模式
定义 :定义一系列算法,把它们一个个封装起来,并且使它们可相互替换。
在本项目中的应用 :
js
// 事件处理策略映射表
const EventHandlerMap = {
showComponent: showComponent, // 策略1:显示组件
remove: removeData, // 策略2:删除数据
export: exportData, // 策略3:导出数据(可扩展)
import: importData // 策略4:导入数据(可扩展)
}
// 根据 eventKey 选择策略
const onOperate = ({ btnConfig, rowData }) => {
const { eventKey } = btnConfig
// 传统方式:大量 if-else
// if (eventKey === 'showComponent') { ... }
// else if (eventKey === 'remove') { ... }
// else if (eventKey === 'export') { ... }
// 策略模式:一行代码
if (EventHandlerMap[eventKey]) {
EventHandlerMap[eventKey]({ btnConfig, rowData })
}
}
优势 :
- 消除条件分支 :用映射表替代 if-else
- 符合开闭原则 :新增策略只需添加映射项,不修改现有代码
- 易于测试 :每个策略独立,可单独测试
- 运行时切换 :可以根据配置动态选择策略
3.2 工厂模式
定义 :定义一个用于创建对象的接口,让子类决定实例化哪一个类。
在本项目中的应用 :
js
// 组件工厂
const ComponentConfig = {
createForm: { component: CreateForm },
editForm: { component: EditForm },
detailDrawer: { component: DetailDrawer },
importDialog: { component: ImportDialog }
}
// 根据 key 生产组件实例
<component :is="ComponentConfig[key]?.component" />
工厂模式的优势 :
- 封装创建逻辑 :业务层只需知道 key,不需要知道组件实现
- 统一管理 :所有组件注册在一个地方,便于维护
- 延迟创建 :只在需要时才创建组件实例
- 易于替换 :修改工厂配置即可替换组件实现 对比传统方式 :
js
<!-- 传统方式:硬编码 -->
<template>
<CreateForm v-if="showCreate" />
<EditForm v-if="showEdit" />
<DetailDrawer v-if="showDetail" />
</template>
<script setup>
import CreateForm from './CreateForm.vue'
import EditForm from './EditForm.vue'
import DetailDrawer from './DetailDrawer.vue'
const showCreate = ref(false)
const showEdit = ref(false)
const showDetail = ref(false)
</script>
<!-- 工厂模式:配置驱动 -->
<template>
<component
v-for="(component, key) in components"
:is="ComponentConfig[key]?.component"
/>
</template>
<script setup>
// 无需 import 具体组件,工厂统一管理
</script>
3.3 观察者模式
定义 :定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
在本项目中的应用 :
js
// 子组件触发事件
const submit = async () => {
// 提交逻辑...
// 通知父组件
emit('command', {
event: 'loadTableData'
})
}
// 父组件监听事件
<component
:is="ComponentConfig[key]?.component"
@command="handleCommand"
/>
const handleCommand = (data) => {
const { event } = data
if (event === 'loadTableData') {
tablePanelRef.value.loadTableData()
}
}
观察者模式的优势 :
- 解耦 :子组件不需要知道父组件的具体实现
- 灵活性 :父组件可以选择性监听事件
- 可扩展 :新增事件类型不影响现有代码 事件流转链路 :
js
用户点击"新增商品"按钮
↓
table-panel.vue: operationHandler({ btnConfig })
↓
emit('operate', { btnConfig })
↓
schema-view.vue: onOperate({ btnConfig })
↓
EventHandlerMap['showComponent']({ btnConfig })
↓
showComponent({ btnConfig })
↓
comMapRef['createForm'].show()
↓
create-form.vue: isShow.value = true
↓
el-drawer 弹出
3.4 组合模式
定义 :将对象组合成树形结构以表示"部分-整体"的层次结构。
在本项目中的应用 :
js
// 组件组合
components = {
createForm: {
schema: { ... }, // 表单 schema
config: { ... } // 组件配置
},
editForm: {
schema: { ... },
config: { ... }
}
}
// 递归渲染
<component
v-for="(component, key) in components"
:is="ComponentConfig[key]?.component"
/>
组合模式的优势 :
- 统一处理 :单个组件和组件集合使用相同的方式处理
- 易于扩展 :可以动态添加新的组件组合
- 简化客户端代码 :不需要区分单个对象和组合对象
四、数据流设计详解
4.1 Provide/Inject 跨层级通信
问题 :如果通过 props 传递数据,需要层层传递(props drilling)
js
<!-- 传统方式:props drilling -->
<SchemaView :config="config">
<TablePanel :config="config">
<SchemaTable :config="config" />
</TablePanel>
<CreateForm :config="config" />
</SchemaView>
解决 :使用 Provide/Inject
js
// schema-view.vue - 提供数据
provide('schemaViewData', {
api, tableConfig, tableSchema,
searchSchema, searchConfig, components
})
// table-panel.vue - 注入数据
const { api, tableSchema, tableConfig } = inject('schemaViewData')
// create-form.vue - 注入数据
const { api, components } = inject('schemaViewData')
优势 :
- ✅ 避免 props drilling
- ✅ 数据源单一,便于维护
- ✅ 组件解耦,便于复用
4.2 数据流向图
js
┌─────────────────────────────────────────────────────────┐
│ model.js (配置层) │
│ schema: { product_name: { tableOptions, createFormOptions } } │
│ tableConfig: { headerButtons, rowButtons } │
│ componentConfig: { createForm: { title, saveBtnText } } │
└────────────────────┬────────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────────────┐
│ schema.js (构建层) │
│ buildDtoSchema(schema, 'table') → tableSchema │
│ buildDtoSchema(schema, 'createForm') → createFormSchema │
│ components = { createForm: { schema, config } } │
└────────────────────┬────────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────────────┐
│ schema-view.vue (调度层) │
│ provide('schemaViewData', { api, tableSchema, components }) │
│ EventHandlerMap = { showComponent, remove } │
└────────────────────┬────────────────────────────────────┘
│
┌──────────┴──────────┐
↓ ↓
┌──────────────────┐ ┌──────────────────┐
│ table-panel.vue │ │ create-form.vue │
│ inject 数据 │ │ inject 数据 │
│ 渲染表格 │ │ 渲染表单 │
│ emit('operate') │ │ emit('command') │
└──────────────────┘ └──────────────────┘
4.3 事件通信机制
事件冒泡链路
js
// 1. schema-table.vue - 表格行按钮点击
const operationHandler = ({ btnConfig, rowData }) => {
emit('operate', { btnConfig, rowData })
}
// 2. table-panel.vue - 转发事件
const operationHandler = ({ btnConfig, rowData }) => {
const { eventKey } = btnConfig
// 本地处理(如删除)
if (EventHandlerMap[eventKey]) {
EventHandlerMap[eventKey]({ btnConfig, rowData })
} else {
// 向上冒泡(如显示组件)
emit('operate', { btnConfig, rowData })
}
}
// 3. schema-view.vue - 最终处理
const onOperate = ({ btnConfig, rowData }) => {
const { eventKey } = btnConfig
if (EventHandlerMap[eventKey]) {
EventHandlerMap[eventKey]({ btnConfig, rowData })
}
}
五、实际案例:商品管理的完整实现
5.1 配置定义
js
// model.js
module.exports = {
menu: [{
key: 'product',
name: '商品管理',
schemaConfig: {
api: '/api/proj/product',
schema: {
type: 'object',
properties: {
product_id: {
type: 'string',
label: '商品ID',
tableOptions: { width: 300, 'show-overflow-tooltip': true },
editFormOptions: { comType: 'input', disabled: true }
},
product_name: {
type: 'string',
label: '商品名称',
tableOptions: { width: 200 },
searchOptions: {
comType: 'dynamicSelect',
api: '/api/proj/getProductList',
default: -1
},
createFormOptions: { comType: 'input' },
editFormOptions: { comType: 'input' }
},
product_price: {
type: 'number',
label: '商品价格',
tableOptions: { width: 200 },
createFormOptions: { comType: 'inputNumber' },
editFormOptions: { comType: 'inputNumber' }
},
product_stock: {
type: 'number',
label: '商品库存',
tableOptions: { width: 200 },
searchOptions: { comType: 'input' },
createFormOptions: {
comType: 'select',
enumList: [
{ value: 1, label: '100件' },
{ value: 2, label: '200件' }
]
}
},
create_time: {
type: 'string',
label: '创建时间',
tableOptions: {},
searchOptions: { comType: 'dateRange' }
}
},
required: ['product_name']
},
tableConfig: {
headerButtons: [{
label: '新增商品',
eventKey: 'showComponent',
eventOptions: { comName: 'createForm' },
type: 'primary',
plain: true
}],
rowButtons: [{
label: '修改商品',
eventKey: 'showComponent',
eventOptions: { comName: 'editForm' },
type: 'warning',
plain: true
}, {
label: '删除商品',
eventKey: 'remove',
type: 'danger',
plain: true,
eventOptions: {
params: { product_id: 'schema::product_id' }
}
}]
},
componentConfig: {
createForm: {
title: '新增商品',
saveBtnText: '新增商品'
},
editForm: {
mainKey: 'product_id',
title: '修改商品',
saveBtnText: '修改商品'
}
}
}
}]
}
5.2 Schema 构建过程
js
// 构建表格 schema
buildDtoSchema(schema, 'table')
// 输出:
{
type: 'object',
properties: {
product_id: {
type: 'string',
label: '商品ID',
options: { width: 300, 'show-overflow-tooltip': true }
},
product_name: {
type: 'string',
label: '商品名称',
options: { width: 200 }
},
product_price: {
type: 'number',
label: '商品价格',
options: { width: 200 }
},
product_stock: {
type: 'number',
label: '商品库存',
options: { width: 200 }
},
create_time: {
type: 'string',
label: '创建时间',
options: {}
}
}
}
// 构建搜索 schema
buildDtoSchema(schema, 'search')
// 输出:
{
type: 'object',
properties: {
product_name: {
type: 'string',
label: '商品名称',
options: {
comType: 'dynamicSelect',
api: '/api/proj/getProductList',
default: -1
}
},
product_price: {
type: 'number',
label: '商品价格',
options: {
comType: 'select',
enumList: [...]
}
},
product_stock: {
type: 'number',
label: '商品库存',
options: { comType: 'input' }
},
create_time: {
type: 'string',
label: '创建时间',
options: { comType: 'dateRange' }
}
}
}
// 构建新增表单 schema
buildDtoSchema(schema, 'createForm')
// 输出:
{
type: 'object',
properties: {
product_name: {
type: 'string',
label: '商品名称',
options: {
comType: 'input',
required: true // 从 required 数组提取
}
},
product_price: {
type: 'number',
label: '商品价格',
options: { comType: 'inputNumber' }
},
product_stock: {
type: 'number',
label: '商品库存',
options: {
comType: 'select',
enumList: [...]
}
}
}
}
5.3 事件处理流程
场景1:点击"新增商品"
js
1. 用户点击表格头部"新增商品"按钮
↓
2. table-panel.vue: operationHandler({ btnConfig })
btnConfig = {
label: '新增商品',
eventKey: 'showComponent',
eventOptions: { comName: 'createForm' }
}
↓
3. table-panel.vue: EventHandlerMap['showComponent'] 不存在
↓
4. table-panel.vue: emit('operate', { btnConfig })
↓
5. schema-view.vue: onOperate({ btnConfig })
↓
6. schema-view.vue: EventHandlerMap['showComponent']({ btnConfig })
↓
7. schema-view.vue: showComponent({ btnConfig })
comName = 'createForm'
comRef = comMapRef.value['createForm']
↓
8. schema-view.vue: comRef.show()
↓
9. create-form.vue: show()
isShow.value = true
↓
10. el-drawer 弹出
六、总结
6.1 核心思想
动态组件架构的核心思想是 配置驱动 + 事件驱动 :
- 配置驱动 :通过 componentConfig 和 schema 描述"要什么"
- 事件驱动 :通过 EventHandlerMap 和 emit 描述"做什么"
- 动态渲染 :通过 实现"怎么渲染"
6.2 设计优势
- 高度灵活 :新增功能只需添加配置,无需修改组件代码
- 易于扩展 :符合开闭原则,支持插件化扩展
- 降低耦合 :组件间通过事件和配置通信,互不依赖
- 提高复用 :通用组件通过配置适配不同业务场景
- 便于维护 :配置集中管理,逻辑清晰
6.3 适用场景
- ✅ 中后台管理系统
- ✅ CRUD 密集型应用
- ✅ 表单密集型应用
- ✅ 需要高度可配置的系统
- ❌ 不适合交互复杂、高度定制化的 C 端应用