引言
在网页开发中,树形菜单是一种非常实用的,它可以清晰地展示具有层级关系的数据,并且能够方便用户进行导航和操作。
整体思路
整个项目主要分为三个部分:HTML 结构搭建、CSS 样式设计和 JavaScript 交互逻辑实现。通过 XMLHttpRequest
对象从 JSON 文件中获取数据,将数据转换为树形结构,然后将树形结构渲染为 HTML 元素,并添加点击事件处理逻辑,实现菜单的展开 / 收起和内容的切换。
效果展示
点击不同的节点会有不同的页面显示,并且菜单是在页面的左侧,引入的iframe是在页面的右侧。
代码总览
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>树形结构示例</title>
<style>
body {
display: flex;
margin: 0;
padding: 0;
}
.left {
width: 220px;
background-color: #98D5FF;
display: flex;
flex-direction: column;
overflow-y: auto;
/* height: 100vh; */
}
.content {
display: flex;
align-items: center;
padding: 10px;
height: 35px;
cursor: pointer;
}
.content img {
margin-right: 8px;
}
.right {
background-color: seagreen;
flex-grow: 1;
height: 100vh;
}
.box {
display: none;
margin-left: 20px;
}
.imgBot {
margin-left: 100px;
}
.imgLeft {
margin-right: 10px;
}
iframe {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<script>
let data;
let xhr = new XMLHttpRequest();
xhr.open('get', './js/trees.json', true);
xhr.send();
//回调函数,(放一些比较依赖data的代码)确保数据请求成功后在进行数据渲染
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
let text = xhr.responseText;
data = JSON.parse(text);
// 开始构建:调用parseTree函数,pid为0开始构建树结构,并将结果存到treeData里
let treeData = parseTree(data, 0);
// 渲染树形结构并插入到body中(相当于重新写了一下body)
document.body.innerHTML = renderTree(treeData);
// 创建右侧 iframe(插入的页面)
//1.创建一个div
let iframeDiv = document.createElement('div');
//2.给这个div起名
iframeDiv.className = 'right';
// 3.把iframe插入到这个div中,后续 通过点击的树形节点更新src
iframeDiv.innerHTML = '<iframe id="contentFrame" src="" frameborder="0"></iframe>';
//4.把创建好的div插入到body
document.body.appendChild(iframeDiv);
}
};
//递归构建树形结构的函数
function parseTree(data, pid) {
// 用来存储树状的数据
let tree = [];
for (let i in data) {
//如果数据的 pid 属性等于传入的 pid(父级的id)
//⭐子集的pid和父级的id是保持一致的,这样才能完成递归
if (data[i].pid === pid) {
tree.push(data[i]);
//递归调用parseTree,传data和当前数据的id作为pid(上面函数里是data和pid),为了查找当前数据的子集然后用child(子集)方法插入到当前数据后。
tree[tree.length - 1].child = parseTree(data, data[i].id);
}
}
// 返回构建的树形结构
return tree;
}
//转化函数(HTML结构)
function renderTree(data) {
// 这个div是左边菜单的底板
let str = '<div class="left">';
for (let i in data) {
//如果当前数据有子节点的话,就为该数据生成一个包含子节点的html
if (data[i].child.length > 0) {
str +=
`<div class="textBox">
<div class="content">
<img class="imgLeft" src="./img/圣诞礼物.png" width="20px" />
${data[i].name}
<img class="imgBot" src="./img/向下箭头.png" width="20px" height="20px"/>
</div>
<div class="box" style="display:none;">`;
//递归调用renderTree函数,生成当前节点的子节点(只有在有子节点的条件下才递归,因为要去检查子节点有没有子节点,没有的话就结束)
str += renderTree(data[i].child);
//拼结束标签
str += '</div></div>';
//如果当前数据没有子节点的话,就添加普通元素
} else {
str +=
`<div class="textBox">
<div class="content">
<img class="imgLeft" src="./img/圣诞礼物.png" width="20px" />
${data[i].name}
</div>
</div>`;
}
}
str += '</div>';
return str;
}
// 监听点击事件(给body上),切换子节点显示/隐藏,并更新 iframe 的 src
document.body.addEventListener('click', function(e) {
//获取点击的目标元素(全局的)
let target = e.target;
//向上查找父元素,找到有content元素
while (target && !target.classList.contains('content')) {
target = target.parentElement;
}
//如果找到了
if (target) {
// 通过 target.nextElementSibling 获取 target 元素的下一个兄弟元素(即紧接着 .content 元素之后的元素)。
let sibling = target.nextElementSibling;
// 如果该兄弟元素有box元素
if (sibling && sibling.classList.contains('box')) {
// 展开或收起子节点
sibling.style.display = sibling.style.display === 'none' ? 'block' : 'none';
}
// 点击叶子节点,更新 iframe
//通过target.textContent获取到点击的目标元素文本
let name = target.textContent.trim();
let item = null;
//1.通过循环在data中找与name相匹配的数据
for (let i = 0; i < data.length; i++) {
// 2.如果相匹配
if (data[i].name === name) {
// 3.就存到item里
item = data[i];
break;
}
}
// 如果找到匹配的数据且path路径不为空,则更新iframe的src
if (item && item.path) {
document.getElementById('contentFrame').src = item.path;
}
}
});
</script>
</body>
</html>
css 部分
body {
display: flex;
margin: 0;
padding: 0;
}
.left {
width: 220px;
background-color: #98D5FF;
display: flex;
flex-direction: column;
overflow-y: auto;
/* height: 100vh; */
}
.content {
display: flex;
align-items: center;
padding: 10px;
height: 35px;
cursor: pointer;
}
.content img {
margin-right: 8px;
}
.right {
background-color: seagreen;
flex-grow: 1;
height: 100vh;
}
.box {
display: none;
margin-left: 20px;
}
.imgBot {
margin-left: 100px;
}
.imgLeft {
margin-right: 10px;
}
iframe {
width: 100%;
height: 100%;
}
JS部分
1.数据请求:
使用 XMLHttpRequest
对象从 ./js/trees.json
文件中获取数据。当请求成功后,将数据解析为 JSON 对象,并调用 parseTree
函数将数据转换为树形结构。调用 renderTree
函数将树形结构转换为 HTML 字符串,并插入到页面的 body
元素中。创建一个包含 iframe
的 div
元素,并添加到页面的 body
元素中。
let data;
let xhr = new XMLHttpRequest();
xhr.open('get', './js/trees.json', true);
xhr.send();
//回调函数,(放一些比较依赖data的代码)确保数据请求成功后在进行数据渲染
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
let text = xhr.responseText;
data = JSON.parse(text);
// 开始构建:调用parseTree函数,pid为0开始构建树结构,并将结果存到treeData里
let treeData = parseTree(data, 0);
// 渲染树形结构并插入到body中(相当于重新写了一下body)
document.body.innerHTML = renderTree(treeData);
// 创建右侧 iframe(插入的页面)
//1.创建一个div
let iframeDiv = document.createElement('div');
//2.给这个div起名
iframeDiv.className = 'right';
// 3.把iframe插入到这个div中,后续 通过点击的树形节点更新src
iframeDiv.innerHTML = '<iframe id="contentFrame" src="" frameborder="0"></iframe>';
//4.把创建好的div插入到body
document.body.appendChild(iframeDiv);
2.构建树形结构的函数
通过递归的方式将数据转换为树形结构,每个节点的子节点存储在 child
属性中。
function parseTree(data, pid)
:定义一个递归函数,用于将扁平的数据转换为树形结构。
let tree = [];
:创建一个空数组 tree
用于存储树形结构的数据。
for (let i in data)
:遍历数据数组。
if (data[i].pid === pid)
:如果当前数据的 pid
属性等于传入的 pid
,表示该数据是当前节点的子节点。
tree.push(data[i]);
:将该数据添加到 tree
数组中。
tree[tree.length - 1].child = parseTree(data, data[i].id);
:递归调用 parseTree
函数,查找当前数据的子节点,并将结果存储在 child
属性中。
function parseTree(data, pid) {
// 用来存储树状的数据
let tree = [];
for (let i in data) {
//如果数据的 pid 属性等于传入的 pid(父级的id)
//⭐子集的pid和父级的id是保持一致的,这样才能完成递归
if (data[i].pid === pid) {
tree.push(data[i]);
//递归调用parseTree,传data和当前数据的id作为pid(上面函数里是data和pid),为了查找当前数据的子集然后用child(子集)方法插入到当前数据后。
tree[tree.length - 1].child = parseTree(data, data[i].id);
}
}
// 返回构建的树形结构
return tree;
}
3.转化函数
根据树形结构生成 HTML 字符串,对于有子节点的菜单项,添加展开 / 收起的箭头图标。
function renderTree(data)
:定义一个函数,用于将树形结构转换为 HTML 字符串。
data
是树形结构的数据。
let str = '<div class="left">';
:初始化一个字符串 str
,用于存储 HTML 代码。
for (let i in data)
:遍历树形结构的数据。
if (data[i].child.length > 0)
:如果当前元素有子节点,则生成包含子节点的 HTML 代码。str += ...
:拼接 HTML 代码,包括左侧的图片、节点名称和向下箭头图标。
str += renderTree(data[i].child);
:递归调用 renderTree
函数,生成子节点的 HTML 代码。str += '</div></div>';
:拼接结束标签。
else
:如果当前元素没有子节点,则生成普通的 HTML 代码。
str += '</div>';
:拼接结束标签。return str;
:返回生成的 HTML 字符串。
function renderTree(data) {
// 这个div是左边菜单的底板
let str = '<div class="left">';
for (let i in data) {
//如果当前数据有子节点的话,就为该数据生成一个包含子节点的html
if (data[i].child.length > 0) {
str +=
`<div class="textBox">
<div class="content">
<img class="imgLeft" src="./img/圣诞礼物.png" width="20px" />
${data[i].name}
<img class="imgBot" src="./img/向下箭头.png" width="20px" height="20px"/>
</div>
<div class="box" style="display:none;">`;
//递归调用renderTree函数,生成当前节点的子节点(只有在有子节点的条件下才递归,因为要去检查子节点有没有子节点,没有的话就结束)
str += renderTree(data[i].child);
//拼结束标签
str += '</div></div>';
//如果当前数据没有子节点的话,就添加普通元素
} else {
str +=
`<div class="textBox">
<div class="content">
<img class="imgLeft" src="./img/圣诞礼物.png" width="20px" />
${data[i].name}
</div>
</div>`;
}
}
str += '</div>';
return str;
}
4.点击事件处理函数
监听 body
元素的点击事件,当点击菜单项时,切换子菜单的显示 / 隐藏状态。当点击叶子节点时,根据节点的 path
属性更新 iframe
的 src
属性,显示相应的内容。
document.body.addEventListener('click', function(e)
:为页面的 body
元素添加一个点击事件监听。
let target = e.target;
:获取点击事件的目标元素
while (target && !target.classList.contains('content'))
:向上查找父元素,直到找到具有 content
类名的元素。
if (target)
:如果找到了具有 content
类名的元素的话就let sibling = target.nextElementSibling;
:获取目标元素的下一个兄弟元素。
if (sibling && sibling.classList.contains('box'))
:如果兄弟元素具有 box
类名,说明它是子菜单。
sibling.style.display = sibling.style.display === 'none' ? 'block' : 'none';
:切换子菜单的显示状态,实现展开和收起的效果。
let name = target.textContent.trim();
:获取目标元素的文本内容,并去除首尾的空格。
let item = null;
:初始化一个变量 item
,用于存储匹配的元素。
for (let i = 0; i < data.length; i++)
:遍历 data
数组,查找与目标元素文本内容匹配的元素。
if (data[i].name === name)
:如果找到匹配的元素。
item = data[i];
:将匹配的元素存储在 item
变量中。break;
:跳出循环。
if (item && item.path)
:如果找到匹配的元素且该元素具有 path
属性。document.getElementById('contentFrame').src = item.path;
:将 iframe
的 src
属性设置为该元素的 path
属性值,从而在 iframe
中加载对应的页面。
document.body.addEventListener('click', function(e) {
//获取点击的目标元素(全局的)
let target = e.target;
//向上查找父元素,找到有content元素
while (target && !target.classList.contains('content')) {
target = target.parentElement;
}
//如果找到了
if (target) {
// 通过 target.nextElementSibling 获取 target 元素的下一个兄弟元素(即紧接着 .content 元素之后的元素)。
let sibling = target.nextElementSibling;
// 如果该兄弟元素有box元素
if (sibling && sibling.classList.contains('box')) {
// 展开或收起子节点
sibling.style.display = sibling.style.display === 'none' ? 'block' : 'none';
}
// 点击叶子节点,更新 iframe
//通过target.textContent获取到点击的目标元素文本
let name = target.textContent.trim();
let item = null;
//1.通过循环在data中找与name相匹配的数据
for (let i = 0; i < data.length; i++) {
// 2.如果相匹配
if (data[i].name === name) {
// 3.就存到item里
item = data[i];
break;
}
}
// 如果找到匹配的数据且path路径不为空,则更新iframe的src
if (item && item.path) {
document.getElementById('contentFrame').src = item.path;
}
}
});