学习如何用NodeJS构建CLI自动化工具
开发人员喜欢CLI工具!以至于我相信,我们会抓住机会,在电脑上用命令行舒适地做一切事情。为什么不这样做呢?在现代库和框架的帮助下,建立我们自己的CLI是一个几小时甚至几分钟的事情。特别是在Node中,你可以使用Oclif框架轻松构建令人兴奋的命令行界面。
在这篇文章中,我们将创建一个简单的CLI工具来帮助我们跟踪我们的体重随时间的变化。它有一些简单的功能,如添加新的体重记录和显示过去的记录。
既然我们使用Oclif来轻松完成这项任务,那么我们就试着了解一下它到底是什么。
开发人员喜欢自动化
什么是Oclif?
Oclif是一个用于在Node中创建CLI工具的框架。它同时支持Javascript和Typescript的实现。Oclif提供了一套丰富的功能来设计和实现命令行程序,这些程序可以通过插件和钩子轻松扩展。
单命令与多命令
我们可以在Oclif中创建两种类型的命令行工具,单命令和多命令。单命令CLI只提供一个命令选项,类似于Linux中的ls 和curl 命令。多命令程序支持继续执行主命令的子命令。git 和npm 是多命令工具的好例子。
在本教程中,我们要建立一个多命令程序,支持add 和show 子命令,以添加新的权重记录和显示旧的记录。
初始化项目
我们可以用一个简单的命令来初始化这个项目:
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上发布你的包,与世界分享它。
谢谢你的阅读!