jQuery 递归渲染多级树形菜单

树形菜单是前端开发中高频且核心的交互组件,广泛应用于后台管理系统、文件目录导航、权限管理面板等场景。其核心价值在于高效组织层级化数据,通过折叠 / 展开的交互形式,让复杂的层级关系更易读、更易操作。

本文将从「底层原理」到「实战落地」,手把手教你用 jQuery + 原生 JavaScript 实现一个高可维护、高拓展性的树形菜单。不仅会讲解代码实现,还会拆解递归核心逻辑、优化交互体验、规避常见坑点,新手也能理解并灵活复用。

一、需求分析与技术选型

1. 核心需求(精准版)

  • 数据源:从 JSON 文件读取扁平化菜单数据(实际开发中可替换为后端接口)
  • 数据处理:将扁平化数据转化为多层级树形结构(核心难点)
  • 页面渲染:动态生成 DOM,区分「有子菜单」和「无子菜单」的样式 / 交互
  • 交互体验:
    • 父菜单点击:折叠 / 展开子菜单 + 箭头平滑旋转动画
    • 子菜单点击:弹窗展示菜单名称(可替换为业务逻辑,如路由跳转)
  • 样式适配:基础美化 + hover 反馈 + 层级缩进清晰

2. 技术选型(附选型理由)

技术 / 方案 选型理由
jQuery 简化 AJAX 请求、DOM 选择 / 操作,降低新手学习成本,兼容大部分项目场景
原生 JS 实现递归逻辑(数据处理 / 渲染),保证核心逻辑的轻量与灵活
CSS3 transition 实现箭头旋转动画,提升交互流畅度,无额外 JS 开销
JSON 扁平化数据存储,符合后端接口返回的常见格式,贴近真实开发场景

二、实现步骤(深度拆解版)

步骤 1:准备标准化菜单数据源

真实开发中,后端返回的菜单数据多为「扁平化结构」(便于数据库存储和查询),核心字段包含:

  • id:菜单唯一标识(主键)
  • name:菜单显示名称
  • pid:父菜单 ID(顶级菜单 pid=0,无父级)

创建 tree-menu.json 文件(规范命名,避免中文空格):

json

复制代码
[
    {"id":1,"name":"首页管理","pid":0},
    {"id":2,"name":"轮播图设置","pid":1},
    {"id":3,"name":"公告管理","pid":1},
    {"id":4,"name":"用户管理","pid":0},
    {"id":5,"name":"普通用户","pid":4},
    {"id":6,"name":"管理员账户","pid":4},
    {"id":7,"name":"系统设置","pid":0},
    {"id":8,"name":"权限配置","pid":7}
]

注意:JSON 格式必须严格(无末尾逗号、引号为双引号),否则会导致 AJAX 请求解析失败。

步骤 2:搭建高可维护的 HTML 结构

HTML 结构遵循「语义化 + 低耦合」原则,仅保留核心容器,样式与逻辑完全分离:

html

预览

复制代码
<!DOCTYPE html>
<html lang="zh-CN"> <!-- 改为中文,符合国内开发场景 -->
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>jQuery 树形菜单实战</title>
    <!-- 引入 jQuery(推荐使用 CDN,避免本地路径问题) -->
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
    <style>
        /* 基础重置 */
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: "Microsoft Yahei", sans-serif; /* 适配中文显示 */
        }
        /* 标题样式 */
        .tree-title {
            text-align: center;
            margin: 20px 0;
            color: #333;
        }
        /* 菜单容器 */
        .tree-container {
            font-size: 16px; /* 调整字号,提升可读性 */
            margin: 0 auto;
            width: 600px; /* 固定宽度,适配PC端;移动端可改为100% */
        }
        /* 菜单项样式 */
        .menu-item {
            margin: 5px 0;
        }
        /* 菜单标题行 */
        .menu-title {
            display: flex;
            align-items: center;
            gap: 8px; /* 箭头与文字间距,替代margin */
            padding: 10px 15px;
            cursor: pointer;
            border-radius: 6px;
            transition: background-color 0.2s ease; /* hover 平滑过渡 */
        }
        .menu-title:hover {
            background-color: #2e8b57; /* 海绿色,更柔和 */
            color: #fff;
        }
        /* 箭头图标 */
        .arrow-icon {
            width: 18px;
            height: 18px;
            transition: transform 0.3s ease;
            flex-shrink: 0; /* 防止图标被压缩 */
        }
        /* 子菜单容器 */
        .submenu-container {
            margin-left: 25px; /* 层级缩进,比原5%更精准 */
            display: none; /* 默认隐藏 */
        }
        /* 箭头旋转类 */
        .arrow-rotate {
            transform: rotate(180deg);
        }
    </style>
</head>
<body>
    <h1 class="tree-title">系统管理菜单</h1>
    <div class="tree-container"></div>

    <script>
        // JS 逻辑写在这里
    </script>
</body>
</html>

优化点:

  1. 新增 CSS 重置,避免浏览器默认样式干扰;
  2. 改用 gap 控制箭头与文字间距,更现代;
  3. 类名语义化(如 tree-container 替代 items),便于维护;
  4. 适配中文显示,调整字号提升可读性。

步骤 3:健壮的 AJAX 数据请求

jQuery 的 $.ajax 封装了请求逻辑,重点增加「异常处理」和「路径兼容」:

javascript

运行

复制代码
$(function () {
    // 封装请求函数,提升复用性
    function getMenuData() {
        $.ajax({
            url: './tree-menu.json', // 相对路径,适配不同部署环境
            type: 'GET', // 大写更规范
            dataType: 'json',
            timeout: 5000, // 新增超时设置,避免无限等待
            success: function(res) {
                console.log('菜单数据加载成功:', res);
                // 转换为树形结构
                const treeData = flatToTree(res, 0);
                // 渲染菜单
                renderTreeMenu(treeData);
            },
            error: function(xhr, status, error) {
                // 分类处理错误,便于排查
                let errorMsg = '';
                if (status === 'timeout') {
                    errorMsg = '请求超时,请检查网络';
                } else if (xhr.status === 404) {
                    errorMsg = 'JSON 文件不存在,请检查路径';
                } else {
                    errorMsg = `数据加载失败:${error}`;
                }
                console.error(errorMsg);
                // 页面友好提示
                $('.tree-container').html(`<div style="color: red; text-align: center;">${errorMsg}</div>`);
            }
        });
    }

    // 初始化请求
    getMenuData();
});

优化点:

  1. 封装请求函数,便于复用和后续拓展(如加加载动画);
  2. 新增超时设置和分类错误提示,提升健壮性;
  3. 页面友好提示,替代仅控制台输出,提升用户体验;
  4. 函数 / 变量名改为英文(如 flatToTree 替代 mentdata),符合开发规范。

步骤 4:吃透递归核心 ------ 扁平化转树形结构

这是树形菜单的底层核心,先理解递归逻辑再写代码:

递归原理
  1. 入口:传入所有扁平化数据 + 顶级菜单 pid=0
  2. 遍历:找到所有 pid 等于当前值的菜单,作为「当前层级菜单」;
  3. 递归:对每个「当前层级菜单」,再次调用函数,传入其 id 作为新的 pid,查找其子菜单;
  4. 终止:当某菜单无对应子菜单时,递归终止,返回空数组。
优化后的递归函数

javascript

运行

复制代码
/**
 * 扁平化数据转树形结构
 * @param {Array} flatData - 扁平化菜单数据
 * @param {Number} parentId - 父菜单ID
 * @returns {Array} 树形结构数据
 */
function flatToTree(flatData, parentId) {
    // 过滤 + 映射,替代for-in循环,更简洁高效
    return flatData.filter(item => item.pid === parentId).map(item => {
        // 递归查找子菜单,挂载到children属性(语义化)
        return {
            ...item, // 解构原属性
            children: flatToTree(flatData, item.id)
        };
    });
}

