npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tni/tree

v2.0.0

Published

TNI 项目树结构工具库

Readme

@tni/tree

通用树结构工具库,提供数组转树、树转数组、节点查找、路径查找、筛选、插入、删除、移动、层级修复等能力。

适合处理:

  • 菜单树、组织架构、分类树
  • 表格树、级联选择器、目录树
  • 依赖 id / parentId / children 模式的前后端数据转换
  • jsTree 一类“只有 level 没有 children”的线性树数据修复

安装

vp add @tni/tree

导出内容

import {
  SKIP_CHILDREN,
  STOP_TRAVERSAL,
  arrayToTree,
  treeToArray,
  traverseTree,
  findTreeNode,
  findTreeNodes,
  findTreePath,
  findTreePaths,
  filterTree,
  insertNodeBefore,
  insertNodeAfter,
  removeTreeNode,
  removeTreeNodes,
  updateTreeNode,
  moveTreeNode,
  getTreeMaxDepth,
  getNodeChain,
  getSiblingNodes,
  getChildNodes,
  getLeafNodes,
  traverseLeafNodes,
  setTreeLevelAndLeaf,
  getAncestorNode,
  countLeafNodes,
  repairJSTree,
  supplementJSTreeLevel,
  getArrayChildren,
  getArrayParent,
  findNodeById,
  createTreeHelper,
  type TreeConfig,
  type TreeVisitContext,
} from "@tni/tree";

快速开始

import { arrayToTree, findTreePath, getLeafNodes, treeToArray } from "@tni/tree";

type Node = {
  id: number;
  parentId: number | null;
  name: string;
  children?: Node[];
};

const list: Node[] = [
  { id: 1, parentId: null, name: "华东" },
  { id: 2, parentId: 1, name: "上海" },
  { id: 3, parentId: 1, name: "杭州" },
  { id: 4, parentId: 3, name: "西湖区" },
];

const tree = arrayToTree(list);
const path = findTreePath(tree, 4).map((item) => item.name);
const leaves = getLeafNodes(tree).map((item) => item.name);
const flat = treeToArray(tree);

设计约定

使用前建议先了解这几个约定,避免误判行为。

1. 默认字段名

默认把节点看作以下结构:

{
  id: unknown;
  parentId: unknown;
  children: T[];
  level: number;
  isLeaf: boolean;
}

默认配置如下:

| 配置项 | 默认值 | | ------------------ | ------------------- | | idKey | "id" | | parentIdKey | "parentId" | | childrenKey | "children" | | levelKey | "level" | | isLeafKey | "isLeaf" | | rootParentValues | [undefined, null] |

2. 大多数“改结构”的方法都不会原地修改入参

以下方法会返回新的树或数组副本:

  • arrayToTree
  • treeToArray
  • filterTree
  • insertNodeBefore
  • insertNodeAfter
  • removeTreeNode
  • removeTreeNodes
  • updateTreeNode
  • moveTreeNode
  • setTreeLevelAndLeaf
  • repairJSTree
  • supplementJSTreeLevel

以下方法主要做遍历或查询,返回的是原树中的节点引用:

  • findTreeNode
  • findTreeNodes
  • findTreePath
  • findTreePaths
  • findNodeById
  • getSiblingNodes
  • getChildNodes
  • getLeafNodes
  • getAncestorNode
  • getArrayChildren
  • getArrayParent

3. 节点 id 应保持唯一

库内部很多方法依赖 id 来匹配节点。如果同一棵树里有重复 id,多数行为都会变得不可靠,例如:

  • arrayToTree 可能把后面的同 id 节点覆盖前面的节点
  • findNodeById / moveTreeNode / removeTreeNode 可能命中错误节点

4. 根节点的 parentId

默认认为 undefinednull 都是合法根节点标记。

  • arrayToTree 遇到 parentId 是根值时会把节点作为根节点
  • treeToArray 会把根节点的 parentId 规范化为 rootParentValues[0]
  • moveTreeNode 把节点移动到根层时,也会写入 rootParentValues[0]

如果你的项目约定根节点必须是 0"root" 或其他值,请通过 rootParentValues 显式配置。

自定义字段

如果你的字段名不是 id / parentId / children,建议用 createTreeHelper 先生成一组绑定配置的方法。

import { createTreeHelper } from "@tni/tree";

type MenuNode = {
  key: string;
  parentKey: string | null;
  label: string;
  nodes?: MenuNode[];
};

const tree = createTreeHelper<MenuNode>({
  idKey: "key",
  parentIdKey: "parentKey",
  childrenKey: "nodes",
});

const menuTree = tree.arrayToTree([
  { key: "dashboard", parentKey: null, label: "Dashboard" },
  { key: "analysis", parentKey: "dashboard", label: "Analysis" },
]);

常用配置项

