☰
在原生 JavaScript 中使用 @pierre/trees:从挂载文件树到路径增删改查的完整配方
2026/10/10 1:50:22 网站建设 项目流程

【免费下载链接】pierre

pierre’s open source code

项目地址:https://gitcode.com/gh_mirrors/pi/pierre
点击查看免费下载

@pierre/trees是一个「以路径为第一公民」的 Web 文件树组件库,其核心思想是:公开状态与回调全部使用规范的路径字符串,而非内部数值 ID。本指南围绕skills/trees/references/recipe-vanilla.md中的原生 JavaScript(Vanilla JS)配方展开,讲解如何在任意 HTML 页面中创建FileTree模型、将其挂载到带高度的容器元素、通过add/remove/move/resetPaths更新路径集合,以及何时调用cleanUp()释放资源。读完本文,你将掌握在不依赖 React 的前提下,用几行代码在普通页面中渲染出一个支持搜索、虚拟化滚动与交互的完整文件树。

一、核心配方概览:模型与挂载分离

Vanilla 用法的本质是「先建模型,再挂载渲染」。下面这段代码取自packages/trees/README.md中的原生用法示例,也是本配方的基础骨架:

import { FileTree } from '@pierre/trees'; const mount = document.querySelector<HTMLElement>('#files'); if (mount == null) throw new Error('Missing file tree mount'); mount.style.height = '320px'; const tree = new FileTree({ paths: ['README.md', 'src/', 'src/index.ts'], initialExpansion: 'open', search: true, }); tree.render({ containerWrapper: mount });

与 React 入口@pierre/trees/react不同,根入口@pierre/trees导出的FileTree是一个命令式模型对象:构造函数只负责把路径列表解析进内部状态机,真正把 DOM 渲染到页面是由render({ containerWrapper })完成的。这种拆分让你可以在挂载之前准备好数据、在挂载之后通过模型方法随时增删路径,而视图会随之自动更新。

为什么必须给容器设置高度?

在示例中我们显式执行了mount.style.height = '320px'。这不是可有可无的装饰:FileTree内部采用虚拟化渲染(参见 virtualization.ts),视口高度决定了首屏渲染多少行。若容器高度为 0,树将没有可滚动的视口,行也无法正常显示。因此任何挂载点都必须拥有确定的高度(内联 style、CSS 类或min-height均可),这是本配方的最关键前提。

找不到挂载点时的防护

if (mount == null) throw new Error('Missing file tree mount')是一道必要的防线:render需要真实存在的HTMLElement,如果页面里没有#files元素,与其在渲染时报出晦涩错误,不如在创建模型前就快速失败,给出清晰提示。

二、构造选项详解:从最小配置到搜索

FileTree构造函数接收一个FileTreeOptions对象,其类型定义在 publicTypes.ts。上面配方用到了三个核心选项,下面逐一说明其含义与取值范围。

paths:路径列表(必填)

以字符串数组形式提供文件的相对路径。路径字符串会被规范化并解析出目录层级,因此你可以只写文件路径、只写目录路径(如'src/'),或两者混写。FileTree会自动为每个文件补齐其祖先目录。示例中的['README.md', 'src/', 'src/index.ts']会渲染出两行:README.md和src/(含展开后的src/index.ts)。

值得注意:最终排序与层级展开由底层的@pierre/path-store负责(见 FileTreeController.ts),而FileTree对外只暴露路径字符串 API——你可以完全不知道内部数值 ID 的存在。

initialExpansion:初始展开策略

类型为'closed' | 'open' | number(见 publicTypes.ts):

  • 'closed':所有目录默认折叠,只显示顶层;
  • 'open':所有目录默认全部展开(配方示例所用);
  • number:只展开到指定深度,适合目录很深的项目,避免首屏行数爆炸。

search:启用内置搜索框

search: true会在树内渲染一个搜索输入框,支持输入过滤。模型同时实现了FileTreeSearchSessionHandle接口,因此你可以在任意时刻通过tree.openSearch()、tree.setSearch(value)、tree.closeSearch()编程式地控制搜索会话(对应方法见 FileTree.ts)。更细粒度的搜索行为(如fileTreeSearchMode、searchBlurBehavior)不在本配方范围内,可参考 recipe-interactions.md。

其他常用选项一览

