@tni/tree
v2.0.2
Published
通用树结构工具集,支持数组转树、遍历、查找、过滤、移动与层级修复。
Readme
@tni/tree
通用树结构工具集,面向“普通对象 + 可配置字段名”的数据模型,提供数组转树、树转数组、遍历、查询、过滤、移动、层级修复与 helper 预绑定能力。
设计目标
- 结构可适配:通过
TreeConfig自定义idKey、parentIdKey、childrenKey、levelKey、isLeafKey,不强绑业务字段名。 - API 语义稳定:对外保持单一入口,内部按基础设施、查询、变更、转换拆分,便于维护和扩展。
- 尽量纯函数:大多数 API 返回新的树或数组副本,避免对传入数据产生隐式修改。
- 查询与变更分层:查找、遍历、兄弟节点、祖先节点等只读能力与插入、删除、移动、更新等写操作分开维护。
- 兼容平铺层级数据:提供
repairJSTree、supplementJSTreeLevel处理层级不稳定或来自 jsTree 风格的数据。
目录结构
src/
index.ts
types.ts
tree.ts
core/
config.ts
node.ts
match.ts
visit.ts
operations/
transform.ts
query.ts
mutation.ts
array.ts
tests/
transform.test.ts
query.test.ts
mutation.test.ts
helper.test.ts快速开始
import {
arrayToTree,
createTreeHelper,
filterTree,
findTreePath,
moveTreeNode,
setTreeLevelAndLeaf,
treeToArray,
} from "@tni/tree";
const rows = [
{ id: 1, parentId: null, name: "Root" },
{ id: 2, parentId: 1, name: "Design" },
{ id: 3, parentId: 1, name: "Engineering" },
{ id: 4, parentId: 2, name: "UI" },
];
const tree = arrayToTree(rows);
const path = findTreePath(tree, 4);
const filtered = filterTree(tree, (node) => node.name.includes("Design"));
const annotated = setTreeLevelAndLeaf(tree);
const moved = moveTreeNode(tree, 4, 3, { position: "inside" });
const flat = treeToArray(tree);核心 API
| 方法名 | 语义 | 入参 | 出参 |
| ----------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ---------------- |
| arrayToTree | 将平铺数组按 id / parentId 关系组装成树结构 | items: readonly T[], options?: ArrayToTreeOptions<T> | T[] |
| treeToArray | 将树结构以前序遍历方式拍平成数组,并回填 parentId | tree: readonly T[], options?: TreeToArrayOptions<T> | T[] |
| filterTree | 按谓词过滤树,保留命中节点及其必要祖先链 | tree: readonly T[], predicate: TreePredicate<T>, options?: FilterTreeOptions<T> | T[] |
| findTreeNode | 查找首个命中的树节点 | tree: readonly T[], matcher: TreeMatch<T>, config?: TreeConfig<T> | T \| undefined |
| findTreeNodes | 查找全部命中的树节点 | tree: readonly T[], matcher: TreeMatch<T>, config?: TreeConfig<T> | T[] |
| findTreePath | 查找首个命中节点从根到当前节点的路径 | tree: readonly T[], matcher: TreeMatch<T>, config?: TreeConfig<T> | T[] |
| findTreePaths | 查找全部命中节点各自的路径 | tree: readonly T[], matcher: TreeMatch<T>, config?: TreeConfig<T> | T[][] |
| traverseTree | 深度优先遍历整棵树,可配合 SKIP_CHILDREN / STOP_TRAVERSAL 控制流程 | tree: readonly T[], visitor: TreeVisitor<T>, config?: TreeConfig<T> | void |
| traverseLeafNodes | 只遍历叶子节点 | tree: readonly T[], visitor: TreeVisitor<T>, config?: TreeConfig<T> | void |
| getSiblingNodes | 获取目标节点的同级节点 | tree: readonly T[], matcher: TreeMatch<T>, options?: GetSiblingNodesOptions<T> | T[] |
| getChildNodes | 获取目标节点的直接子节点或全部后代节点 | tree: readonly T[], matcher: TreeMatch<T>, options?: GetChildNodesOptions<T> | T[] |
| getAncestorNode | 获取目标节点指定距离的祖先节点 | tree: readonly T[], matcher: TreeMatch<T>, options?: GetAncestorNodeOptions<T> | T \| undefined |
| getLeafNodes | 获取整棵树中的全部叶子节点 | tree: readonly T[], config?: TreeConfig<T> | T[] |
| getNodeChain | 获取目标节点的节点链,本质上等同于路径查询 | tree: readonly T[], matcher: TreeMatch<T>, config?: TreeConfig<T> | T[] |
| getTreeMaxDepth | 获取树的最大深度 | tree: readonly T[], config?: TreeConfig<T> | number |
| countLeafNodes | 统计叶子节点数量 | tree: readonly T[], config?: TreeConfig<T> | number |
| findNodeById | 按节点 id 快速查找节点 | tree: readonly T[], id: string \| number \| symbol, config?: TreeConfig<T> | T \| undefined |
| insertNodeBefore | 在目标节点前插入新节点 | tree: readonly T[], oldNode: TreeMatch<T>, newNode: T, config?: TreeConfig<T> | T[] |
| insertNodeAfter | 在目标节点后插入新节点 | tree: readonly T[], oldNode: TreeMatch<T>, newNode: T, config?: TreeConfig<T> | T[] |
| removeTreeNode | 删除首个命中的节点 | tree: readonly T[], matcher: TreeMatch<T>, config?: TreeConfig<T> | T[] |
| removeTreeNodes | 删除全部命中的节点 | tree: readonly T[], matcher: TreeMatch<T>, config?: TreeConfig<T> | T[] |
| updateTreeNode | 更新首个命中的节点 | tree: readonly T[], matcher: TreeMatch<T>, updater: TreeUpdater<T>, config?: TreeConfig<T> | T[] |
| moveTreeNode | 将源节点移动到目标节点前、后或内部 | tree: readonly T[], source: TreeMatch<T>, target: TreeMatch<T>, options?: MoveTreeNodeOptions<T> | T[] |
| getArrayChildren | 在平铺数组中查找某个父节点的直接子节点 | items: readonly T[], parent: TreeMatch<T>, config?: TreeConfig<T> | T[] |
| getArrayParent | 在平铺数组中查找某个节点的父节点 | items: readonly T[], node: TreeMatch<T>, config?: TreeConfig<T> | T \| undefined |
| setTreeLevelAndLeaf | 为树补齐层级字段和叶子节点标记 | tree: readonly T[], options?: SetTreeLevelOptions<T> | T[] |
| repairJSTree | 修复 jsTree 风格平铺数据中的层级、父子关系和叶子标记 | items: readonly T[], options?: RepairJSTreeOptions<T> | T[] |
| supplementJSTreeLevel | 将 jsTree 风格平铺数据的根层级整体平移到目标层级 | items: readonly T[], targetLevel: number, options?: SupplementJSTreeLevelOptions<T> | T[] |
| createTreeHelper | 预绑定一组 TreeConfig,返回同构的 helper 方法集合 | config?: TreeConfig<T> | TreeHelpers<T> |
简单用法与输出
以下示例共用同一份数据,输出做了适度简化,只保留关键字段。
type Node = {
id: number;
parentId: number | null;
name: string;
children?: Node[];
level?: number;
isLeaf?: boolean;
};
const rows: Node[] = [
{ id: 1, parentId: null, name: "Root" },
{ id: 2, parentId: 1, name: "Design" },
{ id: 3, parentId: 1, name: "Engineering" },
{ id: 4, parentId: 2, name: "UI" },
{ id: 5, parentId: 3, name: "API" },
];
const tree = arrayToTree(rows);转换类
const tree = arrayToTree(rows);
// => [
// {
// id: 1,
// name: "Root",
// children: [
// { id: 2, name: "Design", children: [{ id: 4, name: "UI", children: [] }] },
// { id: 3, name: "Engineering", children: [{ id: 5, name: "API", children: [] }] },
// ],
// },
// ]
const flat = treeToArray(tree);
// => [
// { id: 1, parentId: null, name: "Root" },
// { id: 2, parentId: 1, name: "Design" },
// { id: 4, parentId: 2, name: "UI" },
// { id: 3, parentId: 1, name: "Engineering" },
// { id: 5, parentId: 3, name: "API" },
// ]
const filtered = filterTree(tree, (node) => node.name.includes("Design"));
// => [
// {
// id: 1,
// children: [{ id: 2, name: "Design", children: [] }],
// },
// ]
const leveled = setTreeLevelAndLeaf(tree);
// => 根节点 { id: 1, level: 1, isLeaf: false }
// => UI 节点 { id: 4, level: 3, isLeaf: true }
const repaired = repairJSTree([
{ id: 10, parentId: null, name: "A", level: 1 },
{ id: 11, parentId: null, name: "B", level: 4 },
{ id: 12, parentId: null, name: "C", level: 2 },
]);
// => [
// { id: 10, level: 1, parentId: null, isLeaf: false },
// { id: 11, level: 2, parentId: 10, isLeaf: true },
// { id: 12, level: 2, parentId: 10, isLeaf: true },
// ]
const supplemented = supplementJSTreeLevel(
[
{ id: 10, parentId: null, name: "A", level: 1 },
{ id: 11, parentId: 10, name: "B", level: 2 },
],
3,
);
// => [
// { id: 10, level: 3, parentId: null },
// { id: 11, level: 4, parentId: 10 },
// ]查询类
const one = findTreeNode(tree, 4);
// => { id: 4, parentId: 2, name: "UI", children: [] }
const many = findTreeNodes(tree, (node) => node.name.includes("i"));
// => [{ id: 2, name: "Design" }, { id: 3, name: "Engineering" }, { id: 4, name: "UI" }]
const path = findTreePath(tree, 4);
// => [{ id: 1, name: "Root" }, { id: 2, name: "Design" }, { id: 4, name: "UI" }]
const paths = findTreePaths(tree, (node) => node.parentId === 1);
// => [
// [{ id: 1, name: "Root" }, { id: 2, name: "Design" }],
// [{ id: 1, name: "Root" }, { id: 3, name: "Engineering" }],
// ]
const chain = getNodeChain(tree, 5);
// => [{ id: 1, name: "Root" }, { id: 3, name: "Engineering" }, { id: 5, name: "API" }]
const siblings = getSiblingNodes(tree, 2);
// => [{ id: 3, name: "Engineering" }]
const children = getChildNodes(tree, 1);
// => [{ id: 2, name: "Design" }, { id: 3, name: "Engineering" }]
const descendants = getChildNodes(tree, 1, { deep: true });
// => [
// { id: 2, name: "Design" },
// { id: 4, name: "UI" },
// { id: 3, name: "Engineering" },
// { id: 5, name: "API" },
// ]
const ancestor = getAncestorNode(tree, 4);
// => { id: 2, name: "Design" }
const rootAncestor = getAncestorNode(tree, 4, { distance: 2 });
// => { id: 1, name: "Root" }
const leaves = getLeafNodes(tree);
// => [{ id: 4, name: "UI" }, { id: 5, name: "API" }]
const maxDepth = getTreeMaxDepth(tree);
// => 3
const leafCount = countLeafNodes(tree);
// => 2
const byId = findNodeById(tree, 3);
// => { id: 3, parentId: 1, name: "Engineering", children: [{ id: 5, name: "API" }] }const visited: string[] = [];
traverseTree(tree, (node) => {
visited.push(node.name);
});
// => ["Root", "Design", "UI", "Engineering", "API"]
const leafVisited: string[] = [];
traverseLeafNodes(tree, (node) => {
leafVisited.push(node.name);
});
// => ["UI", "API"]变更类
const beforeInserted = insertNodeBefore(tree, 3, { id: 6, parentId: 1, name: "QA" });
// => Root.children 顺序变为 ["Design", "QA", "Engineering"]
const afterInserted = insertNodeAfter(tree, 2, { id: 6, parentId: 1, name: "QA" });
// => Root.children 顺序变为 ["Design", "QA", "Engineering"]
const removedOne = removeTreeNode(tree, 4);
// => Design.children 变为 []
const removedMany = removeTreeNodes(tree, (node) => node.parentId === 1);
// => [{ id: 1, name: "Root", children: [] }]
const updated = updateTreeNode(tree, 5, { name: "Backend API" });
// => id=5 的节点变为 { id: 5, name: "Backend API" }
const movedInside = moveTreeNode(tree, 4, 3, { position: "inside" });
// => UI 从 Design.children 移到 Engineering.children
const movedBefore = moveTreeNode(tree, 3, 2, { position: "before" });
// => Root.children 顺序变为 ["Engineering", "Design"]平铺数组关系查询
const arrayChildren = getArrayChildren(rows, 1);
// => [{ id: 2, name: "Design" }, { id: 3, name: "Engineering" }]
const arrayParent = getArrayParent(rows, 4);
// => { id: 2, parentId: 1, name: "Design" }Helper
const helper = createTreeHelper<Node>({
idKey: "id",
parentIdKey: "parentId",
childrenKey: "children",
});
const helperTree = helper.arrayToTree(rows);
// => 与 arrayToTree(rows) 输出一致
const helperNode = helper.findTreeNode(helperTree, 5);
// => { id: 5, parentId: 3, name: "API", children: [] }
const helperMoved = helper.moveTreeNode(helperTree, 4, 3, { position: "inside" });
// => 与 moveTreeNode(tree, 4, 3, { position: "inside" }) 输出一致遍历控制
import { SKIP_CHILDREN, STOP_TRAVERSAL, traverseTree } from "@tni/tree";
traverseTree(tree, (node) => {
if (node.type === "lazy") {
return SKIP_CHILDREN;
}
if (node.type === "stop") {
return STOP_TRAVERSAL;
}
});验证
vp run tree#typecheck
vp test packages/tree
vp check许可证
MIT
