组件封装技巧
在Vue中使用容器组件 & 展示组件
什么是容器组件和展示组件 ?
在React中有很多种
组件
的概念, 主要是对数据逻辑
和UI展示
进行了分离
。如果你使用过Redux
开发React
,你一定听过容器组件
(Smart/Container Components) 或展示组件
(Dumb/Presentational Components。
- 容器组件: 主要是对数据进行处理,组件内部拥有自己维护的状态、进行数据整理、将数据再传给其他组件(容器组件或展示组件)使用。
- 展示组件: 也可以叫做
UI组件
,它的特点就是负责渲染,组件内部不进行数据的更新,只是将接收的数据渲染到视图进行展示。
容器组件 和 展示组件 的区别
展示组件 | 容器组件 | |
---|---|---|
作用 | 只负责骨架、UI样式 | 不提供样式,只负责逻辑(数据获取、状态更新、提交请求) |
直接使用 store | 否 | 是 |
数据来源 | props | 异步获取、监听 store state |
数据修改 | 从 props 调用回调函数 | 向后端发送请求、向 store 派发 actions |
我们大概了解了两种类型组件的概念,不难看出这种思想的优势,将逻辑与视图分离开,让不同类型的组件负责特定的业务以完成一项功能的开发,可以使代码逻辑变得清晰
,降低代码的耦合度
,方便后续维护。
使用示例
我们以中后台管理系统中Form
组件为例,表单的使用在管理系统中是必不可少的,当管理一条数据时,既有新增的需求也有编辑的需求,表单的填写一般是在点击按钮之后的弹窗中进行的。
1. 简易版本
文件结构
shell
components
├─ AddProduct.vue # 容器组件-表单新增
├─ EditProduct.vue # 容器组件-表单编辑
├─ FormUiData.ts # 用于管理 props 和 type
└─ UiForm.vue # UI组件
vue
<script setup lang="ts">
import { ElInputNumber } from 'element-plus'
import { useVModel } from '@vueuse/core'
import { formProps, rules, services } from './FormUIData'
const props = defineProps(formProps)
const emit = defineEmits(['finish'])
const refForm = ref<HTMLElement>()
const handleSubmit = async (form) => {
try {
await form.validate()
emit('finish', refForm.value)
}
catch (err) {
console.log(err)
}
}
const form = useVModel(props, 'formData', emit)
</script>
<template>
<div class="py-6 px-4 border border-dashed border-blue-500 rounded-lg ">
<ElForm
ref="refForm"
v-loading="loading"
:model="form"
:rules="rules"
status-icon
label-width="80px"
@submit.prevent="handleSubmit(refForm)"
>
<ElFormItem label="商品名称" prop="name">
<ElInput v-model="form.name" placeholder="请输入商品名称" />
</ElFormItem>
<ElFormItem label="价格" prop="price">
<ElInputNumber v-model="form.price" />
</ElFormItem>
<ElFormItem label="商品服务">
<ElCheckboxGroup v-model="form.services">
<ElCheckbox
v-for="(s, idx) in services"
:key="idx"
:label="s"
>
{{ s }}
</ElCheckbox>
</ElCheckboxGroup>
</ElFormItem>
<ElFormItem>
<ElButton native-type="submit" type="primary">
{{ submitText }}
</ElButton>
</ElFormItem>
</ElForm>
</div>
</template>
ts
import type { PropType } from 'vue'
export interface TFormData {
name: string
price: number
services: string[]
}
export const services = ['7天无理由', '30天保价', '2年只换不修']
export const formProps = {
formData: {
type: Object as PropType<TFormData>,
required: true,
},
submitText: {
type: String,
default: '提交',
},
loading: {
type: Boolean,
default: false,
},
} as const
export const rules = {
name: [
{ required: true, message: '请输入商品名称', trigger: 'blur' },
],
price: [
{ required: true, message: '请输入价格', trigger: 'blur' },
],
}
vue
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import ProductForm from './UiForm.vue'
const formData = reactive({
name: '',
price: 0,
services: [],
})
const loading = ref(false)
const finish = () => {
loading.value = true
setTimeout(() => {
ElMessage.success({ message: '添加商品成功' })
loading.value = false
}, 2000)
}
</script>
<template>
<ProductForm
:form-data="formData"
submit-text="新增商品"
:loading="loading"
@finish="finish"
/>
</template>
vue
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import ProductForm from './UiForm.vue'
let formData = reactive<any>({
name: '',
price: 0,
services: [],
})
const loading = ref(true)
onMounted(() => {
setTimeout(() => {
formData = reactive({
name: 'Huawei Mata 60 +',
price: 99999,
services: ['7天无理由', '30天保价'],
})
loading.value = false
}, 600)
})
const finish = () => {
loading.value = true
setTimeout(() => {
ElMessage.success({ message: '修改商品成功' })
loading.value = false
}, 2000)
}
</script>
<template>
<ProductForm
:form-data="formData"
submit-text="提交修改"
:loading="loading"
@finish="finish"
/>
</template>
2. 完整版本
文件结构
shell
components
├─ AddDialog.vue
├─ EditDialog.vue
├─ FormUiData2.ts
└─ UiForm2.vue
vue
<script setup lang="ts">
import type { FormInstance } from 'element-plus'
import { useVModel } from '@vueuse/core'
import { formProps, rules } from './FormUIData2'
const props = defineProps(formProps)
const emit = defineEmits([
'update:open',
'update:loading',
'update:formData',
'submit',
])
const refForm = ref<FormInstance>()
const close = () => {
if (!refForm.value) return
refForm.value.resetFields()
emit('update:open', false)
}
const show = useVModel(props, 'open', emit)
const _loading = useVModel(props, 'loading', emit)
const form = useVModel(props, 'formData', emit)
const handleSubmit = () => {
refForm.value?.validate((errors) => {
if (!errors) emit('submit')
else console.log(errors)
})
}
</script>
<template>
<el-dialog
v-model="show" :title="title"
:destroy-on-close="true"
@close="close"
@open="open"
>
<el-form
ref="refForm"
v-loading="_loading"
:model="form"
:rules="rules"
status-icon
label-width="80px"
>
<el-form-item prop="name" label="姓名">
<el-input v-model="form.name" placeholder="请输入姓名" />
</el-form-item>
<el-form-item prop="age" label="年龄">
<el-input v-model="form.age" placeholder="请输入年龄" />
</el-form-item>
<el-form-item prop="phoneNumber" label="联系方式">
<el-input v-model="form.phoneNumber" placeholder="请输入手机号" />
</el-form-item>
<el-form-item prop="remark" label="备注">
<el-input
v-model="form.remark"
type="textarea"
placeholder="请输入备注"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="close">取消</el-button>
<el-button type="primary" @click="handleSubmit">
{{ submitText }}
</el-button>
</span>
</template>
</el-dialog>
</template>
ts
import type { PropType } from 'vue'
import type { FormRules } from 'element-plus'
export interface IForm {
name: string
age: string | number
phoneNumber: string
remark: string
}
export const defaultFromData = {
name: '',
age: '',
phoneNumber: '',
remark: '',
}
export const formProps = {
formData: {
type: Object as PropType<IForm>,
required: true,
},
submitText: {
type: String,
default: '提交',
},
title: {
type: String,
default: '新增',
},
loading: {
type: Boolean,
default: false,
},
open: {
type: Boolean,
default: false,
},
} as const
export const rules: FormRules = {
name: [
{ required: true, message: '请输入姓名', trigger: 'blur' },
],
age: [
{ required: true, message: '请输入年龄', trigger: 'blur' },
],
phoneNumber: [
{ required: true, message: '请输入电话号码', trigger: 'blur' },
],
remark: [{ required: false }],
}
vue
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import ProductForm from './UiForm.vue'
const formData = reactive({
name: '',
price: 0,
services: [],
})
const loading = ref(false)
const finish = () => {
loading.value = true
setTimeout(() => {
ElMessage.success({ message: '添加商品成功' })
loading.value = false
}, 2000)
}
</script>
<template>
<ProductForm
:form-data="formData"
submit-text="新增商品"
:loading="loading"
@finish="finish"
/>
</template>
vue
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { type IForm } from './FormUIData2'
import FormUI from './UiForm2.vue'
const es = {
name: '',
age: '',
phoneNumber: '',
remark: '',
}
const open = ref(false)
const loading = ref(true)
const formData = ref<IForm>(es)
const init = async (id: string) => {
console.log('-----EditDialog.vue------ init() 触发了 -------\n')
open.value = true
loading.value = true
setTimeout(() => {
formData.value = {
name: 'LiaoYi',
age: '25',
phoneNumber: '13246566775',
remark: 'this is My First ...',
}
loading.value = false
}, 1200)
}
const submit = async () => {
loading.value = true
setTimeout(() => {
ElMessage.success({ message: '编辑成功' })
loading.value = false
formData.value = reactive(es)
}, 2000)
}
defineExpose({ init })
</script>
<template>
<FormUI
v-model:open="open"
:form-data="formData"
:loading="loading"
title="编辑"
@submit="submit"
/>
</template>