樹形菜單是後台管理系統導航的標準組件。本文提供一個零依賴(僅需 jQuery + 簡單 CSS)的樹形菜單實現,採用遞歸構建 DOM、事件委派控制展開收縮、支持異步加載擴展,適合快速集成到項目中。

採用扁平數組 + pid 父子關係,便於後端查詢與前端遞歸構建:
var treeData = [
{ "id": "1", "pid": "0", "name": "頂級分類1" },
{ "id": "2", "pid": "0", "name": "頂級分類2" },
{ "id": "3", "pid": "0", "name": "頂級分類3" },
{ "id": "4", "pid": "1", "name": "分類1父1" },
{ "id": "5", "pid": "1", "name": "分類1父2" },
{ "id": "6", "pid": "2", "name": "分類2父1" },
{ "id": "7", "pid": "2", "name": "分類2父2" },
{ "id": "8", "pid": "3", "name": "分類3父1" },
{ "id": "9", "pid": "3", "name": "分類3父2" },
{ "id": "10", "pid": "4", "name": "分類1父1子1" },
{ "id": "11", "pid": "5", "name": "分類1父2子1" },
{ "id": "12", "pid": "6", "name": "分類2父1子1" },
{ "id": "13", "pid": "7", "name": "分類2父2子1" }
];
<div id="main">
<h1>樹形顯示關系圖 - 默認展開第一節點及其子節點</h1>
<ul id="browser" class="treeview"></ul>
</div>
$(document).ready(function () {
init();
});
// 入口:構建頂級節點並默認展開
function init() {
var $tree = $("#browser");
var firstId = "";
// 1. 構建頂級節點 (pid === "0")
for (var i = 0; i < treeData.length; i++) {
if (treeData[i].pid === "0") {
if (!firstId) firstId = treeData[i].id;
$tree.append(buildNodeHtml(treeData[i]));
}
}
// 2. 默認展開第一個頂級節點
if (firstId) {
expandNode(firstId);
// 可選:默認展開第一個子節點
var firstChildId = getFirstChildId(firstId);
if (firstChildId) expandNode(firstChildId);
}
}
// 構建單個節點 HTML(含展開圖標、標題、空子容器)
function buildNodeHtml(node) {
return "<li id='" + node.id + "' class='hasChildren expandable'>" +
"<span class='folder' onclick='expandNode(\"" + node.id + "\")'>" +
node.name + "</span><ul></ul></li>";
}
// 核心:節點展開/收縮邏輯
function expandNode(id) {
var $node = $("#" + id);
var cls = $node.attr("class");
// 情況 A:已加載子節點 → 切換展開/收縮狀態
if (cls !== "hasChildren expandable") {
$node.find("> ul").slideToggle(200);
$node.attr("class", cls === "expandable" ? "collapsable" : "expandable");
return;
}
// 情況 B:首次展開 → 動態加載子節點
var hasChildren = false;
for (var i = 0; i < treeData.length; i++) {
if (treeData[i].pid === id) {
hasChildren = true;
$node.find("> ul").append(buildNodeHtml(treeData[i]));
}
}
// 更新狀態:若無子節點則標記為葉子節點
if (hasChildren) {
$node.attr("class", "collapsable");
} else {
$node.attr("class", "leaf").find("> span").removeClass("folder").addClass("file");
}
}
// 輔助:獲取首個子節點 ID
function getFirstChildId(parentId) {
for (var i = 0; i < treeData.length; i++) {
if (treeData[i].pid === parentId) return treeData[i].id;
}
return null;
}
.treeview { margin: 0; padding: 0; list-style: none; }
.treeview li { margin: 0; padding: 0 0 0 20px; position: relative; }
.treeview .hasChildren > span.folder { cursor: pointer; background: url(folder.png) no-repeat 0 2px; padding-left: 20px; }
.treeview .expandable > span.folder { background-image: url(folder-closed.png); }
.treeview .collapsable > span.folder { background-image: url(folder-open.png); }
.treeview .leaf > span.file { background: url(file.png) no-repeat 0 2px; padding-left: 20px; }
.treeview ul { display: none; } /* 初始收縮 */
.treeview .collapsable > ul { display: block; } /* 展開狀態顯示 */
將 expandNode 中的數據遍歷替換為 AJAX 請求:
function expandNode(id) {
var $node = $("#" + id);
if ($node.hasClass("loaded")) { // 已加載標記
$node.find("> ul").slideToggle();
$node.toggleClass("expandable collapsable");
return;
}
$.getJSON("/api/tree/children", { parentId: id }, function(nodes) {
$.each(nodes, function(i, node) {
$node.find("> ul").append(buildNodeHtml(node));
});
$node.addClass("loaded collapsable").removeClass("expandable");
$node.find("> ul").show();
});
}
onclick 內聯改為 $(document).on('click', '.treeview .folder', handler),減少內存佔用,支持動態添加節點。react-virtualized 或原生 IntersectionObserver 按需渲染。localStorage,頁面刷新恢復用戶上下文。role="treeitem"、aria-expanded、鍵盤導航(←/→ 展開收縮,↑/↓ 選中)。這個實現邏輯清晰、擴展性強,通過 hasChildren expandable/collapsable/leaf 狀態機精確控制節點生命週期,是構建企業級導航菜單的優質起點。