在前面的章节,我们完成了可媲美Element Plus Tree组件的基本开发。通过实现各种计算属性,tree数据状态变化引起的视图更新被计算属性所接管了,无需我们再手动做各种遍历、查找以及手动监听操作,这样后续开发高级功能变得易如反掌啦!!
看下提供给用户的vitepress
文档说明:
操作演示:
前面我们实现了几个计算属性:
-
index
节点在扁平化列表中的位置索引
-
length
父节点的所有子孙节点的长度
-
visibleLength
可见子孙节点的长度
-
lineLength
参照线的长度
这些计算属性在新增一个节点,尤其是子节点时都会被影响到,触发重新计算以保证前面实现的基本功能是完好的。而无需我们在实现新增节点时再去兼顾基础功能,这就是Vue3 composition api的计算属性的魅力,让复杂的功能变得简单,组件的开发者只需要把关注点放到影响计算属性变化的数据上即可,Life is so easy!
新增类型、接口
定义ts
的类型和接口,注意给用户提供的接口一定要遵循“迪米特法则”。
核心插入逻辑
/*** 新增顶级节点* @param child 要新增的叶子节点* @param data 扁平化节点列表* @param treeData 结构化节点树* @param optionProps 组件配置选项*/
export function appendTop(child: ILeafNode, data: IFlatTreeNode[], treeData: ITreeNode[], optionProps: OptionProps) {// 节点id命名逻辑:如果指定了就用用户指定的,否则按照列表长度生成child.id = child.id || 'id-' + (data.length + 1)// 从新增节点拷贝数据作为original child nodeconst ocNode = { ...child }// 扁平化new child nodeconst ncNode = {...child,level: 1,isLeaf: true,originalNode: ocNode} as IFlatTreeNode// 要插入的位置为列表最后const insertIndex = data.length// 绑定新插入的扁平化节点的前置节点ncNode.prev = data[data.length - 1]// 对新的扁平化节点进行初始化initFlatTreeNode(ncNode, optionProps)// 原始树结构中新增节点,注意!!操作的是响应式数据ref(treeData).value.push(ocNode as never)// 扁平化列表中插入新节点ref(data).value.splice(insertIndex, 0, ncNode as never)
}function initFlatTreeNode(node: IFlatTreeNode, optionProps: OptionProps) {.../*** 给扁平化节点绑定新增子节点的方法* @param child 新增的子节点* @param data 扁平化列表*/node.append = (child: ILeafNode, data: IFlatTreeNode[]) => {// 同新增一级节点child.id = child.id || 'id-' + (data.length + 1)// 当前节点原始节点const oNode = node.originalNode// 新增节点原始节点const ocNode = { ...child }// 新增节点扁平化节点const ncNode = {...child,parent: node, // 绑定父节点level: node.level + 1,isLeaf: true,originalNode: ocNode} as IFlatTreeNode// 计算插入位置const insertIndex = calcInsertIndex(node)// 插入到最后的情况下,设置前置节点if (insertIndex === data.length) {ncNode.prev = data[data.length - 1]} else {// 插入到中间,绑定prev的逻辑,把prev链接起来const next = data[insertIndex]ncNode.prev = next.prev// 注意操作的是响应式对象,以确保可以触发index属性重新计算!!ref(next).value.prev = ncNode as never}// 初始化扁平化节点initFlatTreeNode(ncNode, optionProps)// 通过响应式对象获取其操作对象const oNodeVal = ref(oNode).valueconst nodeVal = ref(node).valueconst childrenName = optionProps.childrenName as 'children'// 对原先的叶子节点进行设置和初始化,变为非叶子节点if (!oNodeVal[childrenName]) {oNodeVal[childrenName] = []initParentNode(oNode, optionProps)nodeVal.isLeaf = false}// 插入到原始结构化节点oNodeVal[childrenName].push(ocNode as never)// 所在的节点将其展开(如果折叠的话)oNodeVal.expanded = truenodeVal.expanded = true// 插入到扁平化节点列表ref(data).value.splice(insertIndex, 0, ncNode as never)}
}/*** 插入子节点位置逻辑:如果是叶节点,则为下一个位置,否则要加上子一代节点的长度* @param node*/
function calcInsertIndex(node: IFlatTreeNode): number {return node.index.value + 1 + (node.isLeaf ? 0 : node.originalNode.length!.value)
}
Tree组件模板调整
原先给icon
插槽传入的节点参数,不符合迪米特法则,暴露了内部操作属性和方法,规范的做法是拷贝一个副本!!只给用户提供其关心的几个属性,调整为:
对于一级节点新增操作,我们将对tree
组件expose
一个可操作的对象,为此把这个对象中要定义的方法抽取到ts
接口中,以方便客户端API的使用:
// Tree组件对外导出的方法定义
export interface ExposeProps {appendTop: (newNode: ILeafNode) => void
}
导出逻辑:
而针对节点的操作,给用户提供的API,包装一个函数来返回要操作的接口:
// 返回节点操作方法的函数
const nodeOperation = (node: IFlatTreeNode): ITreeNodeOperation => {// 注意,这里不应该直接给用户提供node,而是要包成对外公开的ITreeNodeOperation,遵循迪米特法则!!return {append(newNode: ILeafNode) {node.append(newNode, originalFlatData)}}
}
对应的插槽实现的地方: