如何快速学习cac库源码

260 阅读3分钟

1.webp

背景

一直以来就想试着去阅读一个库的源码,但看到项目目录结构就晕了,感觉什么都看不懂,然后就放弃了,这次借着源码共读的机会,下定决心去读一个库的源码。我们选择的是cac这个库,读完最大的感受就是一定要先把库的文档读几遍,英文不懂就一个个查也行,了解库的基本api使用,然后把测试用例跑出来,对感兴趣或者不懂的用例进行调试,这样库的核心实现过程基本就能理解了。

CAC库介绍

一个用于解析命令行参数,构建CLI应用程序的JavaScript库

阅读源码的流程

  1. 下载好了代码先安装依赖(这个库用yarn安装)
  2. 运行一下测试,检测是否可以跑起来
  3. 基于一个测试跑一下,先大概了解一下流程,在 CAC 中使用 jest 作为单元测试框架,使用yarn test运行测试用例
  4. 自己写一个测试,这个测试就写你想要了解的功能实现,然后通过打断点的方式一步步看执行过程 这里需要重点讲一下测试驱动的好处,至于测试驱动开发,对于开发轮子的过程来说,有这几个好处:
  5. 理清开发思路
  6. 测试即需求,通过测试即通过需求
  7. 重构不需要担心原有功能产生破坏

调试过程

这里我以Brackets 是如何实现解析的为例,首先在项目的__test__目录的index.test.ts添加一个测试用例

// 当在命令名中使用括号时,<>表示必需的命令参数,而[]表示可选参数。
// 当在选项名称中使用括号时,<>表示可以是一个字符串/数字值,而[]表示该值也可以为true。
describe('Brackets', () => {
  test.only('angled brackets indicate required command arguments', async () => {
    const cli = cac()
    cli
      .command('deploy <folder>', 'Deploy a folder to AWS')
      .option('--scale [level]', 'Scaling level')
      .action((folder) => {
        console.log('folder', folder)
      })
    const {args, options} = cli.parse(`node bin deploy cac.ts --scale`.split(' '))
    expect(args).toEqual(['cac.ts'])
    expect(options).toEqual({ '--': [], scale: true })
    // toThrowErrorMatchingInlineSnapshot 是jest一个行内快照API
    expect(() => cli.parse(`node bin deploy`.split(' '))).toThrowErrorMatchingInlineSnapshot( )
  })
})

通过打断点调试我们可以知道,在src/CAC.ts文件下的parse()方法中

// run不传则为true
if (run) {
       // 运行匹配的command命令
      this.runMatchedCommand()
 }

进入runMatchedCommand()可以发现

// src/CAC.ts
runMatchedCommand() {
    if (!command || !command.commandAction) return
    // 检查解析的选项是否包含任何未知选项
    command.checkUnknownOptions()
    // 检查所需的字符串类型选项是否存在
    command.checkOptionValue()
    // 检查是否是必选的参数
    command.checkRequiredArgs()
   ...
}
// src/Command.ts
checkRequiredArgs() {
    const minimalArgsCount = this.args.filter((arg) => arg.required).length
    // 通过传入的参数和必选的参数数量比较
    if (this.cli.args.length < minimalArgsCount) {
      throw new CACError(
        `missing required args for command \`${this.rawName}\``
      )
    }
}
// 解析括号的方法在utils.ts中
export const findAllBrackets = (v: string) => {
  const ANGLED_BRACKET_RE_GLOBAL = /<([^>]+)>/g
  const SQUARE_BRACKET_RE_GLOBAL = /\[([^\]]+)\]/g

  const res = []

  const parse = (match: string[]) => {
    let variadic = false
    let value = match[1]
    if (value.startsWith('...')) {
      value = value.slice(3)
      variadic = true
    }
    return {
      required: match[0].startsWith('<'),  // 是否必选
      value, // 括号里面的值
      variadic // 是否可变
    }
  }

  let angledMatch
  while ((angledMatch = ANGLED_BRACKET_RE_GLOBAL.exec(v))) {
    res.push(parse(angledMatch))
  }

  let squareMatch
  while ((squareMatch = SQUARE_BRACKET_RE_GLOBAL.exec(v))) {
    res.push(parse(squareMatch))
  }

  return res
}

到此基本知道了CACcommand命令是如果解析<>[]的,option也是一样的调试过程,这里就不细讲了。再次体验到了测试用例的重要性。功能开发也是同样如此,先把要实现的功能写好测试用例,就已经达到事半功倍的作用,后续重构和修改bug也一点都不慌,只要测试能通过,就没大问题。

总结

CAC 这种因为使用场景可能存在多个解析器,使用 OOP 的编写思想,和我们日常函数式写法不太一样,也为我们日后开发提供了另一个选型。CAC已经很久没有更新了,后续看看可以用新技术优化一下这个库。这是我第一次在掘金输出文章,写的不好或者有错误的地方,还望大佬们多多指教,感谢感谢!!

demo仓库

cac源码学习参考