Skip to content
本页目录

组件封装技巧

在Vue中使用容器组件 & 展示组件

什么是容器组件和展示组件 ?

在React中有很多种组件的概念, 主要是对数据逻辑UI展示进行了分离。如果你使用过 Redux 开发 React,你一定听过 容器组件(Smart/Container Components) 或 展示组件(Dumb/Presentational Components。

  1. 容器组件: 主要是对数据进行处理,组件内部拥有自己维护的状态、进行数据整理、将数据再传给其他组件(容器组件或展示组件)使用。
  2. 展示组件: 也可以叫做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>