组件封装注意事项 —— 递归组件、v-model 和 update事件 的书写顺序可能会影响结果

272 阅读4分钟

递归组件里面的数据状态不共享

递归组件里面的数据状态不共享,所以需要用 pinia 之类的东西去共享数据。

菜鸟这里想了一下,要是状态共享就完了,就真的牵一发动全身了,很难排查问题。

<script setup>
import { useProjectCenterActiveObjStore } from "@/store/modules/projectCenterActiveObj";

defineProps({
  asideData: {
    type: Array,
    default: () => []
  },
  layers: {
    type: Number,
    default: 0
  }
});

const useActiveObj = useProjectCenterActiveObjStore();
const setActiveObj = (item) => {
  useActiveObj.setActiveObj(item);
};
// 设置激活的button为普通样式,非激活的就是plain样式
const getPlain = (id) => {
  if (useActiveObj.activeObj.id === id) {
    return false;
  }
  return true;
};

const getClass = (layers) => {
  if (layers === 0) {
    return `ml-[${layers * 20}px] font-bold w-full`;
  }
  return `ml-[${layers * 20}px] w-[120px]`;
};
</script>

<template>
  <div>
    <div v-for="item in asideData" :key="item.id">
      <template v-if="item.children">
        <el-tag type="info" size="small" :class="getClass(layers)">{{ item.title }} </el-tag>
        <aside-view :aside-data="item.children" :layers="layers + 1"></aside-view>
      </template>
      <el-button
        v-else
        type="primary"
        size="small"
        :plain="getPlain(item.id)"
        :data-id="item.id"
        :class="`ml-[${layers * 20}px] w-[120px]`"
        @click="setActiveObj(item)"
        >{{ item.title }}</el-button
      >
    </div>
  </div>
</template>

<style lang="scss" scoped>
:deep(.el-tag--small) {
  height: 24px;
}
</style>

v-model 和 update事件 的书写顺序可能会影响结果

image.png

菜鸟在修改项目代码时发现,如果按照红色的写,虽然确实会触发更新函数 excute,但是请求后端使用的数据还是老的;如果用绿色的,则请求的数据是新的,这也是组件使用的一个坑!

这里和上篇文章写的:element plus 使用细节 (二) 中的 change事件里面不要访问v-model的数据 还有点差别。

这里的是vue自带的更新方法,上篇文章是element自己封装的change方法,所以上篇文章就算书写顺序是绿色的,也不行!

组件设计

这些都是菜鸟最近在工作中,深有体会的一些设计规范,可以减少后期返工,不能因为工期就不考虑设计!!!

我们公司另一位大佬说了很好的一句话:

封装不应该封装 UI功能(不仅包括样式和组件,还包括“渲染结构的描述方式”,如 template / JSX / JS 配置,这些都属于 UI 层,不应该被当作业务封装),因为组件库已经做得很好了,需要封装的是业务重复的功能,以及UI组件没有做的功能!

组件设计 - 表格列

表格最好通过 JS 配置进行统一管理,而不是在 UI 中写死。每一列应具备独立的配置(如字段、顺序、显隐等)。

原因是:写死在模板中的表格结构,本质是“不可变结构”,一旦涉及列顺序调整、权限控制、个性化配置,就会导致大量重复修改甚至重构。

这样设计的好处是:

  • 可以灵活调整列顺序,而不需要修改模板结构
  • 支持用户个性化配置(如拖拽排序、显示/隐藏列)
  • 降低后续维护成本,避免频繁改动 UI 代码
  • 更容易做持久化(如本地存储或服务端保存用户配置)

本质上是将“表格结构”从“视图层”抽离为“数据配置驱动”,提高扩展性和可维护性。

但是没有设计是完全灵活的,一个组件只要被大量引用,“牵一发动全身”是不可避免的,这是复用带来的天然代价

设计只能减少负担,并不能解决,除非你一个界面就写死一个,不进行复用,但是这样又会导致要修改的时候需要同时修改N个文件!

注意

菜鸟感觉这个是提取组件提取早了,项目未真正成型前,就应该 “一个界面就写死一个”,等后面基本不会改变后再来抽取,过早的抽取反而适得其反!

这里是菜鸟推荐的思路

  1. 前期直接 “一个界面就写死一个” js 文件渲染 table
  2. 后期基本不用改动的时候,提取公用列的 js
  3. 如果后期之后还有修改,就按照如下封装一个函数

公用列,提取成js

export function createColumns(config = {}) {
  const {
    override = {}, // 替换
    order = [], // 排序
    exclude = [], // 删除
    append = [] // 新增
  } = config

  let cols = [
    ……公用列
  ]

  // 1. 删除
  cols = cols.filter(col => !exclude.includes(col.key))

  // 2. 覆盖
  cols = cols.map(col => ({
    ...col,
    ...(override[col.key] || {})
  }))

  // 3. 添加新列
  cols = [...cols, ...append]

  // 4. 排序
  if (order.length) {
    const orderMap = new Map(order.map((k, i) => [k, i]))

    cols.sort((a, b) => {
      const aIndex = orderMap.has(a.key) ? orderMap.get(a.key) : Infinity
      const bIndex = orderMap.has(b.key) ? orderMap.get(b.key) : Infinity
      return aIndex - bIndex
    })
  }

  return cols
}

注意

其实JS也有不好的地方,就是不够直观,特别是很多提取出去之后,再进行组合,那就真的没法看!

所以最好的就是自定义列的顺序,一开始就后端一起做,要么就按照下面的进行封装!

Element plus 自定义列顺序

见文章:Element plus 自定义列顺序