关于Vue项目中组件化开发的一点学习

lxf2023-05-05 04:14:01

前言

特此说明:由于作者还是一个在学习阶段的菜鸟,所以本文主要是介绍一些基础简单的组件化开发,没有难点,都是CV大法。

在实际的项目开发中,组件是前端选手一定要学会的一个概念(或者说方法)。组件化,按官方一点的解释就是对某些需要进行复用的功能进行封装的标准化工作。不那么官方的解释就是可以重复使用的某些类似的模块。总体来说和类比较相似。

一、使用场景

那了解了组件化的概念,其实我们也就知道了为什么要去使用组件了。如果对于一个小型的系统或者学习时候的练习而言,组件化确实是没有必要的,因为这时候我们基本不会遇到需要大量重复的功能。

但是如果在实际的公司业务开发中,一个大型的管理系统就会出现非常多重复的模块,比如说表格、表单、弹窗等等,这些模块在一个系统中可能会被重复使用几十甚至上百次,那么如果不进行一个封装,我们就会产生大量的冗余代码

二、组件化开发的预先了解

前端关于组件化开发其实已经相对成熟了,在项目当中我们也会经常用到一些大厂开发和维护的大型组件库,如elementui、iview、Ant Design等。实际上,我们现在常用的自定义组件,组件化开发基本上都是基于这些大型的组件库进行二次封装和自定义。(这里说明一下,我个人的理解是,学习的时候造重复的轮子是有意义的,但是工作的时候造重复的轮子只会浪费你的精力)。

三、开始组件化开发

下面我们将进行组件化开发的一些实践,本文章主要阐述三种组件封装形式:

  1. 原生html、css自行封装一个组件
  2. 对elementui组件库的一些组件进行二次封装
  3. 基于elementui组件库已有组件自定义模块

以下开发主要在Vue2项目环境中进行开发

3.1、准备工作

在组件化开发前,我们其实是要进行一些预设计工作的,这些预设计工作包括你的组件中的页面功能设计、参数、方法以及获取不同参数的形式。

一般来说,我们都是根据页面功能预估可能需要使用的参数和方法,是否预设计实际上并不会影响你组件开发的展示效果,但是可能会影响到你的使用效果,没有事件的设计规范可能会导致你的参数出现大量的重复和冗余。

3.2、项目准备

关于Vue项目中组件化开发的一点学习

由于是学习使用,所以先初始化一个Vue2的项目,主界面就是home,组件则会放到components文件夹中。

3.3、原生html、css封装组件

细心的朋友可能看到了这里的标题是原生html、css封装组件,这里偷个懒,就不用js了,而是使用到vue中的一些语法绝对不是因为我不会

从刚才的目录就可以看出我们这次主要是进行表格和弹窗组件的封装练习,那么第一种我们就是要封装一个组件

首先在components目录下新建一个table文件夹,然后再table文件夹下新建一个myTable.vue文件

关于Vue项目中组件化开发的一点学习

vue文件中就是标志的template,script和style三大模块:

我们先看script模块中的内容

<script>
export default {
  /**
   * chen
   * 非原始js,利用vue实现
   * 参数说明
   * @titles 表头
   * @data 内容数据
   * @tableOperate 操作栏的内容
   * @showOPerate 控制操作栏的按钮是否展示
   * @border 是否有边框
   * @width 宽度 不传默认100%
   * @center 设置居中 不传默认居中
   */
  props: {
    titles: {
      type: Array,
      default: () => {
        return [];
      },
      required: true,
    },
    data: {
      type: Array,
      default: () => {
        return [];
      },
    },
    tableOperate: {
      type: Array,
      default: () => {
        return [];
      },
    },
    showOPerate: {
      type: Array,
      default: () => {
        return [];
      },
    },
    border: {
      type: Boolean,
    },
    width: {
      type: String,
      default: () => {
        return "100%";
      },
    },
    center: {
      type: Boolean,
      default: () => {
        return true;
      },
    },
  },
  data() {
    return {};
  },
  created() {},
  methods: {
    // 这里本质上是去根据父组件传过来的参数名称去调用父组件的方法
    btnFun(row, fun) {
      this.$parent[fun](row);
    },
  },
};
</script>

