
本文详解如何使用 d3.js 根据层级化 json 数据动态生成符合语义结构的响应式侧边导航栏,重点解决嵌套子菜单(如二级页面列表)的绑定与渲染问题,并提供可直接运行的完整示例代码。
本文详解如何使用 d3.js 根据层级化 json 数据动态生成符合语义结构的响应式侧边导航栏,重点解决嵌套子菜单(如二级页面列表)的绑定与渲染问题,并提供可直接运行的完整示例代码。
在现代前端开发中,将导航结构与数据解耦、实现声明式渲染是提升可维护性的关键。D3.js 不仅适用于可视化图表,其强大的数据绑定(data())与选择集(selection)机制,也使其成为构建动态 dom 结构(如导航菜单)的理想工具。本文将手把手带你用 D3 构建一个支持标题分组(header)和带下拉页单(item + hasPages)的两级侧边导航栏。
核心思路:按类型分支处理,避免嵌套 data() 链式错误
原始代码报错 Uncaught TypeError: undefined is not iterable 的根本原因在于:
.data(d => d.hasItems) // ❌ 字段名错误(应为 hasPages),且未做空值防护
D3 的 .data() 方法要求返回值必须是可迭代对象(Array)或 NulL/undefined;若 d.hasPages 不存在或为 undefined,则传入 undefined 会导致后续 .enter().append() 报错。
正确做法是:在顶层绑定主数据后,对每个
——这既保持逻辑清晰,又规避了嵌套 selectAll().data() 的陷阱。
完整实现代码(兼容 D3 v6/v7)
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <title>D3 侧边导航栏教程</title> <script src="https://d3js.org/d3.v7.min.js"></script> <style> .sidebar-nav { list-style: none; padding: 0; margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; } .sidebar-header { padding: 12px 20px; font-size: 0.875rem; font-weight: 600; color: #6c757d; text-transform: uppercase; letter-spacing: 0.5px; background-color: #f8f9fa; } .sidebar-item > a { display: block; padding: 10px 20px; color: #495057; text-decoration: none; transition: background-color 0.2s; } .sidebar-item > a:hover { background-color: #e9ecef; color: #343a40; } .sidebar-dropdown { list-style: none; padding-left: 0; margin: 0; } .sidebar-dropdown .sidebar-item > a { padding-left: 40px; font-size: 0.875rem; color: #6c757d; } </style> </head> <body> <ul class="sidebar-nav"></ul> <script> const sidenavData = [ { 'name': 'Header name 1', 'type': 'header' }, { 'name': 'Menu item 1', 'type': 'item', 'hasPages': [{ 'name': 'Page 1' }, { 'name': 'Page 2' }] }, { 'name': 'Menu item 2', 'type': 'item', 'hasPages': [{ 'name': 'Report 1' }, { 'name': 'Report 2' }, { 'name': 'Report 3' }] }, { 'name': 'Header name 2', 'type': 'header' }, { 'name': 'Notifications', 'type': 'item' }, { 'name': 'Messages', 'type': 'item' } ]; const nav = d3.select('ul.sidebar-nav'); // 绑定主数据,创建顶层 <li> nav.selectAll('li') .data(sidenavData) .enter() .append('li') .attr('class', d => d.type === 'header' ? 'sidebar-header' : 'sidebar-item') .each(function(d) { const li = d3.select(this); if (d.type === 'header') { // 标题:仅设置文本 li.text(d.name); } else if (Array.isArray(d.hasPages) && d.hasPages.Length > 0) { // 带下拉菜单的项:先添加链接,再嵌套子列表 const link = li.append('a') .attr('class', 'sidebar-link') .text(d.name); const dropdown = link.append('ul') .attr('class', 'sidebar-dropdown'); dropdown.selectAll('li') .data(d.hasPages) .enter() .append('li') .attr('class', 'sidebar-item') .append('a') .attr('class', 'sidebar-link') .text(page => page.name); } else { // 普通菜单项:仅添加链接 li.append('a') .attr('class', 'sidebar-link') .text(d.name); } }); </script> </body> </html>
关键要点与最佳实践
- ✅ 字段健壮性检查:使用 Array.isArray(d.hasPages) && d.hasPages.length > 0 替代简单 d.hasPages 判断,防止 null 或 undefined 导致崩溃;
- ✅ 语义化结构优先:导航使用
- /
- 保证无障碍访问(a11y)与 seo 友好,避免滥用
;
- ✅ CSS 解耦设计:所有样式通过类名控制,便于主题切换与组件复用;
- ⚠️ 不推荐深层嵌套:本方案适用于两级(header → item → [pages])。若需三级及以上(如 pages 再含 subpages),建议改用递归组件(React/Vue)或 D3 的 nest() + 自定义递归渲染函数;
- ? 动态更新提示:如需后续更新数据(如权限变更后刷新菜单),可调用 nav.selectAll(‘li’).data(newData).exit().remove() 并重新 .enter() 渲染,D3 会自动执行 enter/update/exit 三阶段更新。
通过以上实现,你已掌握用 D3 构建数据驱动导航的核心模式:以 each() 为逻辑分发器,按数据形态定制 DOM 结构,兼顾简洁性与健壮性。该模式可轻松扩展至面包屑、顶部导航、多级下拉等场景。
- 保证无障碍访问(a11y)与 seo 友好,避免滥用