在后台管理系统开发中,树形选择器(TreeSelect)是高频使用的组件,Element Plus 提供的 el-tree-select 虽然支持懒加载,但默认需要点击节点才会加载子层级数据。本文将分享如何基于 Vue3 + Element Plus 实现页面初始化时全量预加载树形数据,并结合业务场景实现「指定层级停止加载」的优化方案,解决频繁接口请求、用户体验差的问题。
一、业务场景与痛点
1. 核心需求
- 行政 / 部门树形选择器,层级固定(如省 / 市 / 区三级),三级节点无需加载子节点;
- 页面初始化时自动加载所有层级数据,避免用户点击节点时等待接口响应;
- 保留树形选择器的过滤、选中功能,基于本地数据交互,提升体验。
2. 传统懒加载的痛点
- 每次点击节点都触发接口请求,网络延迟时用户需等待;
- 多次重复请求同一节点数据,增加后端压力;
- 过滤功能需基于已加载的节点,未加载的子节点无法被过滤。
二、技术方案设计
1. 核心思路
- 递归预加载:页面挂载后先加载根节点,再递归遍历所有节点加载子节点,直到指定层级(三级)停止;
- 缓存机制:通过 Set 缓存已加载的节点 ID,避免重复请求;
- 异常处理:请求失败时兜底赋值空数组,防止组件渲染异常;
- 加载状态:添加加载提示,避免页面空白导致用户误解。
2. 技术栈
- 框架:Vue3(Setup 语法糖)
- UI 组件:Element Plus
el-tree-select - 接口请求:Axios(封装后的业务接口)
- 核心特性:异步递归、响应式数据、Set 缓存
三、完整实现代码
1. 模板部分(Template)
<template>
<div class="container">
<!-- 加载状态提示:无动画纯文本,适配极简风格 -->
<div v-if="loading" class="loading-text">加载中...</div>
<!-- 树形选择器:基于预加载的本地数据渲染 -->
<el-tree-select
v-else
v-model="selectedValue"
:data="treeData"
:filter-node-method="filterNodeMethod"
filterable
placeholder="请选择部门/区域"
style="width: 240px"
/>
</div>
</template>
2. 脚本部分(Script Setup)
import { ref, onMounted } from 'vue'
// 业务接口:加载指定节点的子层级数据(根节点不传参)
import { deptTreeJustOneLevel } from "@/api/login"
// 响应式数据定义
const selectedValue = ref('') // 选中值
const treeData = ref([]) // 树形选择器数据源
const loading = ref(false) // 加载状态
const loadedNodeIds = ref(new Set()) // 已加载节点ID缓存,避免重复请求
/**
* 递归预加载所有子节点
* @param {Array} nodes 当前层级的节点列表
* @description 遍历节点加载子数据,三级节点(departmentLevel === "3")停止加载
*/
const preloadAllChildren = async (nodes) => {
// 遍历当前层级所有节点
for (const node of nodes) {
// 核心:三级节点不加载子节点,直接跳过
if (node.departmentLevel === "3") {
continue
}
// 缓存命中:已加载的节点不再请求
if (loadedNodeIds.value.has(node.value)) {
continue
}
try {
// 调用接口加载子节点(传当前节点ID)
const childrenRes = await deptTreeJustOneLevel({ deptId: node.value })
const children = childrenRes.data
// 缓存已加载节点ID
loadedNodeIds.value.add(node.value)
// 给当前节点赋值子节点,供组件渲染
node.children = children
// 递归加载子节点的子节点(非三级节点才继续)
if (children.length > 0 && node.departmentLevel !== "3") {
await preloadAllChildren(children)
}
} catch (err) {
console.error(`加载节点【${node.label}】的子数据失败:`, err)
// 异常兜底:赋值空数组,防止树形组件渲染报错
node.children = []
}
}
}
/**
* 初始化全量预加载数据
* @description 页面挂载后执行,先加载根节点,再递归加载所有子节点
*/
const initPreloadAllData = async () => {
loading.value = true
try {
// 1. 加载根节点(不传参)
const rootRes = await deptTreeJustOneLevel()
const rootNodes = rootRes.data
// 2. 递归加载所有子节点
await preloadAllChildren(rootNodes)
// 3. 赋值给树形选择器数据源
treeData.value = rootNodes
console.log('所有层级数据初始化完成:', treeData.value)
} catch (err) {
console.error('初始化加载树形数据失败:', err)
treeData.value = []
} finally {
// 无论成功/失败,关闭加载状态
loading.value = false
}
}
/**
* 过滤节点方法(可选)
* @param {String} value 过滤关键词
* @param {Object} data 节点数据
* @returns {Boolean} 是否匹配
*/
const filterNodeMethod = (value, data) => {
if (!value) return true
return data.label.toLowerCase().includes(value.toLowerCase())
}
// 页面挂载后执行初始化
onMounted(() => {
initPreloadAllData()
})
3. 样式部分(Style Scoped)
.container {
padding: 20px;
}
/* 加载文本样式:与组件高度一致,避免页面跳动 */
.loading-text {
line-height: 32px; /* 匹配el-tree-select高度 */
text-align: center;
color: #666;
font-size: 14px;
width: 240px;
}
四、核心逻辑解析
1. 递归预加载(preloadAllChildren)
这是整个方案的核心,实现步骤:
- 遍历当前层级的所有节点,判断是否为三级节点,若是则跳过;
- 检查节点 ID 是否在缓存中,避免重复请求;
- 调用接口加载子节点,赋值给当前节点的
children属性; - 递归处理子节点,直到所有层级加载完成或遇到三级节点。
2. 缓存机制(loadedNodeIds)
使用 ES6 的 Set 结构缓存已加载的节点 ID,相比数组:
- 查找效率更高(O (1)),适合高频判断;
- 自动去重,无需手动处理重复 ID。
3. 异常处理
- 接口请求失败时,给节点赋值空数组
node.children = [],防止树形组件因children为 undefined 报错; - 根节点加载失败时,
treeData赋值空数组,保证页面不崩溃。
4. 加载状态管理
- 初始化时开启
loading,请求完成(无论成败)关闭; - 加载文本的
line-height与组件高度一致,避免页面布局跳动。
五、总结与思考
本文实现的「树形选择器全量预加载」方案,核心是通过异步递归将懒加载的接口请求提前到页面初始化阶段,结合缓存机制避免重复请求,既解决了传统懒加载的体验问题,又保证了数据的实时性(可通过刷新方法更新)。
适用场景
- 树形数据层级固定、数据量适中(千级以内);
- 对用户体验要求高,不希望点击节点时等待;
- 需要基于全量数据实现过滤、检索功能。
注意事项
- 若树形数据量极大(万级以上),不建议全量预加载,可结合「虚拟滚动 + 懒加载」优化;
- 需与后端确认接口频率限制,避免初始化时请求过多导致接口限流;
- 缓存数据需考虑时效性,可添加缓存过期时间或手动刷新机制。
通过该方案,既能保留 Element Plus 树形选择器的原生功能,又能基于业务场景优化交互体验,是后台管理系统中树形选择组件的一种高效实现方式。
编辑分享