【免费下载链接】pierre
pierre’s open source code
@pierre/trees是 pierre 仓库中一个以路径字符串为核心的文件树组件库(入口在 packages/trees/src/index.ts),它把树的模型(展开/折叠、选择、搜索、重命名、拖拽、Git 状态)与渲染层分离,底层由@pierre/path-store提供数据结构支撑。本文基于官方 Recipe 文档 recipe-interactions.md 展开,系统讲解如何为文件树开启搜索、重命名、拖拽与 Git 状态四类交互,并深入源码说明每个配置项的回调时机、校验规则与底层实现。读完本文,你将能直接照抄配置模板接入自己的应用,并理解这些交互在 FileTreeController 内部是如何被驱动与约束的。
安装与入口选择
在动手配置交互之前,先安装包并选择合适的入口:
pnpm add @pierre/trees如果应用使用 React 渲染层,还需安装react与react-dom。@pierre/trees提供多套入口(详见 SKILL.md):
| 入口 | 用途 |
|---|---|
@pierre/trees | 核心 API(FileTree类、控制器、工具函数) |
@pierre/trees/react | React 组件封装 |
@pierre/trees/ssr | 服务端预渲染 |
@pierre/trees/web-components | Web Components 封装 |
本文聚焦核心 API:FileTree类同时实现了FileTreeMutationHandle与FileTreeSearchSessionHandle两个公共接口,因此搜索、重命名、拖拽、Git 状态的所有方法都可以直接在树实例上调用。
交互配置总览:按需启用,一次到位
Recipe 的核心思想是「只启用产品实际暴露的交互」。下面这份配置把四类交互全部打开,并给出每个回调的落库/落盘位置:
const tree = new FileTree({ paths, search: true, renaming: { onRename(event) { renamePath(event.sourcePath, event.destinationPath); }, }, dragAndDrop: { canDrop({ target }) { return target.kind === 'directory'; }, onDropComplete(event) { saveMove(event); }, }, gitStatus, });对照源码可以看出,这份配置中search、renaming、dragAndDrop、gitStatus分别落入两条不同的处理路径:
- 控制器层:
search、renaming、dragAndDrop会进入 FileTreeController 的构造函数,被解析为#searchMode、#renameEnabled/#onRename、#dragAndDropConfig等私有状态; - 渲染层:
search、gitStatus会被 FileTree 类 提取出来,gitStatus经resolveFileTreeGitStatusState()预处理成按路径索引的状态 Map,search则决定是否渲染搜索输入框。
搜索:search: true与命令式打开
在构造选项中传入search: true即可启用内嵌搜索输入框(对应FileTree构造器中this.#searchEnabled = search === true,见 render/FileTree.ts)。
如果搜索框不是常驻 UI,而是由应用命令(如快捷键、工具栏按钮)触发,则调用实例方法openSearch():
// 从应用命令打开搜索,并预填初始关键字 tree.openSearch('TODO');FileTreeSearchSessionHandle接口(定义于 model/publicTypes.ts)提供了完整的搜索会话控制方法:
| 方法 | 作用 |
|---|---|
openSearch(initialValue?) | 打开搜索会话,可带初始关键字 |
closeSearch() | 关闭搜索会话并清除过滤 |
setSearch(value) | 直接设置/清空搜索词(null表示关闭) |
getSearchValue() | 读取当前搜索词 |
isSearchOpen() | 查询搜索会话是否处于打开状态 |
getSearchMatchingPaths() | 获取当前命中的路径列表 |
focusNextSearchMatch()/focusPreviousSearchMatch() | 在命中结果间移动焦点 |
搜索模式:三种过滤策略
树在搜索时的呈现策略由fileTreeSearchMode选项控制(类型定义见 model/publicTypes.ts),默认值是'hide-non-matches':
| 模式 | 行为 |
|---|---|
'expand-matches' | 展开所有命中路径的祖先目录,让结果可见 |
'collapse-non-matches' | 保留树结构,但折叠不含命中的目录 |
'hide-non-matches' | 隐藏不匹配的行(默认) |
从实现看,搜索时会建立匹配路径集合、可见路径集合等多组索引,并把用户手动折叠的目录记录为「折叠覆盖项」(#searchCollapsedOverrides),使搜索状态与用户的显式操作互不干扰(见 FileTreeController.ts)。搜索词不区分大小写,控制器会缓存小写化后的路径列表以避免每次击键都重新归一化(见 FileTreeController.ts)。
失焦行为与初始搜索
searchBlurBehavior:'close'(默认)在输入框失焦时立即清空并关闭搜索会话;'retain'保留当前查询与会话,直到显式关闭。'retain'适合挂在initialSearchQuery上的树——并发兄弟组件抢占焦点时过滤效果依然存活(类型注释见 model/publicTypes.ts)。initialSearchQuery:构造时直接注入初始搜索词。onSearchChange:搜索词变化回调,可用于同步外部状态。
重命名:renaming配置与startRenaming()
配置与事件回调
renaming: { canRename(item) { // 可选:控制哪些条目允许重命名 return !item.isFolder; // 例如:只允许文件 }, onRename(event) { // 重命名提交后回调,必须由应用落库 renamePath(event.sourcePath, event.destinationPath); }, onError(error) { // 可选:校验失败回调 showToast(error); }, },canRename({ isFolder, path }):返回false则该项不可进入重命名态。onRename(event):提交重命名后触发,event为FileTreeRenameEvent,包含sourcePath、destinationPath、isFolder三个字段(类型见 model/publicTypes.ts)。注意:树只负责计算新路径,真正的持久化(改磁盘、改仓库)由你在回调里完成。onError(error):名称校验失败时回调,错误信息由底层生成。
从菜单启动重命名
renaming: true只是允许重命名;要真正进入重命名态,还需要调用startRenaming():
// 从右键菜单/上下文菜单启动重命名(不传参时重命名当前聚焦项) tree.startRenaming('src/components/Button.tsx');startRenaming()返回布尔值表示是否成功进入重命名态(未启用重命名、路径不存在、canRename拒绝都会返回false)。从源码看(FileTreeController.ts),启动重命名时控制器会:
- 将路径解析为规范化路径并构造公开重命名路径;
- 自动展开所有折叠的祖先目录——否则重命名行永远无法挂载渲染(源码注释明确指出这能避免 React 重命名交接效果死循环旋转);
- 选中该条目并关闭正在进行的搜索会话;
- 以「叶子名称」预填重命名输入框,聚焦后进入内联编辑。
startRenaming(path, { removeIfCanceled: true })还支持「新建占位条目直接重命名、取消则删除」的流程——取消时若removeIfCanceled为真,会以递归方式移除对应条目(目录按recursive: true删除,见 FileTreeController.ts)。
内置校验规则
重命名路径计算由 renameFileTreePaths.ts 完成,它内置了这些校验,出错时通过onError通知:
- 名称不能为空;
- 名称不能包含
/; - 目标路径已存在(含目录重命名时目标前缀被占用)会报错;
- 未找到要重命名的条目会报错。
重命名语义是「同父目录改名」(same-parent basename rename):目录重命名会递归重写其下所有子路径;若新旧名称相同则视为无操作。控制器在回调onRename之后还会调用move()让内部 store 同步更新(见 FileTreeController.ts)。
拖拽:dragAndDrop配置与多选归一化
配置项
dragAndDrop: { canDrag(paths) { // 可选:决定这组路径能否开始拖拽 return paths.length > 0; }, canDrop({ target }) { // 目标校验:只允许拖到目录上 return target.kind === 'directory'; }, onDropComplete(event) { // 落位成功回调,应用在此持久化移动 saveMove(event); }, onDropError(error, event) { // 可选:落位失败回调 handleDropError(error); }, openOnDropDelay, // 可选:悬停目录自动展开的延迟(毫秒) },回调签名由FileTreeDragAndDropConfig定义(见 model/publicTypes.ts):
canDrop(event)接收FileTreeDropContext:{ draggedPaths, target };target为FileTreeDropTarget:{ kind: 'directory' | 'root', directoryPath, flattenedSegmentPath, hoveredPath }。kind: 'root'表示拖到树根(空白处),'directory'表示拖到某个目录行上;onDropComplete(event)接收FileTreeDropResult:{ draggedPaths, target, operation },其中operation为'move'(单个条目)或'batch'(多条目批量移动)。
多选拖拽的路径归一化
多选时同时拖动「文件夹和它的子孙」会造成重复移动。控制器通过normalizeDraggedPaths()去重:按路径长度排序后,只保留那些没有被选中祖先覆盖的最外层路径,保证每个子树恰好移动一次(实现见 model/dragAndDrop.ts)。
落位前的三重防线
拖拽从startDrag()到completeDrag()会经过层层校验(见 FileTreeController.ts):
- 自拖/拖入自身子孙检查:
isSelfOrDescendantDrop()会拒绝把目录拖进它自己或它的后代(dragAndDrop.ts); canDrop业务校验:返回false即拒绝该目标;- 批量预校验:多条目落位前先在一次性临时 store 上演练一遍
batch操作,避免中途冲突导致已部分写入(见 FileTreeController.ts)。
落位成功后,单条目走store.move(from, to),多条目走store.batch(operations);目标为目录时to: "dir/"被 PathStore 解释为「按源文件 basename 移入该目录」,因此拖拽层完全基于路径工作,无需自行拼接目标叶子路径(注释见 dragAndDrop.ts)。最后回调onDropComplete,由应用决定是否持久化。
Git 状态:gitStatus、setGitStatus()与applyGitStatusPatch()
初始化传入
gitStatus选项接收一组GitStatusEntry,每个条目包含路径与状态值,用于在树上渲染改动/忽略等装饰。例如:
const tree = new FileTree({ paths, gitStatus: [ { path: 'src/App.tsx', status: 'modified' }, { path: 'src/new/Module.ts', status: 'added' }, { path: 'node_modules/', status: 'ignored' }, ], });底层 model/gitStatus.ts 会把条目解析为规范化路径,并构建三组派生数据:按路径索引的状态 Map、包含改动的目录集合、被忽略的目录集合;同时为每个目录维护「子孙改动计数」(changeCountByDirectoryPath),供目录行展示汇总徽标。若传入空数组(签名'0')则整个 Git 状态置空。
仓库状态变化后同步
Recipe 给出的两条命令式更新路径,分别对应「整体替换」与「增量补丁」两种场景:
// 场景一:仓库状态大幅变化,整体替换 tree.setGitStatus(nextGitStatus); // 场景二:增量更新,只描述新增/移除的条目 tree.applyGitStatusPatch({ set: [{ path: 'src/new.ts', status: 'added' }], remove: ['src/old.ts'], });setGitStatus(entries):整体重建状态(render/FileTree.ts);applyGitStatusPatch(patch):增量应用{ set?, remove? }补丁(gitStatus.ts)。remove里的路径会连带递减其祖先目录的改动计数,某个目录计数归零后自动从「有改动目录」集合移除,保证徽标始终准确。
两条路径都会在状态签名变化时触发就地重渲染;通过签名比对,相同状态可以跳过无谓渲染(getGitStatusSignature/getGitStatusStateSignature)。
路径约定:目录带尾斜杠,文件不带
Recipe 最后强调的路径约定是整个组件库的公共契约(FileTreePublicId即字符串路径):
目录输入路径以
/结尾,文件输入路径不以/结尾。
// 目录路径 —— 必须以 / 结尾 'docs/' 'src/components/' // 文件路径 —— 不以 / 结尾 'src/index.ts' 'README.md'这条约定贯穿所有交互的判定逻辑:
- 重命名时
isCanonicalDirectoryPath(path)直接靠path.endsWith('/')区分目录与文件(renameFileTreePaths.ts); - 拖拽的「自拖检查」同样依赖尾斜杠判定目录(dragAndDrop.ts);
- Git 状态条目的路径归一化(
normalizeInputPath)也会把目录补上尾斜杠后再进入状态索引(gitStatus.ts); - 搜索的祖先展开、选择的范围计算、可见行投影等都以规范化后的路径为准。
同时,路径字符串是树的公共标识:getItem(path)、focusPath(path)、scrollToPath(path)、move(from, to)等所有公开 API 都接受这种格式(getItem也兼容src这种不带尾斜杠的目录查询写法,见 FileTreeController.ts)。
与模型无关的交互:关注点分离
值得强调的是,以上所有交互的状态都沉淀在 FileTreeController 中,而渲染层(React / vanilla / SSR / Web Components)只是消费它的快照。这意味着:
- 你可以在不挂载任何 UI 的情况下用控制器跑通「搜索 → 重命名 → 移动」的完整流程;
- 树实例暴露的
add/remove/move/batch/resetPaths等变异方法与onMutation事件(类型见 model/publicTypes.ts)是外部数据源同步树内容的官方通道; subscribe(listener)让任意渲染层都能以「订阅快照」的方式响应交互引起的状态变化。
更多阅读
- 分场景 Recipe:React 渲染、vanilla JS 渲染、SSR 预渲染、主题应用
- API 参考:Core API、React API、SSR API、Web Components API
- 源码探索:FileTreeController、交互相关类型定义、拖拽实现、Git 状态实现、重命名路径计算
【免费下载链接】pierre
pierre’s open source code
相关推荐
@pierre/trees 文件树组件全指南:安装、API 选型、React/SSR/Web Components 接入与交互配置
@pierre/trees 文件树组件全指南:安装、API 选型、React/SSR/Web Components 接入与交互配置 @pierre/trees
在 React 中使用 @pierre/trees 渲染与更新文件树:完整实战指南
在 React 中使用 @pierre/trees 渲染与更新文件树:完整实战指南 本篇技术指南围绕 @pierre/trees 的 React 集成方式展开,
@pierre/trees 文件树组件实战指南:path-first 设计、SSR 预渲染与主题定制
@pierre/trees 文件树组件实战指南:path first 设计、SSR 预渲染与主题定制 @pierre/trees 是 pierre 仓库中一个以
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考