所有 API 都支持 TreeConfig,部分方法还会有自己的额外选项。

| 配置项 | 类型 | 说明 | | ------------------ | ----------- | -------------------------- | | idKey | string | 节点 id 字段名 | | parentIdKey | string | 父节点 id 字段名 | | childrenKey | string | 子节点数组字段名 | | levelKey | string | 层级字段名 | | isLeafKey | string | 叶子节点标记字段名 | | rootParentValues | unknown[] | 被视为根节点的父 id 值集合 |

API 说明

数据转换

arrayToTree(items, options?)

把扁平数组转换成树结构。

额外选项:

| 选项 | 类型 | 默认值 | 说明 | | ----------------------- | --------- | ------ | -------------------------------------- | | preserveOrphansAsRoot | boolean | true | 找不到父节点的孤儿节点是否提升为根节点 |

const tree = arrayToTree(list, {
  preserveOrphansAsRoot: false,
});

treeToArray(tree, options?)

把树结构拍平成数组,默认会删除每个节点的 children 字段。

额外选项:

| 选项 | 类型 | 默认值 | 说明 | | -------------- | --------- | ------- | ---------------------------------------- | | keepChildren | boolean | false | 扁平化后是否保留节点上的 children 字段 |

const flat = treeToArray(tree);

createTreeHelper(config?)

返回一组已经绑定字段配置的 helper,适合项目里统一复用。

查询与遍历

traverseTree(tree, visitor, config?)

深度优先遍历整棵树。

visitor 第二个参数是 TreeVisitContext

| 字段 | 说明 | | ----------- | --------------------------- | | parent | 当前节点父节点 | | ancestors | 当前节点所有祖先节点 | | path | 从根到当前节点的完整路径 | | depth | 当前深度,根节点从 1 开始 | | index | 当前节点在兄弟节点中的索引 | | siblings | 当前层全部兄弟节点 |

可配合两个控制常量:

  • SKIP_CHILDREN: 跳过当前节点的所有子节点
  • STOP_TRAVERSAL: 立即终止整棵树遍历
traverseTree(tree, (node) => {
  if (node.disabled) return SKIP_CHILDREN;
  if (node.id === 100) return STOP_TRAVERSAL;
});

findTreeNode(tree, matcher, config?)

查找第一个匹配节点。

findTreeNodes(tree, matcher, config?)

查找所有匹配节点。

findTreePath(tree, matcher, config?)

返回第一个匹配节点从根到当前节点的路径。

findTreePaths(tree, matcher, config?)

返回所有匹配节点的路径集合。

findNodeById(tree, id, config?)

按 id 查找节点,等价于 findTreeNode(tree, id, config)

getNodeChain(tree, matcher, config?)

返回节点链,当前实现等价于 findTreePath

结构筛选与变更

filterTree(tree, predicate, options?)

筛选树结构。只要当前节点命中,或者子树里存在命中节点,就会保留当前节点。

额外选项:

| 选项 | 类型 | 默认值 | 说明 | | ------------------------------- | --------- | ------- | ------------------------------------------ | | includeDescendantsWhenMatched | boolean | false | 当前节点命中后,是否直接保留它的完整后代树 |

const visibleTree = filterTree(tree, (node) => node.name.includes(keyword), {
  includeDescendantsWhenMatched: true,
});

insertNodeBefore(tree, oldNode, newNode, config?)

在目标节点前插入新节点。

insertNodeAfter(tree, oldNode, newNode, config?)

在目标节点后插入新节点。

removeTreeNode(tree, matcher, config?)

删除第一个匹配节点。

removeTreeNodes(tree, matcher, config?)

删除所有匹配节点。

updateTreeNode(tree, matcher, updater, config?)

更新第一个匹配节点。

updater 可以是对象,也可以是函数:

const nextTree = updateTreeNode(tree, 4, (node) => ({
  name: `${node.name}-updated`,
  status: "active",
}));

moveTreeNode(tree, source, target, options?)

移动节点位置。

额外选项:

| 选项 | 类型 | 默认值 | 说明 | | ---------- | --------------------------------- | ---------- | --------------------------------------------------- | | position | "before" \| "after" \| "inside" | "inside" | 插入到目标节点前、后或内部 | | insertAt | "start" \| "end" | "end" | 当 position === "inside" 时插入到子节点头部或尾部 |

说明:

  • 不允许把节点移动到它自己的后代内部
  • 把节点移动到根层时,会把 parentId 写成 rootParentValues[0]
const moved = moveTreeNode(tree, 4, 5, {
  position: "inside",
  insertAt: "start",
});

关系查询

getSiblingNodes(tree, matcher, options?)

获取兄弟节点。

额外选项:

| 选项 | 类型 | 默认值 | 说明 | | ------------- | --------- | ------- | -------------------------------- | | includeSelf | boolean | false | 是否把当前节点本身也包含在结果里 |

