在前面的章节,我们完成了可媲美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 node
const ocNode = { ...child }
// 扁平化new child node
const 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).value
const nodeVal = ref(node).value
const childrenName = optionProps.childrenName as 'children'
// 对原先的叶子节点进行设置和初始化,变为非叶子节点
if (!oNodeVal[childrenName]) {
oNodeVal[childrenName] = []
initParentNode(oNode, optionProps)
nodeVal.isLeaf = false
}
// 插入到原始结构化节点
oNodeVal[childrenName].push(ocNode as never)
// 所在的节点将其展开(如果折叠的话)
oNodeVal.expanded = true
nodeVal.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)
}
}
}
对应的插槽实现的地方: