学习如何用NodeJS构建CLI自动化工具

111 阅读7分钟

学习如何用NodeJS构建CLI自动化工具

开发人员喜欢CLI工具!以至于我相信,我们会抓住机会,在电脑上用命令行舒适地做一切事情。为什么不这样做呢?在现代库和框架的帮助下,建立我们自己的CLI是一个几小时甚至几分钟的事情。特别是在Node中,你可以使用Oclif框架轻松构建令人兴奋的命令行界面。

在这篇文章中,我们将创建一个简单的CLI工具来帮助我们跟踪我们的体重随时间的变化。它有一些简单的功能,如添加新的体重记录和显示过去的记录。

既然我们使用Oclif来轻松完成这项任务,那么我们就试着了解一下它到底是什么。

开发人员喜欢自动化


什么是Oclif?

Oclif是一个用于在Node中创建CLI工具的框架。它同时支持Javascript和Typescript的实现。Oclif提供了一套丰富的功能来设计和实现命令行程序,这些程序可以通过插件和钩子轻松扩展。


单命令与多命令

我们可以在Oclif中创建两种类型的命令行工具,单命令和多命令。单命令CLI只提供一个命令选项,类似于Linux中的lscurl 命令。多命令程序支持继续执行主命令的子命令。gitnpm 是多命令工具的好例子。

在本教程中,我们要建立一个多命令程序,支持addshow 子命令,以添加新的权重记录和显示旧的记录。


初始化项目

我们可以用一个简单的命令来初始化这个项目:

npx oclif multi [project name]

这里,我们使用npx,即npm包运行器,用Oclif初始化项目。命令multi ,指定我们的项目是一个多命令CLI。当你运行这个命令时,你会被提示输入关于项目的几个细节,以帮助Oclif创建初始项目文件,包括package.json

Oclif项目生成器

由于我们在这个项目中使用的是Javascript,请确保在Typescript字段中输入 "No"。

这一步完成后,你会看到OCLIF为我们的项目创建了一个新的目录,其中有以下子目录:

├── README.md
├── bin
│   ├── run
│   └── run.cmd
├── package.json
├── node_modules
├── src
│   ├── commands
│   │   └── hello.js
│   └── index.js
├── test
│   ├── commands
│   │   └── hello.test.js
│   └── mocha.opts
└── package-lock.json

这就是Oclif为我们创建的初始CLI。我们可以添加新的命令,把它转换为我们需要的程序。正如你所看到的,我们的项目已经有一个预定义的命令叫 "hello"。我们可以使用命令行来运行这个命令:

./bin/run hello

如果你想使用oclif_cli 命令(这是我们CLI的项目名称)从全局范围访问这个应用程序,使用npm link 命令:

npm link

现在,我们可以使用oclif_cli 命令在命令行上运行我们的应用程序:

运行一个oclif应用程序


向我们的应用程序添加新的命令

正如我前面提到的,我们的重量跟踪工具有两个主要命令:添加和显示。add 命令让我们添加weight 的新记录,show 命令则显示过去的weight 记录。

在创建这些命令时,你会遇到诸如标志和参数等术语。让我们首先澄清这些术语的含义。

标志和参数

如果你有一些使用命令行程序的经验,你可能已经知道什么是标志和参数。

旗号提供了一种指定运行特定命令的选项的方法。例如,考虑下面这个命令:

npm install -g oclif

在这里,我们使用标志-g 来指定我们需要全局安装Oclif。但是没有必要在每个命令中都包含这个标志。只有当你想激活它所代表的选项时,你才需要使用一个标志。

参数是由外部提供的,最常见的是由用户提供。在上面的npm命令中,oclif是用户提供的参数。当我们把它作为参数传递时,我们就是告诉命令行运行npm install 命令来安装所提供的软件包。类似地,我们可以用我们的命令接受参数,并在运行程序时将它们作为输入或目标。

创建应用程序的第一个命令

我们可以使用下面的命令来为我们的程序添加新的命令:

npx oclif command [command-name]

它创建新命令所需的所有文件,并更新README文件。

我们将按照这个格式添加我们的第一个命令,add

npx oclif command add

在CLI中添加一个新的命令就是这么简单。现在,oclif_cli add 命令已经可以使用了。但是,我们仍然要实现它的内部逻辑。为了实现这一点,我们应该修改src目录内已经创建的add.js 文件。

完全实现的添加命令应该能够接受一个参数,该参数指定了用户的体重,并以时间戳记录。因此,代码应该能够读取一个体重参数,并将其与添加记录的日期和时间保存在一个文件中。

为此,我们改变了AddCommand类(Oclif已经定义了这个类),包括以下实现:

const {Command, flags} = require('@oclif/command')
const Weight = require('../api/weight')
const weight = new Weight()

class AddCommand extends Command {
  
  async run() {
    const {args} = this.parse(AddCommand)
    const newWeight = args.weight
    weight.add(newWeight)
    this.log(`new weight ${newWeight} kg added`)
  }
}

AddCommand.description = "add a new record of weight"

AddCommand.args = [{
  name: "weight",
  description: "current weight in kilograms; insert only the value, omit kg",
  required: true
}]  

module.exports = AddCommand

在这里,我们给add 命令做了说明,并定义了它所接受的参数。我们声明了参数的名称(重量),并添加了一个描述,给用户提供了如何传递参数的指导。由于我们将该参数定义为 "必需",用户在不提供权重值的情况下将无法运行oclif_cli add 命令。

实现中最重要的部分是异步run 函数内的代码。它在用户每次运行添加命令时被调用。它读取命令中传递的参数,并使用Weight类的add方法(我们将在后面实现)将数据保存到文件中。一旦保存成功,它就会向控制台记录一条成功信息。

创建 "显示 "命令

现在,让我们来创建我们程序的第二个命令,show。它是用来在命令行上显示已经保存在文件中的重量数据。

我们可以用创建add 命令的同样方法创建show 命令:

npx oclif command show

让我们像以前那样实现show命令。这里唯一的区别是,show命令接受一个可选的标志。这个可选的标志允许用户指定他们想看多少条过去的记录。

我们可以这样定义这个标志(名为count):

ShowCommand.flags = {
  count: flags.string({char: 'c', description: 'count of past records to be displayed'}),
  help: flags.help({char: 'h'})
}

通过创建我们新标志的flags.string 函数,我们可以传递一个用于标志的字符和一个描述。当使用字符串函数来创建标志时,用户可以随即传递一个参数。对于我们的程序,我们已经声明这个参数是必需的。所以,在使用count 标志时,这个参数是必不可少的。

你可以在oclif官方flags文档中找到更多关于Oclif的不同类型的flag和接受的选项。

我们还使用Oclif内置的flags.help 功能创建了一个帮助标志,这样用户就可以获得使用该命令的帮助。

我们程序的完整showCommand 类是这样的:

const {Command, flags} = require('@oclif/command')
const Weight = require('../api/weight')
const weight = new Weight()

class ShowCommand extends Command {
  async run() {
    const {flags} = this.parse(ShowCommand)
    const count = flags.count 
    const records = weight.show(parseInt(count))
    for (let i =records.length -1 ; i >= 0; i--){
      let record = records[i]
      this.log(` Date: ${record.date}, Weight: ${record.weight}`)
    }
  }
}

ShowCommand.description = "show past weight records"

ShowCommand.flags = {
  count: flags.string({char: 'c', description: 'count of past records to be displayed'}),
  help: flags.help({char: 'h'})
}

module.exports = ShowCommand

同样,在run 函数中,我们定义了当用户运行这个命令时会发生什么。它解析我们传递的标志(如果有的话),并读取传递的带有计数标志的参数。它调用Weight 类的show 方法来检索必要数量的过去记录。最后,它将检索到的数据按照基于记录创建时间的降序记录到控制台。

实现Weight类

在前面的实现中,我们在程序中使用了Weight类的两个方法。我们在src目录下一个名为api 的新目录中创建这个类。在api 目录内,我们还创建了一个名为weightTracker 的文件夹来保存JSON文件,在这个文件中我们要存储我们的重量记录。这个JSON文件被命名为:weights.json

我不会详细介绍这个类是如何实现的。抽象的说,我们在这里所做的是这样的:

  • 读取weights.json文件的内容,并在类的构造函数中把它解析成一个数组。
  • 在weights数组中添加一个新的权重记录,并在add方法中调用saveWeight ,保存新的记录数组。
  • 在show方法中返回所有或指定数量的过去记录(作为count参数传递)。
  • 将对象数组转换为JSON格式,并在saveWeight 方法中把它写入weights.json文件。
const fs = require('fs')
const path = require('path')

const weightFile = path.join(__dirname, "weightTracker", 'weights.json')

class Weight{

    constructor(){  
        this.weights = []
        let content = fs.readFileSync(weightFile, {encoding: 'utf-8'})
        if (content){
            this.weights = JSON.parse(content)
        }
    }

    add(weight){
        let date = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '')
        let newWeight = {
            date: date,
            weight: weight
        }
        this.weights.push(newWeight)
        this.saveWeight()
    }

    show(count){
        let len = this.weights.length
        if (count && len>count){
            return this.weights.slice(len - count, len)
        }
        return this.weights
    }

    saveWeight(){
        if (!fs.existsSync(path.dirname(weightFile))){
            fs.mkdirSync(path.dirname(weightFile))
        }
        const records = JSON.stringify(this.weights)
        fs.writeFileSync(weightFile, records, {encoding: 'utf-8'})
    }

}

module.exports = Weight

测试我们的体重跟踪CLI工具

现在我们已经完全实现了我们的程序。剩下要做的就是玩一玩,看看它是如何运作的。

测试我们的体重跟踪应用程序

总结

正如我们在本教程中所看到的,oclif使任务自动化变得超级简单,并且使用我们最喜欢的编程语言。它自己处理了一些无聊的部分,如文档,并让我们在实际执行命令的过程中获得乐趣。所以,我希望我已经说服你给Oclif一个机会,让它成为你在Node中最喜欢的CLI框架。如果你对这个话题进行深入挖掘,你就能意识到Oclif是多么强大的CLI框架。

下次你打算自动化一个无聊的任务时,记得使用Oclif,而且别忘了在npm上发布你的包,与世界分享它。

谢谢你的阅读!