在开发思维导图类应用时,节点搜索功能往往不仅要能找到包含关键字的节点,还要在结果中清晰标出匹配片段,并按照某种逻辑把结果排好序,方便用户从最相关的内容开始浏览。用TypeScript来做这件事,核心难点不在于排序代码本身,而在于如何把“匹配结果”“高亮区间”“排序权重”这些概念用类型系统地描述出来,使后续的高亮组件和列表渲染组件都能依赖同一套契约工作。

匹配结果与高亮区间的基础类型设计
首先要明确一次搜索在单个节点上能产生什么。一个思维导图节点通常有唯一标识、展示文本和层级深度。当用户输入关键词后,我们得到的是若干“命中区间”,也就是文本中哪一段被子串匹配。为了支持高亮,命中区间必须记录起始与结束索引。用TypeScript可以定义如下结构,把节点信息和命中信息分离,便于复用。
下面的类型把单次命中抽象为MatchRange,把节点上的全部命中聚合为NodeMatch。这里使用readonly修饰,是因为匹配结果在计算完成后不应被意外修改,否则会导致高亮层和排序层看到不一致的数据。同时,用number类型精确描述索引,配合string保存原始文本,可以让高亮组件直接调用slice方法提取片段。
interface MatchRange {
readonly start: number;
readonly end: number;
readonly matchedText: string;
}
interface NodeMatch {
readonly nodeId: string;
readonly nodeText: string;
readonly depth: number;
readonly ranges: ReadonlyArray<MatchRange>;
}
这种设计把“是否能匹配”和“匹配在哪”彻底分开。如果后续要支持正则匹配或者同义词匹配,只需在MatchRange中增加type字段,而不用改动高亮渲染逻辑。类型上的稳定让团队在迭代搜索功能时更有底气。
排序算法类型的抽象与比较器定义
有了节点匹配数据,下一步是定义“怎么排”。不同产品对排序有不同偏好:有的希望命中关键词开头的节点优先,有的希望层级浅的节点优先,有的希望命中次数多的优先。如果把这些规则写死,类型就失去了表达力。更好的做法是定义一种比较器类型,把排序策略变成可注入的函数。
在TypeScript里,可以用类型别名描述比较器:输入两个NodeMatch,返回负数、零或正数。为了把多种权重组合起来,我们再定义一个RankedNodeMatch,它在原有匹配信息上附加计算出的权重分。这样排序算法本身只关心分数,不关心分数是怎么来的,实现了解耦。
type MatchComparator = (a: NodeMatch, b: NodeMatch) => number;
interface RankedNodeMatch extends NodeMatch {
readonly score: number;
}
function rankAndSort(
matches: ReadonlyArray<NodeMatch>,
scorer: (m: NodeMatch) => number,
comparator: MatchComparator
): ReadonlyArray<RankedNodeMatch> {
const ranked = matches.map(m => ({
...m,
score: scorer(m)
}));
return ranked.sort(comparator);
}
上面的rankAndSort函数展示了类型如何约束行为:调用方必须提供打分函数和比较器,但函数内部不绑定任何业务规则。如果思维导图需要在搜索时优先展示根节点附近的命中,只需写一个考虑depth的scorer即可,类型系统保证所有传入数据都带有depth字段。
组合多种匹配策略的联合类型与泛型封装
实际项目中,搜索高亮常常混合多种匹配模式,比如精确匹配、前缀匹配、模糊匹配。如果只用单一接口,类型就无法区分这些模式带来的权重差异。我们可以用联合类型标记不同匹配来源,并用泛型让排序容器适配任意节点数据。
以下示例定义了MatchStrategy联合类型,并在GenericNodeMatch<T>中把业务节点数据泛型化。这样思维导图的核心树节点不管是MindNode还是OutlineNode,都能复用同一套排序类型。联合类型中的strategy字段让比较器能针对性地提升某种策略的优先级,比如让前缀匹配永远排在模糊匹配前面。
type MatchStrategy = 'exact' | 'prefix' | 'fuzzy';
interface GenericNodeMatch<T> {
readonly data: T;
readonly strategy: MatchStrategy;
readonly ranges: ReadonlyArray<MatchRange>;
readonly depth: number;
}
function strategyWeight(s: MatchStrategy): number {
if (s === 'exact') return 100;
if (s === 'prefix') return 60;
return 20;
}
function defaultScorer<T>(m: GenericNodeMatch<T>): number {
const hitCount = m.ranges.length;
return strategyWeight(m.strategy) + hitCount * 5 - m.depth * 2;
}
通过这种方式,TypeScript的类型不只是“标注”,而是参与了排序算法的设计。高亮模块拿到GenericNodeMatch后,可直接读取ranges渲染标记;列表模块拿到排序后的数组,直接按序展示。类型边界清晰,既支持了思维导图节点搜索高亮的复杂诉求,也避免了任何一处因数据结构错位引发的运行时异常。
TypeScript思维导图排序算法修改时间:2026-08-18 03:10:29