Vue组件封装实战:那些年我造过的轮子

344 阅读3分钟

大家好,我是小杨,一个有着6年前端开发经验的老司机。今天想和大家聊聊我在Vue项目中最爱干的事——封装组件。就像乐高积木一样,好的组件能让开发效率翻倍,项目结构更清晰。下面我就分享几个我实际项目中封装的组件,以及背后的设计思路。

1. 智能表单生成器:告别重复劳动

在后台管理系统开发中,表单占据了70%的工作量。每次都要写一堆el-form-item,于是我决定封装一个智能表单生成器。

// FormGenerator.vue
export default {
  props: {
    formConfig: {
      type: Array,
      required: true
    },
    formData: {
      type: Object,
      required: true
    }
  },
  render(h) {
    return h('el-form', [
      this.formConfig.map(item => {
        return h('el-form-item', {
          props: {
            label: item.label,
            prop: item.prop
          }
        }, [
          this.renderFormItem(h, item)
        ])
      })
    ])
  },
  methods: {
    renderFormItem(h, item) {
      const { type, options, placeholder } = item
      const props = {
        value: this.formData[item.prop],
        placeholder: placeholder || `请输入${item.label}`,
        on: {
          input: val => {
            我.$set(this.formData, item.prop, val)
          }
        }
      }
      
      switch(type) {
        case 'input':
          return h('el-input', { props })
        case 'select':
          return h('el-select', { props }, [
            options.map(opt => h('el-option', {
              props: {
                label: opt.label,
                value: opt.value
              }
            }))
          ])
        // 其他类型...
      }
    }
  }
}

使用起来超级简单:

<template>
  <form-generator 
    :form-config="formConfig" 
    :form-data="formData" 
  />
</template>

<script>
export default {
  data() {
    return {
      formData: { name: '', gender: '' },
      formConfig: [
        {
          label: '姓名',
          prop: 'name',
          type: 'input'
        },
        {
          label: '性别',
          prop: 'gender',
          type: 'select',
          options: [
            { label: '男', value: 'male' },
            { label: '女', value: 'female' }
          ]
        }
      ]
    }
  }
}
</script>

设计亮点:

  • 配置化驱动,减少模板代码
  • 支持动态扩展表单类型
  • 自动双向绑定,无需手动写v-model
  • 内置常用表单校验规则

2. 动态表格组件:数据展示的瑞士军刀

后台管理系统总少不了各种表格展示,我封装了一个支持动态列、排序、分页的超级表格组件。

// SmartTable.vue
export default {
  props: {
    columns: {
      type: Array,
      required: true
    },
    data: {
      type: Array,
      required: true
    },
    pagination: {
      type: Object,
      default: () => ({
        currentPage: 1,
        pageSize: 10,
        total: 0
      })
    }
  },
  methods: {
    handleSortChange({ prop, order }) {
      this.$emit('sort-change', { prop, order })
    },
    handlePageChange(page) {
      this.$emit('page-change', page)
    },
    renderHeader(h, column) {
      return column.label
    },
    renderCell(h, scope, column) {
      const { prop, formatter } = column
      const cellValue = scope.row[prop]
      
      if (formatter) {
        return formatter(cellValue, scope.row)
      }
      
      if (column.slot) {
        return this.$scopedSlots[column.slot](scope)
      }
      
      return cellValue
    }
  },
  render(h) {
    return h('div', [
      h('el-table', {
        props: {
          data: this.data,
          border: true
        },
        on: {
          'sort-change': this.handleSortChange
        }
      }, [
        this.columns.map(column => {
          return h('el-table-column', {
            props: {
              label: column.label,
              prop: column.prop,
              sortable: column.sortable,
              width: column.width
            },
            scopedSlots: {
              header: scope => this.renderHeader(h, column),
              default: scope => this.renderCell(h, scope, column)
            }
          })
        })
      ]),
      h('el-pagination', {
        props: {
          currentPage: this.pagination.currentPage,
          pageSize: this.pagination.pageSize,
          total: this.pagination.total,
          layout: 'total, sizes, prev, pager, next, jumper'
        },
        on: {
          'current-change': this.handlePageChange,
          'size-change': this.handlePageChange
        }
      })
    ])
  }
}