优化点:

  1. filter + map 替代 for-in 循环,代码更简洁、性能更优;
  2. 添加 JSDoc 注释,提升代码可读性和可维护性;
  3. 属性名改为 children(行业通用),替代 child
  4. 解构赋值保留原属性,避免直接修改原数据。

步骤 5:递归渲染 DOM(高性能版)

渲染逻辑同样用递归,但优化字符串拼接方式,减少 DOM 操作次数:

javascript

运行

复制代码
/**
 * 渲染树形菜单
 * @param {Array} treeData - 树形结构数据
 * @returns {String} 拼接好的HTML字符串
 */
function renderTreeHtml(treeData) {
    let html = '';
    treeData.forEach(item => {
        html += `<div class="menu-item">`;
        // 判断是否有子菜单
        if (item.children && item.children.length > 0) {
            // 有子菜单:带箭头,绑定点击事件
            html += `
                <div class="menu-title" data-id="${item.id}">
                    <img class="arrow-icon" src="./arrow-down.png" alt="展开/收起">
                    <span>${item.name}</span>
                </div>
                <div class="submenu-container">
                    ${renderTreeHtml(item.children)}
                </div>
            `;
        } else {
            // 无子菜单:无箭头,绑定点击事件
            html += `
                <div class="menu-title" data-id="${item.id}">
                    <span>${item.name}</span>
                </div>
            `;
        }
        html += `</div>`;
    });
    return html;
}

// 渲染菜单到页面
function renderTreeMenu(treeData) {
    const menuHtml = renderTreeHtml(treeData);
    $('.tree-container').html(menuHtml);
    // 绑定点击事件(事件委托,避免动态DOM绑定失效)
    bindMenuEvent();
}

优化点:

  1. 拆分渲染函数为 renderTreeHtml(拼接 HTML)和 renderTreeMenu(挂载 DOM),职责单一;
  2. forEach 替代 for...of,兼容性更好;
  3. 新增 data-id 属性,便于后续拓展(如获取菜单 ID);
  4. 事件委托绑定(见步骤 6),解决动态 DOM 事件失效问题。

步骤 6:高性能的交互事件绑定

放弃 onclick 内联事件,改用 jQuery 事件委托,提升性能和可维护性:

javascript

运行

复制代码
/**
 * 绑定菜单交互事件(事件委托)
 */
function bindMenuEvent() {
    // 父菜单折叠/展开事件
    $('.tree-container').on('click', '.menu-title', function() {
        const $this = $(this);
        const $submenu = $this.next('.submenu-container');
        const $arrow = $this.find('.arrow-icon');

        // 仅当有子菜单时,执行折叠/展开 + 箭头旋转
        if ($submenu.length > 0) {
            $submenu.toggle();
            $arrow.toggleClass('arrow-rotate');
        } else {
            // 无子菜单:获取菜单名称和ID,执行业务逻辑
            const menuName = $this.find('span').text();
            const menuId = $this.data('id');
            alert(`你点击了菜单:【${menuName}】,ID:${menuId}`);
            // 实际开发中可替换为:路由跳转、接口请求等
        }
    });
}

核心优化:

  1. 事件委托到静态容器 .tree-container,即使动态新增菜单,事件依然有效;
  2. 合并点击事件,区分有无子菜单的逻辑,减少函数数量;
  3. 获取菜单 ID,便于对接后端接口(真实场景必备);
  4. $this 缓存 jQuery 对象,避免重复 DOM 查询,提升性能。

三、进阶优化(加分项)

1. 常见问题排查(精准版)