getChildNodes(tree, matcher, options?)

获取子节点。

额外选项:

| 选项 | 类型 | 默认值 | 说明 | | ------ | --------- | ------- | ------------------------ | | deep | boolean | false | 是否递归返回全部后代节点 |

getAncestorNode(tree, matcher, options?)

获取祖先节点。

额外选项:

| 选项 | 类型 | 默认值 | 说明 | | ---------- | -------- | ------ | -------------------------------------- | | distance | number | 1 | 距离当前节点第几层祖先,1 表示父节点 |

getArrayChildren(items, parent, config?)

在扁平数组里获取某个节点的直接子节点。

getArrayParent(items, node, config?)

在扁平数组里获取某个节点的父节点。

叶子节点与统计

getLeafNodes(tree, config?)

获取所有叶子节点。

traverseLeafNodes(tree, visitor, config?)

只遍历叶子节点。

countLeafNodes(tree, config?)

统计叶子节点数量。

getTreeMaxDepth(tree, config?)

获取树最大深度,根节点深度是 1

setTreeLevelAndLeaf(tree, options?)

为每个节点补齐层级和叶子标记。

额外选项:

| 选项 | 类型 | 默认值 | 说明 | | ------------ | -------- | ------ | -------------- | | startLevel | number | 1 | 根节点起始层级 |

const markedTree = setTreeLevelAndLeaf(tree, {
  startLevel: 1,
});

jsTree 线性树辅助

这两个方法用于处理一类特殊数据:节点没有 children,只靠数组顺序和 level 表示层级。

例如:

[
  { id: 1, level: 1, parentId: null, name: "root" },
  { id: 2, level: 4, parentId: null, name: "child-a" },
  { id: 3, level: 2, parentId: null, name: "child-b" },
];

repairJSTree(items, options?)

修正 level / parentId / isLeaf,让线性树数据变成自洽状态。

额外选项:

| 选项 | 类型 | 默认值 | 说明 | | ------------ | -------- | ------ | ------------ | | startLevel | number | 1 | 最小起始层级 |

修正规则:

  • 第一项层级至少为 startLevel
  • 后一个节点层级最多只允许比前一个节点深一层
  • 会自动重算 parentId
  • 会自动补齐 isLeaf

supplementJSTreeLevel(items, targetLevel, options?)

先执行 repairJSTree,再把整棵线性树整体平移到指定起始层级。

适合把一段局部树挂到更深的层级上下文中。

matcher 支持哪些写法

多数查找类和结构变更类 API 都使用 matcher 参数,它支持 4 种形式:

1. 直接传 id

findTreeNode(tree, 10);
removeTreeNode(tree, "menu:dashboard");

2. 直接传节点对象

默认会按配置中的 idKey 比较 id,而不是要求对象引用必须相同。

moveTreeNode(tree, { id: 4 } as Node, 5);

3. 传 predicate 函数

findTreeNodes(tree, (node, context) => {
  return context.depth === 3 && node.name.startsWith("leaf");
});

4. 在扁平数组辅助方法中也可用 predicate

const parent = getArrayParent(list, (_node, context) => context.index === 3);

推荐用法

场景 1:后端返回扁平数组,前端组件需要树

const tree = arrayToTree(apiList);

场景 2:前端编辑了一棵树,提交给后端前要拍平

const payload = treeToArray(editedTree);

场景 3:搜索树节点并保留祖先链

const filtered = filterTree(tree, (node) => node.name.includes(keyword));

场景 4:给 UI 组件补 level / isLeaf

const uiTree = setTreeLevelAndLeaf(tree);

场景 5:项目里字段名不是默认值

const helper = createTreeHelper({
  idKey: "key",
  parentIdKey: "parentKey",
  childrenKey: "nodes",
});

边界说明

  • arrayToTree 默认会把找不到父节点的孤儿节点提升为根节点;如果不需要,传 preserveOrphansAsRoot: false
  • treeToArray 根节点的 parentId 会被归一化为 rootParentValues[0]
  • getChildNodes(tree, matcher, { deep: true }) 返回的是全部后代的扁平列表,不是子树
  • removeTreeNode / updateTreeNode 只处理第一个命中的节点;如果要批量处理,请用 removeTreeNodes 或自行 filterTree
  • moveTreeNode 不允许把节点移动到自己的后代节点下,否则会直接返回原树副本

测试覆盖

当前仓库已覆盖以下行为:

  • 所有公开方法的主路径调用
  • 遍历控制信号 SKIP_CHILDREN / STOP_TRAVERSAL
  • 扁平数组辅助方法的 predicate 匹配
  • treeToArray 的根节点 parentId 归一化
  • moveTreeNodebefore / after / inside 分支与非法后代移动保护