在组件当中最为重要的就是票prop属性,用于接收父组件传递过来的参数,在这个组件当中比较值得注意的参数就是表头、表格内容、操作栏以及是否展示操作栏。

在预设计中我们要做的其实就是要考虑这个组件应当接收哪些参数,对应的参数要实现一些什么作用

在prop中拿到参数之后我们就要开始考虑,传过来的数组、对象、布尔值要怎么去控制html标签?如果是在原生的JavaScript中,可能就需要去循环变量数组然后通过DOM操作进行节点的创建,因为这里是基于Vue的组件化开发的学习,我们就使用一些Vue的一些指令,这样难度就会大大降低。

既然使用了Vue的指令,那就不需要通过JavaScript去操作节点,而是直接在template中通过指令来进行控制

<template>
  <div>
    <table id="myTable" :width="width" :style="center ? 'margin: auto' : ''">
      <!-- 表头 -->
      <tr :class="border ? 'hasBorderTr' : 'noBorderTr'">
        <!-- 非操作栏 -->
        <td
          :class="border ? 'title' : ''"
          v-for="(item, index) in titles"
          :key="index + '1'"
        >
          {{ item.lable }}
        </td>
        <!-- 操作栏 -->
        <td :class="border ? 'title' : ''" v-if="tableOperate.length !== 0">操作</td>
      </tr>
      <!-- 表格内容 -->
      <tr
        :class="border ? 'hasBorderTr' : 'noBorderTr'"
        v-for="(item, index) in data"
        :key="index + item.prop"
      >
      <!-- 非操作栏内容 -->
        <td
          :class="border ? 'dataTd' : ''"
          v-for="(item1, index1) in titles"
          :key="index1 + 2"
        >
          {{ item[item1.prop] }}
        </td>
        <!-- 操作栏内容 -->
        <td :class="border ? 'dataTd' : ''" v-if="tableOperate.length !== 0">
          <span v-for="(item3, index3) in tableOperate" :key="item3 + index3">
            <button
              v-show="
                showOPerate.length !== 0 ? showOPerate[index][index3] : true
              "
              @click="btnFun(item, item3.fun)"
            >
              {{ item3.operate }}
            </button>
          </span>
        </td>
      </tr>
    </table>
  </div>
</template>

像这种没有考虑太多内容的表格,实现起来就非常的简单了,当然在用V-for的时候各种item可能会让组件看起来比较混乱,一旦多了之后修改维护难度直线上升,所以在循环的时候也尽量命名规范一些。

然后再加上一些样式,基本上一个简单的表格组件就封装完成了

<style scoped>
table {
  border-collapse: collapse;
}
button {
  border: 0;
  background-color: #fff;
  color: rgb(46, 143, 221);
}
button:hover {
  color: rgb(44, 119, 216);
}
.title {
  font-size: 18px;
  border-right: 1px solid #000;
  border-left: 1px solid #000;
}
.noBorderTr {
  border-bottom: 1px solid #000;
}
.hasBorderTr {
  border-top: 1px solid #000;
  border-bottom: 1px solid #000;
}
.dataTd {
  border-right: 1px solid #000;
  border-left: 1px solid #000;
}
</style>