问题现象 根因分析 解决方案
JSON 加载失败 1. 文件路径错误;2. JSON 格式不合法;3. 跨域(本地直接打开 HTML) 1. 检查路径是否为相对路径;2. 用 JSON 校验工具(如 JSON.cn)检查格式;3. 启动本地服务(如 Live Server)
菜单不渲染 1. 递归函数逻辑错误;2. 数据为空;3. DOM 选择器错误 1. 打印 treeData 检查树形结构;2. 确认 JSON 数据非空;3. 检查类名是否匹配
箭头不旋转 1. 图标未找到;2. CSS 类名错误;3. 无箭头元素时执行旋转 1. 检查图标路径;2. 核对 arrow-rotate 类名;3. 加 $arrow.length 判断
点击事件失效 1. 动态 DOM 用了静态事件绑定;2. 事件委托容器错误 1. 改用事件委托;2. 确认委托容器是静态 DOM(如 .tree-container

2. 功能拓展(企业级场景)

  1. 默认展开指定菜单

javascript

运行

复制代码
// 渲染后,展开ID为4的菜单(用户管理)
function expandSpecifiedMenu(menuId) {
    const $menu = $(`.menu-title[data-id="${menuId}"]`);
    $menu.next('.submenu-container').show();
    $menu.find('.arrow-icon').addClass('arrow-rotate');
}
// 在 renderTreeMenu 中调用
expandSpecifiedMenu(4);
  1. 添加菜单选中状态

css

复制代码
.menu-title.active {
    background-color: #1e90ff;
    color: #fff;
}

javascript

运行

复制代码
// 绑定选中事件
$('.tree-container').on('click', '.menu-title', function() {
    $(this).addClass('active').siblings('.menu-title').removeClass('active');
    // 其他逻辑...
});
  1. 防止 XSS 攻击:如果菜单名称来自用户输入,需转义 HTML 特殊字符:

javascript

运行

复制代码
function escapeHtml(str) {
    return str.replace(/&/g, '&amp;')
              .replace(/</g, '&lt;')
              .replace(/>/g, '&gt;')
              .replace(/"/g, '&quot;')
              .replace(/'/g, '&#039;');
}
// 渲染时调用
<span>${escapeHtml(item.name)}</span>

四、核心总结(深度版)

本文实现的树形菜单,核心价值在于「原理清晰 + 代码可维护 + 贴近实战」,核心要点:

  1. 数据层:用「递归 + filter/map」将扁平化数据转为树形结构,这是所有树形组件的底层逻辑;
  2. 渲染层:递归拼接 HTML 字符串,减少 DOM 操作次数,提升性能;
  3. 交互层:事件委托替代内联事件,解决动态 DOM 事件失效问题,同时降低耦合;
  4. 工程化:语义化命名、JSDoc 注释、错误分类处理,符合企业级开发规范。

这个实现方案不仅能满足基础需求,还能轻松拓展为「带选中状态、默认展开、权限控制」的企业级树形菜单。掌握递归逻辑和事件委托核心,你可以将这套思路迁移到 Vue/React 等框架中,实现跨框架复用。

五、完整代码包(附加价值)

为方便你直接使用,整理了完整的文件结构:

plaintext

复制代码
tree-menu/
├── index.html       // 核心页面
├── tree-menu.json   // 菜单数据
└── arrow-down.png   // 箭头图标(可自行替换)

可直接下载使用,无需修改路径,开箱即用。

相关推荐
闲蛋小超人笑嘻嘻2 小时前
Flexbox 属性总结
前端·css
TOPGUS2 小时前
谷歌将移除部分搜索功能:面对AI时代的一次功能精简策略
前端·人工智能·搜索引擎·aigc·seo·数字营销
运筹vivo@2 小时前
攻防世界: ics-05
前端·web安全·php
qq_338032922 小时前
Vue/JS项目的package.json文件 和java项目里面的pom文件
java·javascript·vue.js·json
不思念一个荒废的名字2 小时前
【黑马JavaWeb+AI知识梳理】Web前端开发 - Vue3 / ElementPlus
前端
月明长歌2 小时前
Selenium中隐式等待(Implicit Wait)和显式等待(Explicit Wait)的区别
前端·javascript·selenium
姜太小白2 小时前
【前端】JavaScript字符串执行方法总结
开发语言·前端·javascript
GIS之路2 小时前
GDAL 实现影像合并
前端·python·信息可视化
心易行者2 小时前
Claude Code + Chrome:浏览器层面的AI编程新范式已至
前端·chrome·ai编程