选项类型说明
flattenEmptyDirectoriesboolean将只含一个子项的空目录折叠进该子项行,如src/components/Button.tsx直接显示为一行
itemHeightnumber每行像素高度,参与虚拟化视口计算
initialVisibleRowCountnumber首屏行数预算提示,用于浏览器尚未测得真实视口前的初始渲染(允许小数,见 publicTypes.ts)
overscannumber视口外预渲染的行数余量,滚动更平滑
stickyFoldersboolean滚动时让目录行吸顶
sort'default' \| 比较函数自定义行排序

从源码看,FileTree构造函数会把这些选项拆分:视图相关选项(itemHeight、overscan、stickyFolders、initialVisibleRowCount)存入#viewOptions,其余行为选项转发给内部FileTreeController(见 FileTree.ts)。

三、路径更新:add / remove / move / resetPaths

配方指出:用add、remove、move或resetPaths来更新路径集合。这四个方法是FileTree对外暴露的变更句柄(FileTreeMutationHandle),其底层实现都委托给FileTreeController(见 FileTreeController.ts)。

// 新增一条路径(目录会自动补齐祖先) tree.add('src/utils/format.ts'); // 移动:from -> to tree.move('src/index.ts', 'src/main.ts'); // 删除(可配合 options 控制递归) tree.remove('src/', { recursive: true }); // 整体重置:一次性替换整个路径集合 tree.resetPaths(['README.md', 'docs/', 'docs/guide.md']); // 多条操作也可以打包成一次批量事务 tree.batch([ { type: 'add', path: 'LICENSE' }, { type: 'remove', path: 'CHANGELOG.md' }, ]);

各方法对应的类型定义:

  • add(path):新增路径;
  • remove(path, options?):FileTreeRemoveOptions支持{ recursive?: boolean }(publicTypes.ts);
  • move(fromPath, toPath, options?):FileTreeMoveOptions支持{ collision?: 'error' | 'replace' | 'skip' },即目标路径冲突时的碰撞策略(publicTypes.ts);
  • resetPaths(paths | { preparedInput }):支持两种重载——传入路径数组,或传入预先处理好的preparedInput(FileTree.ts);
  • batch(operations):FileTreeBatchOperation是add/remove/move的联合类型(publicTypes.ts)。

这些变更都是事件驱动的:FileTreeController内部基于@pierre/path-store的PathStore,路径更新会触发模型变更事件并自动重渲染,你无需手动调用render。仓库的 e2e 测试夹具file-tree-mutations.html就演示了resetPaths(initialPaths)的动态切换场景(见 file-tree-mutations.html)。

四、读取与导航:以路径为键的查询 API

路径不仅是写入的键,也是读取的键。FileTree提供了一组以路径字符串为参数的查询与导航方法(见 FileTree.ts):

// 查询 tree.getItem('src/index.ts'); // 返回 FileTreeItemHandle 或 null tree.getSelectedPaths(); // 当前选中的路径数组 tree.getFocusedPath(); // 当前聚焦的路径或 null tree.getVisibleRows(start, end); // 读取虚拟化视口内的行快照 // 导航 tree.focusPath('src/index.ts'); tree.scrollToPath('src/index.ts', { focus: false, offset: 'top' });

getItem(path)返回的FileTreeItemHandle是一个轻量句柄,提供select()、deselect()、toggleSelect()、focus()、getPath()等操作;目录句柄额外具备expand()、collapse()、toggle()、isExpanded()(publicTypes.ts)。scrollToPath的offset支持'top' | 'center' | 'nearest'(publicTypes.ts)。

这套 API 的底层由PathStoreVisibleTreeProjection支撑:控制器维护一棵可见树投影,getVisibleRows(start, end)正是从该投影中切片,因此即便路径总数巨大(源码注释提到可支撑 494k 行量级),查询与渲染也只接触当前窗口内的行(见 FileTreeController.ts)。

五、资源释放:cleanUp() 的正确时机

配方最后一条指令:当宿主移除树时调用cleanUp()。这个方法做的事情比「卸载 DOM」更多,其实现(FileTree.ts)包含三步:

public cleanUp(): void { this.unmount(); // 1. 卸载虚拟化包装器与宿主容器上的样式属性 this.#selectionSubscription?.(); // 2. 退订选择变更监听 this.#selectionSubscription = null; this.#controller.destroy(); // 3. 销毁内部 PathStore 控制器 }
  • unmount()会调用unmountFileTreeRoot(wrapper)移除渲染根节点,并清理本实例写入宿主上的密度相关 CSS 变量,让容器可被复用;
  • 退订onSelectionChange监听,避免悬挂回调;
  • destroy()销毁控制器及其底层的PathStore,释放事件订阅与内部状态。

典型使用场景是 SPA 中页面卸载或组件销毁时:

// 页面离开时释放 window.addEventListener('beforeunload', () => tree.cleanUp());

如果你只调用render而不管理生命周期,多实例反复挂载同一个容器时可能残留样式属性或重复订阅——这正是cleanUp()存在的意义。仓库的 profile 测试夹具file-tree-profile-main.ts中即可看到currentFileTree?.cleanUp()与render成对出现的模式(见 file-tree-profile-main.ts)。

六、大数据量优化:preparedInput 预备输入

配方没有提到但值得一补的实战优化:对于大列表或频繁整体重载的场景,@pierre/trees提供「预备输入」机制,将解析/排序工作前移,避免每次构造都重复计算(preparedInput.ts):

import { FileTree, preparePresortedFileTreeInput } from '@pierre/trees'; // 已知最终顺序已排好:跳过排序与重复解析 const paths = ['src/', 'src/index.ts', 'README.md']; const preparedInput = preparePresortedFileTreeInput(paths); const tree = new FileTree({ preparedInput }); // 之后整体刷新时复用同一个预备结果 tree.resetPaths({ preparedInput });

两个入口的取舍:

  • prepareFileTreeInput(paths, options?):处理原始(未排序)输入,可传flattenEmptyDirectories与自定义sort;
  • preparePresortedFileTreeInput(paths):标记输入已按最终顺序排好,跳过排序与解析。

二者都返回不透明句柄FileTreePreparedInput(带唯一符号标记),防止调用方手搓形状绕过 PathStore 预期的预处理(见 preparedInput.ts)。与之匹配的resetPaths重载可直接接收该句柄,实现「一次预备、多次复用」。

七、完整实战示例:一个可增删的迷你文件树

把以上知识点串起来,下面是一个可直接运行的最小完整示例——创建树、挂载、动态增删、并在卸载时清理:

import { FileTree } from '@pierre/trees'; const mount = document.querySelector<HTMLElement>('#files'); if (mount == null) throw new Error('Missing file tree mount'); mount.style.height = '320px'; const tree = new FileTree({ paths: ['README.md', 'src/', 'src/index.ts'], initialExpansion: 'open', search: true, }); tree.render({ containerWrapper: mount }); // 演示动态更新:把 README.md 移到 docs/ 下,再新增一条 tree.move('README.md', 'docs/README.md'); tree.add('src/utils/format.ts'); // 某个按钮触发整体重置 document.querySelector('#reset')?.addEventListener('click', () => { tree.resetPaths(['package.json', 'pnpm-lock.yaml']); }); // 宿主移除树时释放资源 window.addEventListener('beforeunload', () => tree.cleanUp());

八、进一步阅读

本配方聚焦 Vanilla JS 场景,@pierre/trees还提供其他入口与主题,可依需查阅:

  • React 配方:在 React 组件中使用useFileTree与<FileTree model={...} />
  • SSR 配方:服务端预加载声明式 Shadow DOM 标记
  • 主题配方:用themeToTreeStyles()把 Shiki / VS Code 主题翻译为树样式
  • 交互配方:搜索、重命名、拖拽与 git status 配置
  • Core API 参考:FileTreeOptions与各方法的完整类型定义
  • 包级总览:packages/trees/README.md,仓库根目录下另有技能索引 skills/INDEX.md

安装方式:pnpm add @pierre/trees(React 入口需额外安装react与react-dom)。运行包内测试可使用moonx trees:test、moonx trees:test-e2e等命令,详见 packages/trees/README.md 的 Development 一节。

【免费下载链接】pierre

pierre’s open source code

项目地址:https://gitcode.com/gh_mirrors/pi/pierre
点击查看免费下载
上一篇:KiCad符号库:电子设计的核心资源宝库
下一篇:youtube-dl-gui命令行接口隐藏功能:高级用户必备技巧

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询