最后我们需要在主界面(父组件)中导入并使用该组件(这里是在需要使用的父组件中单独导入,如果想要同时在很多个页面使用的话可以在全局挂载

<template>
  <div>
    <h1>主界面</h1>
    <my-table1
      :titles="tableTitle"
      :data="tableData"
      :tableOperate="tableOperate"
      :showOPerate="showOPerate"
      :border="true"
      :width="'60%'"
    ></my-table1>
  </div>
</template>

<script>
import myTable1 from "@/components/table/myTable1.vue";
export default {
  components: {
    myTable1,
  },
  data() {
    return {
      tableTitle: [
        { lable: "序号", prop: "id" },
        { lable: "姓名", prop: "name" },
        { lable: "性别", prop: "sex" },
        { lable: "年龄", prop: "age" },
        { lable: "爱好", prop: "hobby" },
      ],
      tableData: [
        { id: 1, name: "小明", sex: "男", age: 20, hobby: "唱跳" },
        { id: 2, name: "小蔡", sex: "男", age: 20, hobby: "唱跳" },
        { id: 3, name: "小徐", sex: "男", age: 20, hobby: "唱跳" },
        { id: 4, name: "小红", sex: "男", age: 20, hobby: "唱跳" },
      ],
      // 操作 这里的fun的方法名要与父组件中的方法名一致
      tableOperate: [
        { operate: "查看", fun: "check" },
        { operate: "修改", fun: "modify" },
        { operate: "删除", fun: "delete" },
      ],
      // 展示操作
      showOPerate: [
        [true, false, false],
        [true, true, true],
        [true, true, false],
        [true, false, true],
      ],
    };
  },
  methods: {
    // 查看
    check(row) {
      console.log("查看", row);
    },
    // 修改
    modify(row) {
      console.log("修改", row);
    },
    // 删除
    delete(row) {
      console.log("删除", row);
    },
  },
};
</script>
<style scoped>
</style>

页面效果如下:

关于Vue项目中组件化开发的一点学习

3.4 基于elementui组件库的二次封装

如果只是了解一下组件封装的内容,其实到3.3就已经可以结束了,而作者还要加上3.4和3.5只是为了水长度

二次封装组件库和自己用原生写有什么不同呢?大概就是站在巨人的肩膀上会显得我更高。其实实际生产上很少有人会用原生自己手搓组件,因为最卷最强那一批人以及写好了,我要是写的比他们好,那我早就替代掉他们了。

所以,能CV就CV,学习可以慢慢学,但工作不要浪费自己宝贵的生命。

虽然这些强大的组件库拥有强大的需求和开发,但是他们不可能面面俱到甲方的需求,所以,基于组件库进行二次开发就有了必要性

接下来我们就要对elementui的表格进行二次封装,规则很简单,什么都没有,只有一个接口,但是接口已经规定好了返回表头和表格内容,所以要根据给的请求地址渲染出一个表格。

下面主要使用到axios和json-server进行模拟请求(如何封装axios以及使用json-serveer请自行搜索)

由于需要用到请求,所以添加了一些配置,目录结构如下

关于Vue项目中组件化开发的一点学习

首先我们需要造一点假数据

{
    "data": {
        "tltle": [
            { "label" : "序号", "prop": "id"},
            { "label" : "用户名", "prop": "username"},
            { "label" : "性别", "prop": "age"},
            { "label" : "年龄", "prop": "sex"},
            { "label" : "爱好", "prop": "hobby"}
        ],
        "result": [
            {
                "id": 1,
                "username": "name1",
                "age": 20,
                "sex": "男",
                "hobby": "唱跳"
            },
            {
                "id": 2,
                "username": "name2",
                "age": 20,
                "sex": "男",
                "hobby": "唱跳"
            },
            {
                "id": 3,
                "username": "name3",
                "age": 20,
                "sex": "男",
                "hobby": "唱跳"
            },
            {
                "id": 4,
                "username": "name4",
                "age": 20,
                "sex": "男",
                "hobby": "唱跳"
            },
            {
                "id": 25,
                "username": "name5",
                "age": 20,
                "sex": "男",
                "hobby": "唱跳"
            }
        ]
    },
    "meta": {
        "msg": "获取成功",
        "status": 200
    }
}

然后关于页面,就复制粘贴一下elementui上面的就好了,因为考虑到一些操作以及状态回显的问题,主要的解决思路就是在需要的地方加上一些插槽就好了

<template>
  <div>
    <el-table :data="tableData.result" style="width: 100%">
      <template slot="name"> </template>
      <el-table-column
        v-for="(item, index) in tableData.tltle"
        :key="item.prop + index"
        :prop="item.prop"
        :label="item.label"
      >
      </el-table-column>
    </el-table>
  </div>
</template>
<script>
import { getTableList } from "../../http/api";
export default {
  props: {
    url: {
      type: String,
    },
  },
  data() {
    return {
      tableData: {},
    };
  },
  created() {
    this.getTable();
  },
  methods: {
    getTable() {
      getTableList(this.url)
        .then((res) => {
          this.tableData = res;
        })
        .catch((err) => {
          console.log(err);
        });
    },
  },
};
</script>

这个的思路就是父组件将接口地址作为参数传递给子组件,子组件获取到参数之后将url作为参数传递给封装好的请求方法,然后通过请求获取到对用的表格数据。

这种做法要基于项目有了较为完善的接口规范,并且不会轻易更改,否则很难封装起来进行统一处理,这里其实是较为简单的一种思路介绍,像搜索请求以及插槽还有比较多的学问,这里还没学到位,就不多加赘述了。

3.4、基于elementui组件库已有组件自定义模块

可能有同学看到这里会觉得有点奇怪,3.3和3.4有什么区别吗,在我的理解上,3.3是对单个组件的二次封装,在保持原有功能不变的情况下去实现这个组件目前还没有的功能(所以3.3其实不完整,只是一个思路),3.4的话则是使用多个组件实现一个模块的封装,这种模块的封装使用场景比较多的就是弹窗。

假如我们一个表单中,可能会出现新增、查看、修改、审核、复核等等操作,我们就会发现,这些操作展示的弹窗信息基本一致,可能只是出现了个别字段或者是否禁用而已,那么这时候将弹窗封装起来就可以减少冗余重复的代码,也便于我们之后的更改

在基于3.2的表格实现一个弹窗的查看、修改和审核的弹窗封装

<template>
  <div>
    <el-form :model="form">
      <el-form-item label="序号" :label-width="formLabelWidth">
        <el-input
          :disabled="isCheck || isAudit"
          v-model="form.id"
          autocomplete="off"
        ></el-input>
      </el-form-item>
      <el-form-item label="姓名" :label-width="formLabelWidth">
        <el-input
          :disabled="isCheck || isAudit"
          v-model="form.name"
          autocomplete="off"
        ></el-input>
      </el-form-item>
      <el-form-item label="性别" :label-width="formLabelWidth">
        <el-select
          :disabled="isCheck || isAudit"
          v-model="form.sex"
          placeholder="请选择"
        >
          <el-option label="男" value="男"></el-option>
          <el-option label="女" value="女"></el-option>
        </el-select>
      </el-form-item>
      <el-form-item label="年龄" :label-width="formLabelWidth">
        <el-input
          :disabled="isCheck || isAudit"
          v-model="form.age"
          autocomplete="off"
        ></el-input>
      </el-form-item>
      <el-form-item label="姓名" :label-width="formLabelWidth">
        <el-input
          :disabled="isCheck || isAudit"
          v-model="form.hobby"
          autocomplete="off"
        ></el-input>
      </el-form-item>
      <el-form-item
        v-if="isAudit"
        label="是否通过"
        :label-width="formLabelWidth"
      >
        <el-select v-model="form.audit" placeholder="请选择">
          <el-option label="通过" value="1"></el-option>
          <el-option label="不通过" value="0"></el-option>
        </el-select>
      </el-form-item>
      <el-form-item
        v-if="isAudit"
        label="审核意见"
        :label-width="formLabelWidth"
      >
        <el-input v-model="form.opinion" autocomplete="off"></el-input>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
export default {
  /**
   * chen
   * 参数说明
   * @form 表单数据
   * @isCheck 当前弹窗是否为查看弹窗‘
   * @isModify 当前弹窗是否为修改弹窗
   * @isAudit 当前弹窗是否为审核弹窗
   * @说明
   * 这几个判断弹窗其实可以规划为一个属性例如:
   * dialogType:{type:String},
   * 然后传入的值就可以为check、modify、audit等等
   * 弹窗较多的时候建议采用这个
   */
  props: {
    form: {
      type: Object,
      default: () => {
        return {};
      },
    },
    isCheck: {
      type: Boolean,
    },
    isModify: {
      type: Boolean,
    },
    isAudit: {
      type: Boolean,
    },
  },
  created() {},
  data() {
    return {
      formLabelWidth: "120px",
    };
  },
};
</script>

<style scoped>
</style>

在父组件中调用

<template>
  <div>
    <h1>主界面</h1>
    <my-table1
      :titles="tableTitle"
      :data="tableData"
      :tableOperate="tableOperate"
      :showOPerate="showOPerate"
      :border="true"
      :width="'60%'"
    ></my-table1>
    <!-- 弹窗 -->
    <el-dialog
      @close="dialogClose"
      title="查看"
      center
      :visible.sync="checkDialogVisible"
    >
      <my-dialog :form="form" :isCheck="true"></my-dialog>
      <div slot="footer" class="dialog-footer">
        <el-button @click="checkDialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="checkDialogVisible = false"
          >确 定</el-button
        >
      </div>
    </el-dialog>
    <!-- 修改 -->
    <el-dialog
      @close="dialogClose"
      title="修改"
      center
      :visible.sync="modifyDialogVisible"
    >
      <my-dialog :form="form" :isModify="true"></my-dialog>
      <div slot="footer" class="dialog-footer">
        <el-button @click="modifyDialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="modifyDialogVisible = false"
          >确 定</el-button
        >
      </div>
    </el-dialog>
    <!-- 审批 -->
    <el-dialog
      @close="dialogClose"
      title="审批"
      center
      :visible.sync="auditDialogVisible"
    >
      <my-dialog :form="form" :isAudit="true"></my-dialog>
      <div slot="footer" class="dialog-footer">
        <el-button @click="auditDialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="auditDialogVisible = false"
          >确 定</el-button
        >
      </div>
    </el-dialog>
  </div>
</template>

<script>
import myTable1 from "@/components/table/myTable1.vue";
import myTable2 from "@/components/table/myTable2.vue";
import myDialog from "@/components/dialog/myDialog.vue";
export default {
  components: {
    myTable1,
    myTable2,
    myDialog,
  },
  data() {
    return {
      checkDialogVisible: false,
      modifyDialogVisible: false,
      auditDialogVisible: false,
      form: {},
      tableTitle: [
        { lable: "序号", prop: "id" },
        { lable: "姓名", prop: "name" },
        { lable: "性别", prop: "sex" },
        { lable: "年龄", prop: "age" },
        { lable: "爱好", prop: "hobby" },
      ],
      tableData: [
        { id: 1, name: "小明", sex: "男", age: 20, hobby: "唱跳" },
        { id: 2, name: "小蔡", sex: "男", age: 20, hobby: "唱跳" },
        { id: 3, name: "小徐", sex: "男", age: 20, hobby: "唱跳" },
        { id: 4, name: "小红", sex: "男", age: 20, hobby: "唱跳" },
      ],
      // 操作 这里的fun的方法名要与父组件中的方法名一致
      tableOperate: [
        { operate: "查看", fun: "check" },
        { operate: "修改", fun: "modify" },
        { operate: "审批", fun: "audit" },
      ],
      // 展示操作
      showOPerate: [
        [true, false, false],
        [true, true, true],
        [true, true, false],
        [true, false, true],
      ],
    };
  },
  methods: {
    // 查看
    check(row) {
      this.form = row;
      this.checkDialogVisible = true;
    },
    // 修改
    modify(row) {
      this.form = row;
      this.modifyDialogVisible = true;
    },
    // 审批
    audit(row) {
      this.form = row;
      (this.form.audit = ""), (this.form.opinion = "");
      this.auditDialogVisible = true;
    },
    // 关闭弹窗是置空表单
    dialogClose() {
      this.form = {};
    },
  },
};
</script>
<style scoped>
</style>

四、总结

总的来说,组件的开发封装是前期需要耗费大量时间,但是维护和修改相对较方便的一种方案,但是组件的开发一定要提前做好规范和计划,要不然大量的参数可能就会让这个组件变得不那么容易维护,关于是二次封装还是自定义,实际上是根据需求来确定的,而组件化不仅仅是一种方法,更多的时候是一种思维,需要慢慢去锻炼。