容器组件和展示组件的使用

889 阅读3分钟

组件化的思想在开发中需要充分的利用,项目开发时,尽可能的将页面拆分成一个个小的、可重复利用的组件,这样可以降低代码耦合度,并且在阅读性和扩展性上会得到一定的提升。

在react中有很多种组件的概念,主要是对数据逻辑和UI展示进行了分离,其中包含了这两种组件:容器组件(Container Component)和展示组件(Presentational Component)。

  • 容器组件主要是对数据进行处理,组件内部拥有自己维护的状态、进行数据整理、将数据再传给其他组件(容器组件或展示组件)使用。
  • 展示组件也可以叫做UI组件,它的特点就是负责渲染,组件内部不进行数据的更新,只是将接收的数据渲染到视图进行展示。

现在我们大概了解了两种类型组件的概念,不难看出这种思想的优势,将逻辑与视图分离开,让不同类型的组件负责特定的业务以完成一项功能的开发,可以使代码逻辑变得清晰,降低代码的耦合度,如果后期需要进行扩展也可以快速的理解应用程序。

我们以中后台管理系统中表单组件为例,表单的使用在管理系统中是必不可少的,当管理一条数据时,既有新增的需求也有编辑的需求,表单的填写一般是在点击按钮之后的弹窗中进行的。

Snipaste_2023-03-27_22-50-52.png

上图为管理数据所需要使用的新增和编辑的表单界面,这部分的代码应该怎样书写呢?

对于该业务我见过很多种编写方式,有直接将一个页面的代码放在一个文件中的,包含数据搜索、表格、分页器以及弹窗中的表单代码,这种方式造成的后果就是一个文件包含所有的功能代码,逻辑较为混乱;也有将弹窗抽离出单独的文件作为组件使用的,这种方式较第一种方式好处就是可以降低代码耦合度,使得组件代码逻辑清晰一些,但是这种方式也有一定的问题,如果将新增功能和编辑功能放在一个组件中,那么在进行不同的业务编写时需要进行if判断;如果是将新增和编辑放在不同的组件分开编写,虽可以做到不同的功能独立开发,但是dom结构的编写是重复的,因此对于以上这些问题,就可以使用容器组件和展示组件来编写代码。这里以Vue3 + NaiveUI进行代码编写

文件结构

cpns               
├─ AddDialog.vue   
├─ EditDialog.vue  
└─ FormUI.vue      

AddDialog.vue

<script setup lang="ts">
import { ref, reactive } from "vue"

import FormUI from "./formUI.vue"

import type { IForm } from "../types"
import { add } from "@/service/iot"

let modelShow = ref(false)
const form: IForm = reactive({
  name: "",
  age: "",
  phoneNumber: "",
  remark: "",
})

const init = () => {
  modelShow.value = true
}
const handleSubmit = async () => {
  //网络请求
  await add(form)
  window.$message.success("新增成功")
  handleClose()
}
const handleClose = () => {
  modelShow.value = false
  form.name = ""
  form.age = ""
  form.phoneNumber = ""
  form.remark = ""
}

defineExpose({
  init,
})
</script>

<template>
  <FormUI
    title="新增"
    v-model:modelShow="modelShow"
    v-model:form="form"
    @handleClose="handleClose"
    @handleSubmit="handleSubmit"
  />
</template>

editDialog.vue

<script setup lang="ts">
import { ref, reactive } from "vue"

import FormUI from "./formUI.vue"

import type { IForm } from "../types"
import { getById, edit } from "@/service/iot"

let modelShow = ref(false)
const form: IForm = reactive({
  name: "",
  age: "",
  phoneNumber: "",
  remark: "",
})

const init = async (id: number) => {
  const info = await getById(id)
  const { name, age, phoneNumber, remark } = info
  form.name = name
  form.age = age
  form.phoneNumber = phoneNumber
  form.remark = remark

  modelShow.value = true
}
const handleSubmit = async () => {
  //网络请求
  await edit(form)
  window.$message.success("编辑成功")
  handleClose()
}
const handleClose = () => {
  modelShow.value = false
  form.name = ""
  form.age = ""
  form.phoneNumber = ""
  form.remark = ""
}

defineExpose({
  init,
})
</script>

<template>
  <FormUI
    title="编辑"
    v-model:modelShow="modelShow"
    v-model:form="form"
    @handleClose="handleClose"
    @handleSubmit="handleSubmit"
  />
</template>

FormUI.vue

<script setup lang="ts">
import { ref, computed } from "vue"
import { FormInst, FormRules, FormItemRule } from "naive-ui"
import type { IForm } from "../types"

const props = defineProps<{
  title: string
  form: IForm
  modelShow: boolean
}>()

const formRef = ref<FormInst | null>(null)

const emit = defineEmits([
  "update:modelShow",
  "update:form",
  "handleClose",
  "handleSubmit",
])
const modelShowComputed = computed({
  set(value: boolean) {
    emit("update:modelShow", value)
  },

  get(): boolean {
    return props.modelShow
  },
})
const formComputed = computed({
  set(value: IForm) {
    emit("update:form", value)
  },

  get(): IForm {
    return props.form
  },
})

const bodyStyle = {
  ...
}
const rules: FormRules = {
  ...
}

const handleSubmit = () => {
  formRef.value?.validate((errors) => {
    if (!errors) {
      emit("handleSubmit")
    } else {
      console.log(errors)
    }
  })
}
</script>

<template>
  <n-modal
    :title="title"
    v-model:show="modelShowComputed"
    transform-origin="center"
    :mask-closable="false"
    :style="bodyStyle"
    preset="card"
    size="medium"
    :bordered="false"
  >
    <n-form ref="formRef" :model="formComputed" :rules="rules">
      <n-form-item path="name" label="姓名">
        <n-input v-model:value="form.name" placeholder="请输入姓名" />
      </n-form-item>
      <n-form-item path="age" label="年龄">
        <n-input v-model:value="form.age" placeholder="请输入年龄" />
      </n-form-item>
      <n-form-item path="phoneNumber" label="联系方式">
        <n-input v-model:value="form.phoneNumber" placeholder="请输入手机号" />
      </n-form-item>
      <n-form-item path="remark" label="备注">
        <n-input
          type="textarea"
          v-model:value="form.remark"
          placeholder="请输入备注"
        />
      </n-form-item>
    </n-form>
    <template #footer>
      <n-space>
        <n-button @click="handleSubmit" strong secondary type="info">
          确认
        </n-button>
        <n-button @click="emit('handleClose')" strong secondary type="tertiary">
          取消
        </n-button>
      </n-space>
    </template>
  </n-modal>
</template>

使用容器组件和展示组件来封装这个表单,实际会出现三个文件,新增和编辑业务处理的组件,和一个UI展示的组件,虽说文件数量变多,但是对于业务本身来说,dom结构只编写了一份,重用性加强;业务逻辑分开编写,耦合度降低,这就是展示型组件和容器型组件的使用方式。

有人会说对于上面的表单,直接将新增和编辑在一个组件中对不同业务判断做不同的处理也很方便,确实,像这种表单项不多并且逻辑较为简单的表单这么做没有问题,但是如果遇到表单项很多,并且表单项会根据数据做不同的显示,随着业务逻辑的复杂度增加,还是逻辑处理与UI渲染分开更为合理。

学会容器组件和展示组件的思想之后,对于我们的编码风格及处理业务的能力都会有一定程度的提升!