Skip to content

第一篇:文件搜索工具

一、Glob 工具实现

glob 工具是存在降级策略的,为了提高执行效率和降低运行占用量

Excalidraw 文件链接:https://my.feishu.cn/file/Wkhfbmlhqor0aHxivtfc52IunNh

Glob 工具实现

实现 glob 工具有两种方式,这两种方式“各有各的”好处

  • glob 依赖包:返回的是完整的文件的信息(存在文件元信息等,例如文件大小,文件修改时间),所以不需要额外的操作,并且 glob 依赖包是天然的 Node 环境的包
  • ripgrep 命令:返回的是文件路径,没有任何文件元信息,所以需要在操作读取文件信息的操作,stat方法,但是 ripgrep 检索的速度是比 glob 快的,但是 ripgrep 是 Rust 实现的,所以运行时需要加载一个二进制文件

关于 glob 工具执行的总时间,这里有一个形象的公式:

总时间=检索时间+ N*单文件的处理时间

  1. glob 依赖包的实现方式,后面的那个单文件处理时间完全可以忽略不计,所以其检索时间就约等于总时间
  2. ripgrep 命令的实现方式,检索时间是比 glob 依赖包的方式更快的,但是其需要单文件的处理时间,也就是 stat 方法的调用时间

我的建议和总结是:

  • 🚀如果是追求开发方便,那么我是建议直接使用 glob 实现,会快很多,并且不需要考虑外部文件的执行
  • 🪐如果是追求可操作的检索效率,那么是可以考虑使用 ripgrep 来实现,检索工具不可能只实现 glob,也会考虑使用 grep 的,要实现 grep,ripgrep 这个命令是优先考虑的,这么一看也不算是另外单独引入一个外部依赖
  • 🌴如果是追求稳定,那么可以考虑降级策略,先使用 ripgrep,如果 ripgrep 这个环境不存在或者下载失败,那么就可以降级为 glob,保证了程序或者项目可以运行

二、Grep 工具的实现

Excalidraw 文件链接:https://my.feishu.cn/file/IMGxbbcAXonPLDxsbcdcjQuZnfb

Grep 工具实现

目前 grep 有四种的方式实现,按照优先级排序,保证系统的稳定的可用性,使用降级策略保证 grep 工具执行成功,我们会先验证这些策略的可用性,再考虑优先级高的先使用

  1. ripgrep:是使用 Rust 编写的二进制文件,检索速度非常非常快
  2. git grep 命令:这个是直接读取.git/index 中已缓存的文件列表,跳过耗时的目录遍历操作
  3. 系统的 grep 命令:大部分是传统的 C 实现的,单线程递归搜索,速度还可以,大部分 Unix 系统都有,不过 windows 系统是没有的
  4. js 实现的 grep 命令:是纯 JS 实现的,是一个保底方案,用 glob 获取文件列表,逐个读取文件内容,逐行正则匹配,速度最慢

执行结果的目录:TODO

三、Ripgrep 自动下载机制

Excalidraw 文件链接:https://my.feishu.cn/file/ZAqfbP8MHo6pP9xMUDicMwNwnyD

Ripgrep 自动下载机制

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 });
}

所以目前的判断获取的策略是这样的:

  1. 先看看内存缓存中是否存在,如果没有就进入下一级,有就返回
  2. 再看看系统是否有安装 ripgrep,如果有就返回并且赋值给缓存,下一次就可以直接缓存取啦,如果没有就下一级
  3. 然后在看看本地路径是否安装了 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  // 用户可手动取消
);