ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Vue Element UI el-tree组件全选、展开等基础功能封装实战

Vue Element UI el-tree组件全选、展开等基础功能封装实战 1. 项目概述el-tree基础功能封装实战在Vue项目里用Element UI的el-tree组件几乎是处理树形数据的标配。但每次新开一个页面涉及到全选、清空、展开、收缩这些基础操作你是不是又要去翻文档或者从老项目里复制粘贴一遍代码更头疼的是不同页面的tree数据源、选中逻辑可能还不一样复制过来的代码总得改改才能用一来二去项目里就散落着各种大同小异的“轮子”。今天我们就来彻底解决这个问题。我将基于一个真实的后台管理系统场景分享如何对el-tree的这些基础功能进行一次高质量的封装。目标很明确封装一个通用的TreeOperate组件它不关心你的数据从哪里来是权限树、部门树还是分类树只提供一套稳定、易用的操作方法。你只需要传入一个tree的ref就能像调用函数一样轻松实现全选、清空、展开所有节点、收缩所有节点甚至获取当前所有选中节点。这不仅能极大提升开发效率更能保证项目里树形控件操作逻辑的一致性减少潜在的bug。2. 核心思路与设计考量2.1 为什么需要封装直接使用el-tree的原生方法当然可以但存在几个明显问题代码重复每个用到tree的页面你都要写一遍this.$refs.tree.setCheckedKeys([])来清空写一遍递归来展开所有节点。依赖Ref操作严重依赖模板中reftree的获取如果ref名称不一致或获取时机不对比如tree还未渲染就容易报错。逻辑分散全选逻辑可能混杂在业务代码里比如全选前需要过滤掉某些禁用的节点这个逻辑散落在各处难以维护。体验不一致不同开发者实现“展开全部”的细节可能不同比如是否展开懒加载的节点导致用户操作体验有差异。封装的核心思想是面向接口编程。我们不直接操作DOM或组件实例而是通过一个统一的“操作器”来发起指令由这个操作器内部去处理与具体el-tree实例的交互。2.2 方案选型Composition API vs Mixins在Vue 3和Vue 2.7的生态下我们有多种选择。Option API (Vue 2传统方式)在mixins里定义方法混入到组件中。缺点明显命名冲突、数据来源不清晰、类型支持差。Composition API (推荐)使用ref、computed和自定义组合式函数逻辑可以更灵活地组合和复用并且天然对TypeScript友好。我们的封装将主要采用Composition API的思想即使你的项目暂时是Vue 2也可以使用vue/composition-api插件来实现。我们会创建一个自定义Hook或Composable函数例如useTreeOperations它接收一个tree组件的引用返回一系列操作方法。2.3 设计目标与接口定义我们希望最终的使用方式尽可能简洁。理想状态下在Vue组件中应该是这样的template el-tree reftreeRef ... / button clickoperateTree.selectAll()全选/button button clickoperateTree.clear()清空/button /template script setup import { ref, onMounted } from vue import { useTreeOperations } from /hooks/useTreeOperations const treeRef ref(null) // el-tree的引用 let operateTree null onMounted(() { // 初始化操作器传入tree引用 operateTree useTreeOperations(treeRef) }) /script为了实现这个目标我们的useTreeOperations需要提供以下接口selectAll(options): 全选。可配置是否包含半选节点、是否跳过禁用节点。clear(): 清空所有选中。expandAll(): 展开所有节点。collapseAll(): 收缩所有节点。getCheckedNodes(): 获取所有选中的节点数据。getCheckedKeys(): 获取所有选中的节点key。3. 核心功能实现与源码解析接下来我们深入每个功能的实现细节。我会先给出代码然后解释关键点和注意事项。3.1 初始化与Ref处理首先我们需要安全地获取el-tree的组件实例。el-tree通过Vue的ref暴露出来但它的类型可能是null或undefined特别是在组件尚未挂载时。我们的操作函数需要处理这种边界情况。// hooks/useTreeOperations.js import { getCurrentInstance, onMounted } from vue export function useTreeOperations(treeRef) { // 内部保存tree实例 let treeInstance null // 一个初始化方法确保在组件mounted后调用 const init (ref) { if (!ref || !ref.value) { console.warn(Tree reference is not available.) return } treeInstance ref.value // 验证treeInstance是否具有el-tree的必要方法 if (treeInstance typeof treeInstance.setCheckedKeys function) { console.log(Tree operations initialized successfully.) } else { console.error(The provided ref is not a valid el-tree instance.) treeInstance null } } // 如果传入的是ref立即初始化也可以在onMounted后手动调用init if (treeRef) { // 注意如果treeRef是在setup中定义的此时可能还未绑定到组件实例上。 // 更稳妥的做法是在onMounted钩子中调用init。 } // 提供手动初始化的方法 const initialize (ref) init(ref) // 后续所有操作都先检查treeInstance是否存在 const checkInstance () { if (!treeInstance) { throw new Error(Tree operations are not initialized. Please call initialize() first.) } return true } return { initialize, // 其他操作方法... } }注意这里有一个关键细节。在Vue 3的script setup中模板ref在组件挂载前是null。因此最安全的做法不是在useTreeOperations内部立即初始化而是暴露一个initialize方法由调用者在组件的onMounted生命周期中执行。或者我们可以利用watch或watchEffect来监听treeRef的变化但为了逻辑清晰我们采用手动初始化。3.2 全选功能实现el-tree本身没有直接的全选API。我们需要自己计算所有可选节点的key。这里涉及到几个关键点数据源、node-key配置、以及禁用节点的处理。// 在useTreeOperations函数内部 const selectAll (options {}) { if (!checkInstance()) return const { includeHalfChecked false, // 是否包含当前半选的节点 skipDisabled true, // 是否跳过被disabled的节点 data null, // 可传入自定义数据默认使用treeInstance.store.data } options try { const store treeInstance.store const treeData data || store.data const allKeys [] // 递归遍历函数 const traverse (nodes) { if (!nodes || !Array.isArray(nodes)) return nodes.forEach(node { // 如果配置了跳过禁用节点且当前节点被禁用则跳过 if (skipDisabled node.disabled) { // 即使跳过当前节点仍需递归其子节点因为子节点可能未被禁用 if (node.children node.children.length 0) { traverse(node.children) } return } // 获取节点key使用配置的node-key属性 const nodeKey treeInstance.nodeKey ? node[treeInstance.nodeKey] : node.id if (nodeKey ! undefined) { allKeys.push(nodeKey) } // 递归遍历子节点 if (node.children node.children.length 0) { traverse(node.children) } }) } traverse(treeData) // 如果需要包含半选节点需要获取当前半选状态的key let keysToSet [...allKeys] if (includeHalfChecked) { const halfCheckedKeys treeInstance.getHalfCheckedKeys ? treeInstance.getHalfCheckedKeys() : [] // 合并去重 keysToSet Array.from(new Set([...keysToSet, ...halfCheckedKeys])) } // 调用el-tree的API设置选中 treeInstance.setCheckedKeys(keysToSet) console.log(Select all completed. Total ${keysToSet.length} nodes selected.) } catch (error) { console.error(Failed to select all nodes:, error) } }实操要点node-key是核心el-tree通过node-key属性来唯一标识节点。我们的遍历函数必须使用这个key来收集节点标识。通常这个属性会设置为id。递归遍历树形数据是嵌套的递归是最直接的遍历方式。确保你的递归终止条件正确避免无限循环。禁用节点的处理业务上禁用的节点如无权限的部门通常不允许被选中。我们的skipDisabled参数提供了这个灵活性。注意即使父节点被禁用其子节点也可能未被禁用所以递归不能因为遇到禁用节点就完全停止。半选状态在父子节点关联选中check-strictly为false时如果只选了部分子节点父节点会处于半选状态。getHalfCheckedKeys()方法可以获取这些节点的key。是否包含它们取决于业务例如“全选”操作可能希望把半选节点也变为全选。3.3 清空功能实现清空功能相对简单直接调用el-tree的setCheckedKeys([])方法即可。但需要考虑是否同时清空半选状态。const clear (clearHalfChecked true) { if (!checkInstance()) return try { treeInstance.setCheckedKeys([]) if (clearHalfChecked treeInstance.setHalfCheckedKeys) { // 注意element-ui的el-tree可能没有setHalfCheckedKeys方法 // element-plus的el-tree有getHalfCheckedKeys但设置半选通常不是直接API // 更常见的做法是清空选中后半选状态自然消失。 // 这里我们主要处理选中状态的清空。 } console.log(All checked nodes cleared.) } catch (error) { console.error(Failed to clear checked nodes:, error) } }注意在Element UI对应Vue 2中getHalfCheckedKeys和setHalfCheckedKeys可能不存在。在Element Plus对应Vue 3中这些API是存在的。封装时要注意版本差异。一个更兼容的做法是清空选中键后半选状态通常会随之更新不需要额外处理。3.4 展开与收缩功能实现展开和收缩需要遍历所有节点并修改它们的expanded状态。el-tree提供了store对象里面包含了所有节点的数据模型Node对象我们可以通过它来操作。const expandAll () { if (!checkInstance()) return const store treeInstance.store if (!store) return // 遍历所有节点将其展开 const expandNode (node) { if (node node.expanded ! true) { node.expanded true // 对于非懒加载树还需要触发视图更新。直接操作node后可能需要调用tree的updateKeyChildren。 // 更标准的方法是使用store的_setExpanded方法或直接调用node的expand方法。 } // 递归处理子节点 if (node.childNodes) { node.childNodes.forEach(child expandNode(child)) } } // 从根节点开始遍历 store.root.childNodes.forEach(node expandNode(node)) // 对于el-tree更简单的方式是直接操作data并触发更新但可能不响应式。 // 另一种可靠但“黑盒”的方式利用el-tree的default-expand-all属性在数据变化时重置但这不够动态。 }上面的方法直接操作node.expanded可能无法触发视图更新。更可靠、更推荐的做法是利用el-tree的updateKeyChildren方法或者采用一种更巧妙的方法通过改变一个响应式变量触发tree的重新渲染并结合default-expand-all属性。这里分享一个我在实际项目中验证有效的方案const expandAll () { if (!checkInstance()) return try { // 思路获取当前所有节点的key然后通过setCurrentKey等API间接触发 // 实际上el-tree没有直接的expandAll API。 // 方法递归调用每个节点的expand方法。store里的每个node有一个expand方法。 const expand (nodes) { nodes.forEach(node { node.expand() // 调用节点的expand方法 if (node.childNodes node.childNodes.length 0) { expand(node.childNodes) } }) } expand(treeInstance.store.root.childNodes) } catch (error) { console.error(Failed to expand all nodes:, error) } } const collapseAll () { if (!checkInstance()) return try { const collapse (nodes) { nodes.forEach(node { node.collapse() // 调用节点的collapse方法 if (node.childNodes node.childNodes.length 0) { collapse(node.childNodes) } }) } collapse(treeInstance.store.root.childNodes) } catch (error) { console.error(Failed to collapse all nodes:, error) } }关键解析store与node对象el-tree内部使用一个Store类管理状态每个节点对应一个Node对象。node.expand()和node.collapse()是这些内部对象的方法它们会改变节点的expanded属性并触发视图更新。递归调用我们需要从根节点的直接子节点store.root.childNodes开始递归地对每一个节点调用expand()或collapse()。性能考量对于节点数量巨大的树如超过1000个一次性展开所有节点可能导致页面卡顿。在生产环境中可以考虑加入防抖或懒加载优化。3.5 获取选中状态el-tree提供了getCheckedNodes和getCheckedKeys方法封装它们主要是为了统一错误处理和可能的格式转换。const getCheckedNodes (leafOnly false, includeHalfChecked false) { if (!checkInstance()) return [] try { let nodes [] if (includeHalfChecked treeInstance.getHalfCheckedNodes) { const halfNodes treeInstance.getHalfCheckedNodes() nodes [...halfNodes] } const checkedNodes treeInstance.getCheckedNodes(leafOnly) // 合并去重基于节点唯一标识如id const mergedMap new Map() ;[...nodes, ...checkedNodes].forEach(node { const key node[treeInstance.nodeKey] || node.id if (key ! undefined) { mergedMap.set(key, node) } }) return Array.from(mergedMap.values()) } catch (error) { console.error(Failed to get checked nodes:, error) return [] } } const getCheckedKeys (leafOnly false) { if (!checkInstance()) return [] try { return treeInstance.getCheckedKeys(leafOnly) } catch (error) { console.error(Failed to get checked keys:, error) return [] } }4. 完整封装与组件集成示例现在我们将上述所有功能整合到useTreeOperations这个自定义Hook中并提供一个在Vue组件中集成的完整示例。4.1 完整的Hook代码// src/hooks/useTreeOperations.js import { ref, onMounted } from vue /** * el-tree通用操作Hook * param {Ref} treeRef - el-tree组件的模板引用 * returns {Object} 操作对象 */ export function useTreeOperations(treeRef) { const treeInstance ref(null) const isInitialized ref(false) // 初始化应在onMounted后调用 const initialize (ref) { if (!ref || !ref.value) { console.warn([useTreeOperations] Tree ref is invalid.) return false } treeInstance.value ref.value // 基础验证 if (treeInstance.value typeof treeInstance.value.setCheckedKeys function) { isInitialized.value true console.log([useTreeOperations] Initialized successfully.) return true } else { console.error([useTreeOperations] The ref is not a valid el-tree instance.) treeInstance.value null return false } } const checkInstance () { if (!isInitialized.value || !treeInstance.value) { console.error([useTreeOperations] Not initialized or instance lost. Call initialize() first.) return false } return true } // 全选 const selectAll (options {}) { if (!checkInstance()) return const { includeHalfChecked false, skipDisabled true, customData } options const instance treeInstance.value const store instance.store const treeData customData || store.data const allKeys [] const traverse (nodes) { if (!Array.isArray(nodes)) return nodes.forEach(node { if (skipDisabled node.disabled) { if (node.children) traverse(node.children) return } const nodeKey instance.nodeKey ? node[instance.nodeKey] : node.id if (nodeKey ! undefined) allKeys.push(nodeKey) if (node.children) traverse(node.children) }) } traverse(treeData) let keysToSet allKeys if (includeHalfChecked instance.getHalfCheckedKeys) { const halfKeys instance.getHalfCheckedKeys() keysToSet Array.from(new Set([...allKeys, ...halfKeys])) } try { instance.setCheckedKeys(keysToSet) } catch (err) { console.error([useTreeOperations] selectAll error:, err) } } // 清空 const clear () { if (!checkInstance()) return try { treeInstance.value.setCheckedKeys([]) } catch (err) { console.error([useTreeOperations] clear error:, err) } } // 展开全部 const expandAll () { if (!checkInstance()) return const expand (nodes) { nodes.forEach(node { // 确保node有expand方法且未展开 if (node.expand !node.expanded) { node.expand() } if (node.childNodes) expand(node.childNodes) }) } try { expand(treeInstance.value.store.root.childNodes) } catch (err) { console.error([useTreeOperations] expandAll error:, err) } } // 收缩全部 const collapseAll () { if (!checkInstance()) return const collapse (nodes) { nodes.forEach(node { if (node.collapse node.expanded) { node.collapse() } if (node.childNodes) collapse(node.childNodes) }) } try { collapse(treeInstance.value.store.root.childNodes) } catch (err) { console.error([useTreeOperations] collapseAll error:, err) } } // 获取选中节点和key封装原生方法增加容错 const getCheckedNodes (leafOnly false) { return checkInstance() ? treeInstance.value.getCheckedNodes(leafOnly) : [] } const getCheckedKeys (leafOnly false) { return checkInstance() ? treeInstance.value.getCheckedKeys(leafOnly) : [] } const getHalfCheckedNodes () { return checkInstance() treeInstance.value.getHalfCheckedNodes ? treeInstance.value.getHalfCheckedNodes() : [] } const getHalfCheckedKeys () { return checkInstance() treeInstance.value.getHalfCheckedKeys ? treeInstance.value.getHalfCheckedKeys() : [] } return { initialize, isInitialized, selectAll, clear, expandAll, collapseAll, getCheckedNodes, getCheckedKeys, getHalfCheckedNodes, getHalfCheckedKeys, } }4.2 在Vue组件中的使用示例template div classtree-demo div classoperation-bar el-button clickhandleSelectAll全选/el-button el-button clickhandleClear清空/el-button el-button clickhandleExpandAll展开全部/el-button el-button clickhandleCollapseAll收缩全部/el-button el-button clicklogChecked打印选中项/el-button /div el-tree reftreeRef :datatreeData node-keyid show-checkbox default-expand-all :propsdefaultProps / /div /template script setup import { ref, onMounted } from vue import { ElMessage } from element-plus // 或 element-ui import { useTreeOperations } from /hooks/useTreeOperations // 模拟树形数据 const treeData ref([ { id: 1, label: 一级 1, children: [ { id: 4, label: 二级 1-1, children: [{ id: 9, label: 三级 1-1-1 }, { id: 10, label: 三级 1-1-2 }] }, ], }, { id: 2, label: 一级 2, children: [ { id: 5, label: 二级 2-1 }, { id: 6, label: 二级 2-2, disabled: true }, // 禁用的节点 ], }, { id: 3, label: 一级 3, children: [{ id: 7, label: 二级 3-1 }, { id: 8, label: 二级 3-2 }] }, ]) const defaultProps { children: children, label: label } // 模板引用 const treeRef ref(null) // 初始化操作Hook const treeOps useTreeOperations() // 组件挂载后初始化操作器 onMounted(() { if (treeRef.value) { const success treeOps.initialize(treeRef) if (!success) { ElMessage.warning(树组件初始化失败) } } }) // 按钮事件处理 const handleSelectAll () { treeOps.selectAll({ skipDisabled: true }) // 跳过禁用节点 } const handleClear () { treeOps.clear() } const handleExpandAll () { treeOps.expandAll() } const handleCollapseAll () { treeOps.collapseAll() } const logChecked () { const nodes treeOps.getCheckedNodes() const keys treeOps.getCheckedKeys() console.log(选中的节点:, nodes) console.log(选中的键:, keys) ElMessage.info(选中了 ${nodes.length} 个节点) } /script style scoped .tree-demo { padding: 20px; } .operation-bar { margin-bottom: 20px; } /style5. 进阶优化与常见问题排查5.1 性能优化应对大数据量树当树节点数量庞大比如超过5000个时全选、展开全部等操作可能会阻塞UI线程。我们可以采用以下策略优化分片操作将全选遍历操作拆分成多个小任务使用setTimeout或requestAnimationFrame放入事件循环的不同阶段执行避免长时间占用主线程。const selectAllLazy (options) { // ... 获取allKeys const chunkSize 100 // 每批处理100个节点 let index 0 const setKeysChunk () { const chunk allKeys.slice(index, index chunkSize) if (chunk.length 0) { // 注意setCheckedKeys会替换所有选中项不能分批设置。 // 对于全选可以一次性设置所有key但遍历收集key的过程可以分片。 // 这里演示的是遍历数据收集key的分片。 index chunkSize setTimeout(setKeysChunk, 0) // 下一帧继续 } else { // 收集完所有key后一次性设置 treeInstance.value.setCheckedKeys(allKeys) } } setKeysChunk() }虚拟滚动考虑使用支持虚拟滚动的树组件如Element Plus的el-tree-v2来从根本上解决渲染大量DOM节点的性能问题。我们的操作Hook需要适配不同组件版本。防抖操作为频繁触发的方法如搜索后展开对应节点添加防抖。5.2 兼容性处理Element UI vs Element Plus我们的Hook需要处理两个主要版本的差异功能/方法Element UI (Vue 2)Element Plus (Vue 3)我们的处理策略获取半选节点getHalfCheckedNodes()getHalfCheckedNodes()方法名相同直接调用。获取半选键getHalfCheckedKeys()getHalfCheckedKeys()方法名相同直接调用。节点对象node.expand()存在node.expand()存在通用。Ref类型this.$refs.treetreeRef.value通过Vue的ref统一处理。组合式API需插件vue/composition-api原生支持Hook内部使用Composition API在Vue2项目中需确保安装插件。在Hook中对于可能不存在的方法使用可选链操作符?.或进行条件判断增强健壮性。const halfCheckedKeys treeInstance.value.getHalfCheckedKeys?.() || []5.3 常见问题与排查技巧在实际使用中你可能会遇到以下问题操作无效控制台无报错检查点首先确认treeOps.isInitialized是否为true。确保在onMounted之后才调用initialize。检查点确认传入的treeRef是否正确指向了el-tree组件。在复杂组件结构中ref可能被覆盖。检查点检查el-tree的node-key属性是否设置且值在数据中唯一。这是所有选中操作的基础。全选时选中了不想选的节点检查点确认skipDisabled参数是否传入了true。如果业务上某些节点应被跳过但数据中disabled字段不为true则需要在遍历逻辑中添加自定义过滤条件。检查点检查数据中是否有重复的node-key值这会导致选中状态错乱。展开/收缩全部对懒加载节点无效原因懒加载节点的子数据在未展开时不存在于node.childNodes中因此expand()方法可能无法加载子节点。解决方案对于懒加载树展开全部是一个复杂需求通常需要结合业务逻辑。一种思路是递归调用el-tree的load方法但需注意性能。在封装时可以为expandAll增加一个lazyLoad参数提示用户此功能对懒加载树有限制。TypeScript类型支持为了让Hook更好用可以为其添加TypeScript类型定义。// useTreeOperations.ts import type { Ref } from vue import type ElTree from element-plus/es/components/tree // 或 element-ui的类型 export interface TreeOperations { initialize: (ref: RefInstanceTypetypeof ElTree | null) boolean selectAll: (options?: SelectAllOptions) void clear: () void // ... 其他方法 } export function useTreeOperations(treeRef?: Ref): TreeOperations { ... }5.4 封装后的价值与扩展思路通过这次封装我们得到了一个与业务逻辑解耦的树操作工具。它带来了几个明显的好处高复用任何页面的el-tree都可以直接使用无需重复编写逻辑。易维护所有树操作逻辑集中在一处bug修复和功能升级只需修改Hook文件。可测试可以单独为useTreeOperations编写单元测试保证核心逻辑的稳定性。你还可以在此基础上进行扩展选中策略模式将全选算法如是否跳过叶子节点、是否只选父节点等抽象为策略通过参数切换。操作历史记录实现简单的撤销/重做功能记录选中状态的变化。与状态管理集成将当前选中的节点key列表同步到Pinia或Vuex中供其他组件使用。最后封装没有银弹最重要的是贴合自己项目的实际需求。希望这个详细的el-tree功能封装总结能帮你下次面对树形控件时更加游刃有余。
返回列表