- 基础静态树形菜单:使用 HTML 和 CSS 实现一个可折叠/展开的树。
- 引入 jQuery 和事件处理:使用 jQuery 来处理点击事件,实现动态的折叠/展开效果。
- 从服务器动态加载数据:这是核心部分,我们将通过 AJAX 从后端 API 获取数据,并动态生成树形菜单。
- 完整代码示例:提供一个完整的、可运行的 HTML 文件,包含所有功能。
- 总结与扩展:讨论如何进行功能扩展,如添加复选框、图标等。
最终效果预览
我们将创建一个类似下面这样的树形菜单,节点可以点击展开/折叠,并且子节点是动态加载的。
根节点
├─ 子节点 1
│ ├─ 孙节点 1.1
│ └─ 孙节点 1.2
├─ 子节点 2
└─ 子节点 3 (点击后,从这里开始加载它的子节点)
第 1 步:基础静态树形菜单
我们用 HTML 和 CSS 创建一个静态的树形结构,这是所有后续功能的基础。
HTML 结构
我们将使用嵌套的 <ul> 和 <li> 列表来表示树形层级,为了方便 jQuery 选择,我们给每个可点击的节点添加一个 data-node-id 属性。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">jQuery 动态树形菜单</title>
<style>
/* 基础样式 */
body {
font-family: Arial, sans-serif;
margin: 20px;
}
ul {
list-style-type: none; /* 移除默认列表项符号 */
padding-left: 20px; /* 为子列表添加缩进 */
}
li {
margin: 5px 0;
}
.node-content {
cursor: pointer;
padding: 5px;
border-radius: 4px;
}
.node-content:hover {
background-color: #f0f0f0;
}
.node-children {
display: none; /* 默认隐藏子节点 */
}
.node-children.expanded {
display: block; /* 展开时显示 */
}
.has-children::before {
content: '▶ '; /* 可折叠节点前的箭头 */
display: inline-block;
transition: transform 0.2s;
}
.has-children.expanded::before {
transform: rotate(90deg); /* 展开时旋转箭头 */
}
</style>
</head>
<body>
<h1>jQuery 动态树形菜单</h1>
<ul id="tree-root">
<li>
<div class="node-content has-children" data-node-id="1">
根节点
</div>
<ul class="node-children">
<li>
<div class="node-content has-children" data-node-id="2">
子节点 1
</div>
<ul class="node-children">
<li><div class="node-content" data-node-id="4">孙节点 1.1</div></li>
<li><div class="node-content" data-node-id="5">孙节点 1.2</div></li>
</ul>
</li>
<li>
<div class="node-content has-children" data-node-id="3">
子节点 2 (动态加载)
</div>
<ul class="node-children">
<!-- 子节点将通过 AJAX 动态插入这里 -->
</ul>
</li>
</ul>
</li>
</ul>
<!-- 引入 jQuery 库 -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</body>
</html>
代码解释:
- HTML: 使用嵌套
<ul>表示层级关系。.node-content是每个节点的可点击区域。.node-children是其子节点的容器,默认display: none。 - CSS:
list-style-type: none和padding-left创建了树的层级视觉效果。.node-children.expanded类通过display: block来控制子节点的显示与隐藏。.has-children::before使用 CSS 伪元素添加了一个小箭头,.expanded类会旋转这个箭头,提供视觉反馈。
第 2 步:引入 jQuery 并实现交互
我们使用 jQuery 来为节点添加点击事件,切换 .expanded 类,实现动态的折叠和展开。
JavaScript 代码
在 HTML 文件的底部 <script> 标签内添加以下代码:
$(document).ready(function() {
// 为所有有子节点的节点添加点击事件
$('.node-content.has-children').on('click', function(e) {
e.stopPropagation(); // 阻止事件冒泡,避免不必要的触发
// 1. 切换当前节点的 'expanded' 类
$(this).toggleClass('expanded');
// 2. 切换其子节点的显示/隐藏
$(this).siblings('.node-children').toggleClass('expanded');
});
});
代码解释:
$(document).ready(function() { ... });: 确保在 DOM 完全加载后再执行我们的代码。$('.node-content.has-children').on('click', function(e) { ... });: 为所有带有has-children类的节点绑定点击事件。e.stopPropagation(): 这是一个好习惯,它防止点击事件被父元素重复捕获。$(this).toggleClass('expanded'): 切换被点击节点自身的expanded类,这会改变箭头的方向。$(this).siblings('.node-children').toggleClass('expanded'): 找到被点击节点的兄弟元素中class为node-children的<ul>,并切换其expanded类,从而控制子列表的显示和隐藏。
你可以直接打开这个 HTML 文件,点击 "根节点" 和 "子节点 1",它们应该可以正常折叠和展开了。
第 3 步:从服务器动态加载数据
这是本教程的核心,我们将实现一个功能:当用户点击一个节点("子节点 2")时,才去服务器请求它的子节点数据,并动态插入到 DOM 中。
模拟后端 API
在实际开发中,后端会提供一个 API 接口,根据节点 ID 返回其子节点数据,这里我们用一个简单的 JavaScript 函数来模拟这个 API 的返回。
// 模拟从后端 API 获取数据
function fetchChildNodes(parentNodeId) {
console.log(`正在为节点 ${parentNodeId} 获取子节点...`);
// 模拟网络延迟
return new Promise((resolve) => {
setTimeout(() => {
// 模拟的数据库
const mockData = {
'3': [ // 节点ID为3的子节点
{ id: '6', name: '动态子节点 3.1' },
{ id: '7', name: '动态子节点 3.2' },
{ id: '8', name: '动态子节点 3.3 (可继续展开)' }
],
'8': [ // 节点ID为8的子节点
{ id: '9', name: '动态孙节点 8.1' },
{ id: '10', name: '动态孙节点 8.2' }
]
};
// 返回数据或空数组
resolve(mockData[parentNodeId] || []);
}, 800); // 模拟 800ms 的网络延迟
});
}
修改 jQuery 代码以支持动态加载
我们需要修改之前的点击事件处理函数,增加动态加载的逻辑。
$(document).ready(function() {
// 为所有有子节点的节点添加点击事件
$(document).on('click', '.node-content.has-children', function(e) {
e.stopPropagation();
const $this = $(this);
const $childrenContainer = $this.siblings('.node-children');
const nodeId = $this.data('node-id');
// 检查是否已经加载过子节点
// 我们可以通过检查子容器是否为空来判断
if ($childrenContainer.children().length > 0) {
// 如果已经加载,则直接切换显示/隐藏
$this.toggleClass('expanded');
$childrenContainer.toggleClass('expanded');
} else {
// 如果未加载,则先显示加载中提示
$childrenContainer.html('<li>加载中...</li>').addClass('expanded');
// 调用模拟的API获取数据
fetchChildNodes(nodeId).then(data => {
// 清空 "加载中..." 提示
$childrenContainer.empty();
if (data.length > 0) {
// 遍历返回的数据,并生成HTML
data.forEach(child => {
// 判断子节点是否还有子节点(这里我们简单判断,实际应由后端返回)
const hasChildren = child.id === '8'; // 模拟节点8有子节点
const childLi = $(`<li><div class="node-content ${hasChildren ? 'has-children' : ''}" data-node-id="${child.id}">${child.name}</div></li>`);
$childrenContainer.append(childLi);
});
// 展开父节点
$this.addClass('expanded');
// 递归绑定事件:为新添加的节点也绑定点击事件
// 这样可以实现无限层级的动态加载
bindTreeEvents();
} else {
// 如果没有子节点,移除 has-children 类,防止用户再次点击
$this.removeClass('has-children');
}
});
}
});
// 将事件绑定逻辑抽离出来,方便递归调用
function bindTreeEvents() {
$('.node-content.has-children').off('click').on('click', function(e) {
// 这里的事件处理逻辑和上面的大同小异
// 为了代码清晰,我们可以调用一个统一的处理函数
handleNodeClick($(this));
});
}
// 统一的节点点击处理函数
function handleNodeClick($node) {
e.stopPropagation();
const $childrenContainer = $node.siblings('.node-children');
const nodeId = $node.data('node-id');
// ... (这里把上面 else 块里的逻辑放进来)
// 为了简化,我们直接在原来的事件处理函数里写逻辑
// 在实际项目中,抽离成函数是更好的做法
}
});
代码解释:
- 事件委托: 我们使用
$(document).on('click', '.node-content.has-children', ...)而不是直接$('.node-content.has-children').on(...),这样做的好处是,即使后来通过 AJAX 动态添加了新的.node-content.has-children元素,它们也能自动获得点击事件,无需重新绑定。 - 检查是否已加载:
if ($childrenContainer.children().length > 0)是一个简单的判断,如果子容器里已经有内容了,说明数据已经加载过,我们只需要切换显示状态即可。 - 动态加载流程:
- 如果未加载,先在子容器里显示 "加载中...",并立即展开它,给用户反馈。
- 调用
fetchChildNodes(nodeId)获取数据。 - 数据返回后,清空 "加载中..." 提示。
- 遍历
data数组,使用 jQuery 的 和.html()/.append()方法动态创建<li>和<div>节点,并插入到 DOM 中。 - 为新添加的节点添加
has-children类(如果它确实有子节点)。 - 我们调用一个
bindTreeEvents()(这个概念在完整代码中会优化),确保新节点也能响应点击事件。
第 4 步:完整可运行的代码示例
将以上所有部分整合在一起,你将得到一个完整的、功能齐全的动态树形菜单。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">jQuery 动态树形菜单 - 完整示例</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 20px;
background-color: #f9f9f9;
}
h1 {
color: #333;
}
#tree-root {
list-style-type: none;
padding-left: 0;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
ul {
list-style-type: none;
padding-left: 25px;
border-left: 1px dashed #ccc;
}
li {
margin: 2px 0;
position: relative;
}
.node-content {
cursor: pointer;
padding: 8px 10px;
border-radius: 4px;
user-select: none; /* 防止文本被选中 */
transition: background-color 0.2s;
}
.node-content:hover {
background-color: #e9e9e9;
}
.node-children {
display: none;
}
.node-children.expanded {
display: block;
}
.has-children::before {
content: '▶ ';
display: inline-block;
margin-right: 5px;
font-size: 0.8em;
color: #666;
transition: transform 0.2s ease;
}
.has-children.expanded::before {
transform: rotate(90deg);
}
.loading {
color: #888;
font-style: italic;
padding-left: 20px;
}
</style>
</head>
<body>
<h1>jQuery 动态树形菜单</h1>
<p>点击带有箭头的节点来展开/折叠,子节点将在点击时动态加载。</p>
<ul id="tree-root">
<li>
<div class="node-content has-children" data-node-id="1">
根节点
</div>
<ul class="node-children">
<li>
<div class="node-content has-children" data-node-id="2">
子节点 1 (静态)
</div>
<ul class="node-children">
<li><div class="node-content" data-node-id="4">孙节点 1.1</div></li>
<li><div class="node-content" data-node-id="5">孙节点 1.2</div></li>
</ul>
</li>
<li>
<div class="node-content has-children" data-node-id="3">
子节点 2 (动态加载)
</div>
<ul class="node-children">
<!-- 子节点将通过 AJAX 动态插入这里 -->
</ul>
</li>
</ul>
</li>
</ul>
<!-- 引入 jQuery 库 -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(document).ready(function() {
// 模拟从后端 API 获取数据
function fetchChildNodes(parentNodeId) {
console.log(`正在为节点 ${parentNodeId} 获取子节点...`);
return new Promise((resolve) => {
setTimeout(() => {
const mockData = {
'3': [
{ id: '6', name: '动态子节点 3.1' },
{ id: '7', name: '动态子节点 3.2' },
{ id: '8', name: '动态子节点 3.3 (可继续展开)' }
],
'8': [
{ id: '9', name: '动态孙节点 8.1' },
{ id: '10', name: '动态孙节点 8.2' }
]
};
resolve(mockData[parentNodeId] || []);
}, 800);
});
}
// 统一的节点点击处理逻辑
function handleNodeClick($node) {
const $childrenContainer = $node.siblings('.node-children');
const nodeId = $node.data('node-id');
// 如果已经加载过子节点,则直接切换显示状态
if ($childrenContainer.data('loaded')) {
$node.toggleClass('expanded');
$childrenContainer.toggleClass('expanded');
} else {
// 显示加载中提示
$childrenContainer.html('<li class="loading">加载中...</li>').addClass('expanded');
// 调用API获取数据
fetchChildNodes(nodeId).then(data => {
$childrenContainer.empty(); // 清空加载提示
if (data.length > 0) {
data.forEach(child => {
// 判断子节点是否还有子节点
const hasChildren = child.id === '8';
const childLi = $(`<li><div class="node-content ${hasChildren ? 'has-children' : ''}" data-node-id="${child.id}">${child.name}</div></li>`);
$childrenContainer.append(childLi);
});
$node.addClass('expanded'); // 加载成功后,保持展开状态
$childrenContainer.data('loaded', true); // 标记为已加载
} else {
// 如果没有子节点,移除 has-children 类,防止用户再次点击
$node.removeClass('has-children');
}
});
}
}
// 使用事件委托为所有 .node-content.has-children 绑定点击事件
// 这样动态添加的节点也能自动获得事件
$(document).on('click', '.node-content.has-children', function(e) {
e.stopPropagation();
handleNodeClick($(this));
});
});
</script>
</body>
</html>
完整代码的优化点:
data('loaded'): 我们使用 jQuery 的.data()方法来标记一个节点的子数据是否已经加载过,这比检查子元素数量更可靠。- 事件委托: 在
$(document)上使用.on(),确保所有动态生成的节点都能正确响应点击。 - 统一的
handleNodeClick函数: 将核心逻辑封装,使代码更清晰、更易于维护。
第 5 步:总结与扩展
我们成功地从零开始,构建了一个功能完善的 jQuery 动态树形菜单,核心步骤如下:
- 结构先行: 用语义化的 HTML (
<ul>,<li>) 搭建树的基本结构。 - 样式美化: 用 CSS 控制树的层级、折叠/展开状态和交互效果。
- 静态交互: 用 jQuery 实现基本的点击折叠/展开。
- 动态加载: 结合 AJAX (或 Promise 模拟) 和 DOM 操作,实现按需加载子节点数据。
- 事件优化: 使用事件委托,确保动态内容也能正确绑定事件。
扩展功能
你可以基于这个基础,轻松地添加更多功能:
-
添加复选框:
- 在 HTML 的
.node-content中添加<input type="checkbox">。 - 使用 CSS 调整复选框和文本的布局。
- 在 jQuery 中监听
change事件,获取选中状态,并可以递归地处理子节点的选中状态(父节点选中则所有子节点自动选中)。
- 在 HTML 的
-
添加图标:
- 使用图标库(如 Font Awesome)。
- 根据节点是否有子节点、是否展开等状态,动态切换不同的图标类名,
fa-folder和fa-folder-open。
-
异步加载失败处理:
- 在
fetchChildNodes的.then()后面添加.catch()或Promise的catch块,当请求失败时,在$childrenContainer中显示“加载失败,请重试”的提示,并提供一个重试按钮。
- 在
-
搜索与高亮:
- 添加一个搜索框。
- 遍历整个树,将匹配的节点高亮显示(改变背景色),并自动展开其所有父节点以便用户看到。
希望这份详细的教程能帮助你掌握 jQuery 动态树形菜单的实现!
