第一篇:文件搜索工具
一、Glob 工具实现
glob 工具是存在降级策略的,为了提高执行效率和降低运行占用量
Excalidraw 文件链接:https://my.feishu.cn/file/Wkhfbmlhqor0aHxivtfc52IunNh
.DYm0hDcY.png)
实现 glob 工具有两种方式,这两种方式“各有各的”好处
- glob 依赖包:返回的是完整的文件的信息(存在文件元信息等,例如文件大小,文件修改时间),所以不需要额外的操作,并且 glob 依赖包是天然的 Node 环境的包
- ripgrep 命令:返回的是文件路径,没有任何文件元信息,所以需要在操作读取文件信息的操作,
stat方法,但是 ripgrep 检索的速度是比 glob 快的,但是 ripgrep 是 Rust 实现的,所以运行时需要加载一个二进制文件
关于 glob 工具执行的总时间,这里有一个形象的公式:
总时间=检索时间+ N*单文件的处理时间
- glob 依赖包的实现方式,后面的那个单文件处理时间完全可以忽略不计,所以其检索时间就约等于总时间
- ripgrep 命令的实现方式,检索时间是比 glob 依赖包的方式更快的,但是其需要单文件的处理时间,也就是 stat 方法的调用时间
我的建议和总结是:
- 🚀如果是追求开发方便,那么我是建议直接使用 glob 实现,会快很多,并且不需要考虑外部文件的执行
- 🪐如果是追求可操作的检索效率,那么是可以考虑使用 ripgrep 来实现,检索工具不可能只实现 glob,也会考虑使用 grep 的,要实现 grep,ripgrep 这个命令是优先考虑的,这么一看也不算是另外单独引入一个外部依赖
- 🌴如果是追求稳定,那么可以考虑降级策略,先使用 ripgrep,如果 ripgrep 这个环境不存在或者下载失败,那么就可以降级为 glob,保证了程序或者项目可以运行
二、Grep 工具的实现
Excalidraw 文件链接:https://my.feishu.cn/file/IMGxbbcAXonPLDxsbcdcjQuZnfb
.Dsmg1oIX.png)
目前 grep 有四种的方式实现,按照优先级排序,保证系统的稳定的可用性,使用降级策略保证 grep 工具执行成功,我们会先验证这些策略的可用性,再考虑优先级高的先使用
- ripgrep:是使用 Rust 编写的二进制文件,检索速度非常非常快
- git grep 命令:这个是直接读取.git/index 中已缓存的文件列表,跳过耗时的目录遍历操作
- 系统的 grep 命令:大部分是传统的 C 实现的,单线程递归搜索,速度还可以,大部分 Unix 系统都有,不过 windows 系统是没有的
- js 实现的 grep 命令:是纯 JS 实现的,是一个保底方案,用 glob 获取文件列表,逐个读取文件内容,逐行正则匹配,速度最慢
执行结果的目录:TODO
三、Ripgrep 自动下载机制
Excalidraw 文件链接:https://my.feishu.cn/file/ZAqfbP8MHo6pP9xMUDicMwNwnyD
.Djl5JaAj.png)
ripgrep 命令的执行需要完整的路径,在 Node 子进程中使用 spawn 的时候,需要完整的路径才可以成功执行命令
typescript
async function grepWithRipgrep(pattern, cwd, options) {
// 获取 ripgrep 路径
const rgPath = await Ripgrep.filepath(options.binDir);
// 返回:/usr/bin/rg 或 ~/.reason/bin/rg
// 使用路径执行命令
const proc = spawn(rgPath, [
'--line-number',
'--no-heading',
pattern,
], { cwd });
}所以目前的判断获取的策略是这样的:
- 先看看内存缓存中是否存在,如果没有就进入下一级,有就返回
- 再看看系统是否有安装 ripgrep,如果有就返回并且赋值给缓存,下一次就可以直接缓存取啦,如果没有就下一级
- 然后在看看本地路径是否安装了 ripgrep 的二进制文件,要是有安装的话,和上面同理,如果没有就开始进行下载文件到相应的目录中
四、超时控制
实现这个需求,在 Node 中会使用到中止控制器AbortController/AbortSignal,这里有一个简单的例子:
typescript
const controller = new AbortController();
function customTask(signal: AbortSignal): Promise<string> {
return new Promise((resolve, reject) => {
//初始状态的检查
if (signal.aborted) {
reject(new Error('Task aborted'));
return;
}
const timer = setTimeout(() => resolve('done'), 5000);
// 使用 { once: true } 自动清理监听器
const abortHandler = () => {
clearTimeout(timer);
reject(new Error('Task aborted'));
};
signal.addEventListener('abort', abortHandler, { once: true });
// 或者手动清理(如果需要在 resolve 时也清理)
const cleanup = () => {
signal.removeEventListener('abort', abortHandler);
};
// 修改 timer 回调
const timerCallback = () => {
cleanup();
resolve('done');
};
setTimeout(timerCallback, 5000);
});
}
customTask(controller.signal);
// 3秒后取消
setTimeout(() => {
controller.abort();
}, 3000);AbortController:这个是控制器,用来发送“取消”信号AbortSignal:这个是信号,传递给异步操作,让它们可以被取消
关于 AbortSignal 这个对象,有一些属性是值得理解一下的,会让你在异步操作使用更加熟练
typescript
interface AbortSignal{
readonly aborted:boolean //是否已经被取消
readonly reason:any //取消的原因
//监听取消事件
addEventListener(
type:'abort',
listener:(event:Event)=>void,
options?:{once?:boolean}
):void
//移除取消事件监听器
removeEventListener(
type:'abort',
listener:(event:Event) => void
):void
//用于检查信息是否已取消
throwIfAborted():void
}那我们开始整理超时控制的函数式如何写的,主要就是三步:
- 封装取消函数,这个函数返回信号对象
- 创建一个包装器,传入要取消的函数操作,用于包装任何的异步操作
- 传入参数给异步操作
点击展开完整代码
typescript
//完整的核心代码如下
//1、创建取消函数,返回信号对象
export function createTimeoutSignal(
timeoutMs: number,
externalSignal?: AbortSignal
): {
signal: AbortSignal;
cleanup: () => void;
isTimeout: () => boolean;
} {
const controller = new AbortController();
let timedOut = false;
// 超时定时器
const timeoutId = setTimeout(() => {
timedOut = true;
controller.abort();
}, timeoutMs);
// 监听外部中止信号
const abortHandler = () => {
clearTimeout(timeoutId);
controller.abort();
};
externalSignal?.addEventListener('abort', abortHandler, { once: true });
// 清理函数
const cleanup = () => {
clearTimeout(timeoutId);
externalSignal?.removeEventListener('abort', abortHandler);
};
return {
signal: controller.signal,
cleanup,
isTimeout: () => timedOut,
};
}
//2、创建异步操作包装器
export async function withTimeout<T>(
promiseFactory: (signal: AbortSignal) => Promise<T>,
timeoutMs: number,
operation: string,
externalSignal?: AbortSignal
): Promise<T> {
// 1. 提前检查
if (externalSignal?.aborted) {
throw createAbortError();
}
// 2. 创建超时信号
const { signal, cleanup, isTimeout } = createTimeoutSignal(timeoutMs, externalSignal);
try {
// 3. 执行操作,传入信号
const result = await promiseFactory(signal);
// 4. 成功完成,清理资源
cleanup();
return result;
} catch (error) {
// 5. 失败,清理资源
cleanup();
// 6. 如果是超时导致的中止,抛出 TimeoutError
if (isTimeout() && isAbortError(error)) {
throw createTimeoutError(operation, timeoutMs);
}
// 7. 其他情况原样抛出
throw error;
}
}
//3、传递取消信号为进程执行的异步函数 - 简化版
function spawnAsync(command: string, args: string[], signal?: AbortSignal): Promise<void> {
return new Promise((resolve, reject) => {
const proc = child_process.spawn(command, args);
// 监听取消信号
signal?.addEventListener('abort', () => {
proc.kill();
reject(new Error('Aborted'));
}, { once: true });
proc.on('close', (code) => {
code === 0 ? resolve() : reject(new Error(`Exit code ${code}`));
});
proc.on('error', reject);
});
}
await withTimeout(
(signal) => spawnAsync('long-command', [], { signal }),
5000, // 5秒超时
'command execution',
userCancelSignal // 用户可手动取消
);