
本文详解如何用 D3.js 基于嵌套 JSON 数据动态生成符合语义化结构的侧边栏导航菜单,重点解决 d.hasPages 嵌套数据绑定失败问题,并提供可直接运行的健壮实现方案。
本文详解如何用 d3.js 基于嵌套 json 数据动态生成符合语义化结构的侧边栏导航菜单,重点解决 `d.haspages` 嵌套数据绑定失败问题,并提供可直接运行的健壮实现方案。
在构建现代单页应用(SPA)或管理后台时,一个结构清晰、数据驱动的侧边栏导航(Side Navigation)至关重要。D3.js 虽以可视化见长,但其强大的数据绑定(data join)与 DOM 操作能力,同样适用于动态生成复杂 HTML 结构——尤其是具有层级关系的导航菜单。
核心挑战在于:如何正确绑定并渲染嵌套数据(如 hasPages 数组),避免 Uncaught TypeError: undefined is not iterable 错误? 该错误通常源于对 undefined 值调用 .data()(例如 d.hasItems 拼写错误或字段缺失),而 D3 v6+ 对非可迭代值更严格。
✅ 正确做法是:在父元素 .each() 中按类型分支处理,而非尝试全局统一绑定嵌套数据。这既保证逻辑清晰,又规避了无效数据绑定。
以下为完整、生产就绪的实现方案(兼容 D3 v5/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; }
.sidebar-header {
padding: 12px 20px;
font-weight: bold;
color: #666;
text-transform: uppercase;
font-size: 0.85em;
letter-spacing: 1px;
}
.sidebar-item > a {
display: block;
padding: 10px 20px;
text-decoration: none;
color: #333;
transition: background-color 0.2s;
}
.sidebar-item > a:hover { background-color: #f5f5f5; }
.sidebar-dropdown {
list-style: none;
padding-left: 20px;
margin: 0;
}
.sidebar-dropdown .sidebar-item > a {
padding-left: 30px;
font-size: 0.9em;
color: #555;
}
</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' }
];
// 1. 绑定顶层数据到 <ul> 的所有 <li>
const nav = d3.select('ul.sidebar-nav');
nav.selectAll('li')
.data(sidenavData)
.enter()
.append('li')
.attr('class', d => d.type === 'header' ? 'sidebar-header' : 'sidebar-item')
// 2. 使用 .each() 分支处理每种类型
.each(function(d) {
const li = d3.select(this);
if (d.type === 'header') {
// 标题项:仅设置文本
li.text(d.name);
}
else if (d.hasPages && 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');
// 关键:在此处绑定 hasPages 数据(确保 d.hasPages 存在且为数组)
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>? 关键要点与注意事项:
- d.hasPages 安全性检查:代码中显式判断 Array.isArray(d.hasPages) && d.hasPages.length > 0,防止因数据缺失或类型错误导致崩溃;
- 避免 .data(d.hasItems) 拼写错误:原始问题中误写为 d.hasItems(应为 d.hasPages),这是常见低级错误,务必核对字段名;
- .each() 是嵌套渲染的黄金法则:D3 的 .data().enter().append() 适用于扁平数据;对于嵌套结构,应在父元素回调中独立处理子数据,逻辑更可控;
- CSS 可扩展性强:示例中已提供基础样式,实际项目中可轻松集成 Bootstrap、Tailwind 或自定义主题;
-
后续增强方向:
- 添加点击展开/收起交互(监听 click + transition 动画);
- 支持路由跳转(为 添加 href 或绑定 router.push());
- 动态更新:利用 D3 的 update 模式响应数据变更(如权限过滤后重绘)。
该方案简洁、健壮、易维护,完美契合“数据即配置”的前端开发范式,是 D3 在非可视化场景中的典范应用。











