前端组件库造轮子——Tree组件开发教程

前言

本系列旨在记录前端组件库开发经验,我们的组件库项目目前已在Github开源,下面是项目的部分组件。文章会详细介绍一些造组件库轮子的技巧并且最后会给出完整的演示demo。

image.png

文章旨在总结经验,开源分享,有问题的话也希望路过的大佬指正。

组件开发流程

组件递归

Tree组件算是比较有难度的组件了,其核心功能其实就是实现树一样的联级结构。其实实现就是组件递归。

我们来复习一下递归代码

我们的递归代码实现,必然是由一个函数和调用函数组成的。同理,要实现组件递归也需要做类似的操作。

function dfs() {
    ...
}

function Main() {
    dfs()
}

在组件递归中,我们就需要类比递归函数的操作,我们需要用一个组件node来作为递归组件,这个组件起到主要渲染的作用,并且需要一个tree组件,来调用组件执行。

🆗,现在知道了大致思路,我们在补充一下如何编写组件。

对于递归函数,很重要的一点,我们如何让他不断递归同时让他停下来。

我们可以利用props把参数传进去,然后在渲染的时候去判断有没有孩子,如果没有孩子就不渲染,这个可以用v-if来完成。

// node 组件中
<div v-if="isRender" v-show="items.isOpen">
    <node
      v-for="(child, index) in items.children"
      :key="index"
      :items="child"
      :label="label"
      :children="children"></node>
</div>

// 判断是否要渲染
const isRender = computed(() => {
  return (
    props.items.children && props.items.children.length
  );
});

那这样我们就可以实现node组件的正确递归,所以我们只需在tree组件中在调用一次node组件就可以了。

<div class="tree">
    <node
      v-for="(item, index) in copyData"
      :key="index"
      :items="item"
      :label="label"
      :children="children"></node>
  </div>

深拷贝和初始化

还没完,我们需要对传进来的数据做一些深拷贝和初始化。

为什么要深拷贝应该知道吧?vueprops是单向数据流,我们是不能直接修改的,因此我们需要深拷贝一份来操作。

const deepCopy = (target: any, hash_table = new WeakMap()) => {
  if (typeof target === "object") {
    let clone = Array.isArray(target) ? [] : {};
    if (hash_table.get(target)) return hash_table.get(target);
    hash_table.set(target, clone);
    for (const key in target) {
      clone[key] = deepCopy(target[key], hash_table);
    }
    return clone;
  } else {
    return target;
  }
};

为什么要初始化呢?因为在开发tree还需要预设置很多数据,例如:是否展开?那需要实现展开的功能,那么每个节点必然需要一个isOpen来控制,除此之外,还有很多其它的功能,比如判断层级等。

interface dataType {
  label: string;
  children?: dataType[];
  isOpen: boolean;
}
const copyData = ref([]);
onMounted(() => {
  copyData.value = init(deepCopy(props.data));
});

const init = (data: dataType[]) => {
  if (!data.length || !data) return [];
  let res = [];
  for (let i = 0; i < data.length; i++) {
    const child = data[i];
    const children = init(child[props.children] || []);
    const label = child[props.label];
    const isOpen = false;
    res.push({
      label,
      children,
      isOpen,
    });
  }
  return res;
};

展开和收缩

接下来,我们实现如何渲染节点和展开,这个其实很简单,我们只要在递归组件上面补充我们的想要插入的数据即可,同时绑定好事件,利用isOpen属性来实现展开收缩,我们只需要在渲染v-if上在添加v-show即可。

<ul class="tree-node">
    <div class="tree-node-content" @click.stop="handleToggle(items)">
      <span>{{ items.label }}</span></div
    >
    <div v-if="isRender" v-show="items.isOpen">
      <node
        v-for="(child, index) in items.children"
        :key="index"
        :items="child"
        :label="label"
        :children="children"></node>
    </div>
</ul>
  

const handleToggle = (item: any) => {
  item.isOpen = !item.isOpen;
}; 

参数设置

接下来我们来设置一些参数,因为我们不清楚用户传进来的树结构的属性是什么样子的,因此我们可以用参数来标识,比如用children来标识子节点,这些东西就可以自由发挥了。

const props = defineProps({
  data: {
    type: Array,
    default: () => [],
  },
  label: {
    type: String,
    default: "label",
  },
  children: {
    type: String,
    default: "children",
  },
});

🆗到此为止,我们就把核心功能实现完成,其实基础的功能并没有多困难,后续会补充源码。

image.png

懒加载优化

在这里我补充一个优化吧,一个简单的懒加载可以是这样的,只渲染第一层,深层的如果没有点击过就不去渲染。

这个实现思路也很容易,再增加一个isLazy参数,在初始化的时候给每个节点绑定上isLazy,在渲染时v-if增加判断isLazy就可以了。在点击的时候再把isLazy取消即可。

在参考了element的源码后,他们的懒加载还可以传入一个load函数,并用isLeft来标识动态加入新的数据。参考链接

其实实现起来也不难,我们只需要多传入一个load函数,在点击时调用该函数,并且new Promise来回调执行即可。

const init = (data: dataType[], level: number) => {
  if (!data.length || !data) return [];
  let res = [];
  for (let i = 0; i < data.length; i++) {
    const child = data[i];
    const children = init(child[props.children] || [], level + 1);
    const label = child[props.label];
    const isOpen = false;
+   const isLazy = props.isLazy;
+   const isLeft = child["isLeft"] || false;
    res.push({
      label,
      children,
      isOpen,
      isLazy,
+     isLeft,
+     level,
    });
  }
  return res;
};

点击后加载数据

const handleToggle = async (item: any) => {
  item.isOpen = !item.isOpen;
  if (item.isLazy) {
    if (item.isLeft && props.load) {
      await new Promise((resolve) => {
        props.load(item, resolve);
      })
        .then((res: any) => {
          for (let i = 0; i < res.length; i++) {
            res[i].isLazy = item.isLazy;
            res[i].level = item.level + 1;
          }
          item.children = res.slice();
        })
        .catch((err) => {
          console.log("[Tree Component] load Funtion Error", err);
        });
    }
    item.isLazy = false;
  }
};

演示demo

完整项目demo

结语

Tree组件的核心开发功能就是上面这些,其他更多的详细功能开发可以参考Hview-ui项目源码

如果想要了解更多的组件轮子开发,或者组件库开发流程,更多详细的组件开发过程更新在GitHub项目源码,最后觉得我们项目or文章不错可以点个star,点点小手支持一下,也欢迎各路大佬为我们的开源项目添砖加瓦。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