使用示例:

<template>
  <smart-table
    :columns="columns"
    :data="tableData"
    :pagination="pagination"
    @page-change="handlePageChange"
    @sort-change="handleSortChange"
  >
    <template #status="{ row }">
      <el-tag :type="row.status | statusType">
        {{ row.status | statusText }}
      </el-tag>
    </template>
  </smart-table>
</template>

为什么这么设计?

  • 列配置与数据解耦,灵活性高
  • 支持自定义渲染和插槽
  • 内置分页和排序逻辑
  • 减少重复的表格样式代码

3. 可拖拽的树形菜单:让交互更友好

在做一个CMS系统时,需要实现菜单的拖拽排序功能,于是有了这个组件。

// DraggableTree.vue
import { Tree, MessageBox } from 'element-ui'

export default {
  extends: Tree,
  methods: {
    handleDrop(draggingNode, dropNode, dropType) {
      try {
        const draggingData = draggingNode.data
        const dropData = dropNode.data
        
        // 验证是否允许放置
        if (dropType === 'inner' && !dropData.isFolder) {
          MessageBox.warning('只能拖拽到文件夹内')
          return
        }
        
        // 触发父组件事件
        this.$emit('node-drop', {
          draggingNode,
          dropNode,
          dropType
        })
        
        // 调用原始Tree的drop方法
        this.$options.extends.methods.handleDrop.call(
          this, 
          draggingNode, 
          dropNode, 
          dropType
        )
      } catch (error) {
        console.error('拖拽失败:', error)
        MessageBox.error('拖拽操作失败')
      }
    }
  }
}

增强功能:

  • 继承原生el-tree所有功能
  • 添加拖拽验证逻辑
  • 友好的错误提示
  • 保持原始API不变,易于迁移

4. 图片上传预览组件:一站式解决方案

图片上传是高频需求,我封装了一个带预览、裁剪、压缩的增强版上传组件。

// AdvancedUpload.vue
export default {
  data() {
    return {
      dialogVisible: false,
      cropImg: '',
      fileList: []
    }
  },
  methods: {
    beforeUpload(file) {
      const isImage = file.type.includes('image/')
      const isLt5M = file.size / 1024 / 1024 < 5
      
      if (!isImage) {
        this.$message.error('只能上传图片!')
        return false
      }
      
      if (!isLt5M) {
        this.$message.error('图片大小不能超过5MB!')
        return false
      }
      
      return new Promise(resolve => {
        const reader = new FileReader()
        reader.readAsDataURL(file)
        reader.onload = event => {
          this.cropImg = event.target.result
          this.dialogVisible = true
          resolve(false) // 暂停默认上传
        }
      })
    },
    handleCrop() {
      // 获取裁剪后的图片
      this.$refs.cropper.getCroppedCanvas().toBlob(blob => {
        const croppedFile = new File([blob], 'cropped_' + Date.now() + '.jpg', {
          type: 'image/jpeg'
        })
        
        // 手动添加到文件列表
        this.fileList.push({
          uid: Date.now(),
          name: croppedFile.name,
          size: croppedFile.size,
          raw: croppedFile
        })
        
        this.dialogVisible = false
      }, 'image/jpeg', 0.8) // 80%质量压缩
    }
  }
}

功能特色:

  • 内置图片类型和大小校验
  • 集成图片裁剪功能
  • 自动压缩图片质量
  • 保持与el-upload一致的API

组件封装的心得体会

经过这些年封装组件的经验,我总结了几个原则:

  1. 单一职责原则:一个组件只做一件事,做好一件事
  2. 开放封闭原则:对扩展开放,对修改封闭
  3. 约定优于配置:提供合理的默认值,减少必须的配置项
  4. 保持一致性:API设计遵循项目或框架的约定
  5. 文档和示例:好的组件必须配有好的文档和示例

记住,不要为了封装而封装。当发现某段代码被复制粘贴超过3次,或者一个组件超过500行代码时,就该考虑拆分了。

最后

组件封装是前端工程化的重要部分,好的组件就像乐高积木,能让团队开发效率倍增。希望我的这些经验对你有帮助。如果你有更好的组件封装思路,欢迎在评论区交流!

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!