如何使用 Node.js 构建命令行应用程序

作为一名开发人员,你很可能在终端中度过大部分时间,键入命令来帮助你绕过一些任务。

其中一些命令嵌入到您的操作系统中,而其中一些命令是通过第三方辅助程序(如 npm 或 brew)安装的,甚至是下载二进制文件并将其添加到您的 $PATH。

常用的应用程序的一个很好的例子是 npm、eslint、typcript 和项目生成器,例如 Angular CLI, Vue CLICreate React App

在本教程中,您将在 Node.js 中构建两个小型 CLI 应用程序:

  1. 每日引用工具从 https://quotes.rest/qod获取当天的引用。
  2. 一个使用JSON来保存数据的任务列表应用程序。

前提条件

要完成本教程,您将需要:

步骤1 - 了解 Shebang

每当你查看任何脚本文件时,你会在文件开始时看到这样的字符:

1[secondary_label file.sh]
2#!/usr/bin/env sh

或者这个:

1[secondary_label file.py]
2#!/usr/bin/env python -c

它们为您的操作系统程序加载器提供一种方法,以查找并使用对可执行文件的正确解读器。

從 [ 維基百科 ]( [ LINK0 ] ) :

在计算中,Shebang是由脚本开始时的字符号号符号和呼声符号(# !)组成的字符序列。

NodeJS 拥有自己的支持的 shebang 字符。

在编辑器中创建一个名为logger.js的新文件:

1nano logger.js

添加以下代码:

1#!/usr/bin/env node
2
3console.log("I am a logger")

第一行告诉程序加载器用NodeJS分析此文件,第二行将文本打印到屏幕上。

您可以尝试通过在终端中键入此文件来运行该文件,您将收到被拒绝执行的权限。

1./logger
1[secondary_label Output]
2zsh: permission denied: ./logger

你需要给文件执行权限,你可以这样做。

1chmod +x logger
2./logger

这一次,你会看到结果。

1[secondary_label Output]
2I am a logger

您可以使用节点日志器运行这个程序,但添加 shebang 并使程序可自行执行,可以避免键入节点来运行它。

创建每日报价应用程序

让我们创建一个目录,并将其称为qod

1mkdir qod
2cd qod
3npm init -y

接下来,我们知道我们需要向报价服务器提出请求,这样我们就可以使用现有库来做这件事。

1npm install --save axios

我们还会添加一个 chalk,一个图书馆,以帮助我们在终端中打印颜色。

1npm install --save chalk

然后我们写出需要回收这些引文的逻辑。

创建一个名为qod的新文件:

1nano qod

将下列代码添加到 qod 文件中,以指定 shebang,加载库,并存储 API URL:

1[label qod]
2#!/usr/bin/env node
3
4const axios = require('axios');
5const chalk = require('chalk');
6
7const url = "https://quotes.rest/qod";

接下来,添加此代码来对API进行GET请求:

 1[label qod]// make a get request to the url
 2axios({
 3  method: 'get',
 4  url: url,
 5  headers: { 'Accept': 'application/json' }, // this api needs this header set for the request
 6}).then(res => {
 7  const quote = res.data.contents.quotes[0].quote
 8  const author = res.data.contents.quotes[0].author
 9  const log = chalk.green(`${quote} - ${author}`) // we use chalk to set the color green on successful response
10  console.log(log)
11}).catch(err => {
12  const log = chalk.red(err) // we set the color red here for errors.
13  console.log(log)
14})

保存檔案

更改文件权限以使文件可执行:

1chmod +x qod

然后运行应用程序:

1./qod

你会看到一个引用:

1[secondary_label Output]
2The best way to not feel hopeless is to get up and do something. Don’t wait for good things to happen to you. If you go out and make some good things happen, you will fill the world with hope, you will fill yourself with hope. - Barack Obama

此示例显示,您可以在 CLI 应用程序中使用外部库。

现在让我们创建一个保存数据的 CLI 程序。

创建一个To-Do列表

这将是更复杂的,因为它将涉及数据存储和检索,这是我们试图实现的。

我们需要一个命令叫做todo 2 命令将包含四个参数:,得到,完成帮助

可用的命令将是

1./todo new // create a new todo
2./todo get // get a list of all your todos
3./todo complete // complete a todo item.
4./todo help // print the help text

创建一个名为todo的目录,然后创建一个Node.js应用程序:

1mkdir todo
2cd todo
3npm install -y

接下来,重新安装 chalk,以便您可以使用颜色登录。

1npm install --save chalk

要使命令工作,我们将使用NodeJs的 process/argv,它返回了命令行参数的字符串数组。

创建全部文件:

1nano todo

把它添加到整个文件中。

1[label todo]
2#!/usr/bin/env node
3
4console.log(process.argv)

给该文件可执行的权限,然后使用新命令运行它。

1chmod +x ./todo
2./todo new

你会得到这个输出:

1[secondary_label Output]
2[ '/Users/sammy/.nvm/versions/node/v8.11.2/bin/node',
3  '/Users/sammy/Dev/scotch/todo/todo',
4  'new' ]

请注意,数组中的前两个字符串是解释器和到程序的完整文件路径,其余的数组包含通过的参数;在这种情况下,它是

为了安全,让我们限制这些,以便我们只能接受正确的论点数目,即一个,它们只能是,得到完整

更改全部文件,以便它看起来如下:

 1[label todo]
 2#!/usr/bin/env node
 3
 4const chalk = require('chalk')
 5const args = process.argv
 6
 7// usage represents the help guide
 8const usage = function() {
 9  const usageText = `
10  todo helps you manage you todo tasks.
11
12  usage:
13    todo <command>
14
15    commands can be:
16
17    new:      used to create a new todo
18    get:      used to retrieve your todos
19    complete: used to mark a todo as complete
20    help:     used to print the usage guide
21  `
22
23  console.log(usageText)
24}
25
26// used to log errors to the console in red color
27function errorLog(error) {
28  const eLog = chalk.red(error)
29  console.log(eLog)
30}
31
32// we make sure the length of the arguments is exactly three
33if (args.length > 3) {
34  errorLog(`only one argument can be accepted`)
35  usage()
36}

我们首先将命令行参数分配给一个变量,然后在底部检查长度不超过三。

我们还添加了一个使用字符串,它会打印指令行应用程序所期望的内容。

1./todo new app
 1[secondary_label Output]
 2only one argument can be accepted
 3
 4todo helps you manage you todo tasks.
 5
 6usage:
 7  todo <command>
 8
 9  commands can be:
10
11  new:      used to create a new todo
12  get:      used to retrieve your todos
13  complete: used to mark a todo as complete
14  help:     used to print the usage guide

如果你用一个参数运行它,它不会打印任何东西,这意味着代码通过。

接下来,我们需要确保只有四个命令是预期的,其他一切都将被打印为无效。

添加文件顶部的命令列表:

1[label todo]
2const commands = ['new', 'get', 'complete', 'help']

然后检查在我们检查了长度后通过的命令:

1[label todo]
2...
3if (commands.indexOf(args[2]) == -1) {
4  errorLog('invalid command passed')
5  usage()
6}

现在,如果我们用无效的命令运行应用程序,我们会得到这一点。

1./todo ne
 1[secondary_label Output]
 2invalid command passed
 3
 4  todo helps you manage you todo tasks.
 5
 6  usage:
 7    todo <command>
 8
 9    commands can be:
10
11    new:      used to create a new todo
12    get:      used to retrieve your todos
13    complete: used to mark a todo as complete
14    help:     used to print the usage guide

现在让我们通过调用usage函数来实现帮助命令,让我们将此添加到 todo 文件中:

 1[label todo]
 2
 3//...
 4switch(args[2]) {
 5  case 'help':
 6    usage()
 7    break
 8  case 'new':
 9    break
10  case 'get':
11    break
12  case 'complete':
13    break
14  default:
15    errorLog('invalid command passed')
16    usage()
17}
18//...

我们有一个交换声明,它会根据被调用的命令调用函数,如果你仔细观察,你会注意到帮助案例只是调用使用函数。

命令将创建一个新的 todo 项目,并将其保存到 json 文件中. 我们将使用的库是 lowdb.如果我们想要的话,我们可以轻松地写函数来读和写到 json 文件中。

安装LowDB

1npm install --save lowdb

让我们添加[readline](https://nodejs.org/api/readline.html)lowdb依赖,以帮助我们存储数据。

 1[label todo]
 2
 3//...
 4const rl = require('readline');
 5
 6const low = require('lowdb')
 7const FileSync = require('lowdb/adapters/FileSync')
 8
 9const adapter = new FileSync('db.json')
10const db = low(adapter)
11
12// Set some defaults (required if your JSON file is empty)
13db.defaults({ todos: []}).write()
14//...

接下来,我们将添加一个函数,要求用户输入数据。

 1[label todo]
 2
 3//...
 4function prompt(question) {
 5  const r = rl.createInterface({
 6    input: process.stdin,
 7    output: process.stdout,
 8    terminal: false
 9  });
10  return new Promise((resolve, error) => {
11    r.question(question, answer => {
12      r.close()
13      resolve(answer)
14    });
15  })
16}
17//...

在这里,我们正在使用 readline 库创建一个界面,这将帮助我们提示用户,然后阅读输出。

接下来,我们需要添加一个函数,当用户在命令中键入时将被调用:

 1[label todo]
 2
 3//...
 4function newTodo() {
 5  const q = chalk.blue('Type in your todo\n')
 6  prompt(q).then(todo => {
 7    console.log(todo)
 8  })
 9}
10//...

我们正在使用来获取提示的蓝色颜色,然后我们将记录结果。

最后,在案例中称呼函数。

 1[label todo]
 2
 3// ...
 4switch(args[2]) {
 5  //...
 6  case 'new':
 7    newTodo()
 8    break
 9    // ...
10}
11// ...

当您现在使用新命令运行应用时,您将被要求添加 todo。

1./todo new
1[secondary_label Output]
2Type in your todo
3This my todo aaaaaaw yeah
4This my todo aaaaaaw yeah

你应该看到类似的东西。

另外,请注意,在您的文件系统中已创建一个 db.json 文件,并且它具有 todos 属性。

接下来,让我们在添加 todo 的逻辑中添加. 更改 newTodo 函数。

 1[label todo]
 2
 3//...
 4function newTodo() {
 5  const q = chalk.blue('Type in your todo\n')
 6  prompt(q).then(todo => {
 7    // add todo
 8    db.get('todos')
 9      .push({
10      title: todo,
11      complete: false
12      })
13      .write()
14  })
15}
16//...

再次运行代码。

1./todo new
1[secondary_label Output]
2Type in your todo
3Take a Scotch course

如果你看你的 db.json,你会看到添加的 todo. 添加两个,这样我们可以在下一个 get 命令中获取它们. 这里是 db.json 文件看起来像更多的记录:

 1[label db.json]
 2
 3{
 4  "todos": [
 5    {
 6      "title": "Take a Scotch course",
 7      "complete": false
 8    },
 9    {
10      "title": "Travel the world",
11      "complete": false
12    },
13    {
14      "title": "Rewatch Avengers",
15      "complete": false
16    }
17  ]
18}

创建命令后,您应该已经有关于如何执行得到命令的想法。

创建一个功能,将恢复所有人。

 1[label todo]
 2
 3//...
 4function getTodos() {
 5  const todos = db.get('todos').value()
 6  let index = 1;
 7  todos.forEach(todo => {
 8    const todoText = `${index++}. ${todo.title}`
 9    console.log(todoText)
10  })
11}
12//...
13
14// switch statements
15switch(args[2]) {
16    //...
17    case 'get':
18    	getTodos()
19    	break
20    //...
21}
22//....

再次运行命令:

1./todo get

现在运行该应用程序将提供此输出:

1[secondary_label Output]
21. Take a Scotch course
32. Travel the world
43. Rewatch Avengers

您可以使用「chalk.green」來變成綠色。

接下来,添加完整命令,这有点复杂。

你可以用两种方式做到这一点。

  1. 每当用户在 ./todo complete 中键入时,我们可以列出所有 todos,并要求他们键入数字/钥匙,以便 todo 标记为 complete.
  2. 我们可以添加另一个参数,以便用户可以键入 ./todo get,然后选择与参数标记为 complete 的任务,如 ./todo complete 1

由于您在实施命令时学会了如何执行第一个方法,我们将看看选项2。

使用此选项,命令 ./todo complete 1 将导致我们对所给出的命令数量的验证失败,因此我们首先需要处理此问题。

 1[label todo]
 2
 3//...
 4// we make sure the length of the arguments is exactly three
 5if (args.length > 3 && args[2] != 'complete') {
 6  errorLog('only one argument can be accepted')
 7  usage()
 8  return
 9}
10///...

此方法使用真相表,其中真与假将等于,当完整通过时,代码将被跳过。

然后,我们将抓取新参数的值,并将 todo 的值作为完成:

 1[label todo]
 2
 3//...
 4function completeTodo() {
 5  // check that length
 6  if (args.length != 4) {
 7    errorLog("invalid number of arguments passed for complete command")
 8    return
 9  }
10
11  let n = Number(args[3])
12  // check if the value is a number
13  if (isNaN(n)) {
14    errorLog("please provide a valid number for complete command")
15    return
16  }
17
18  // check if correct length of values has been passed
19  let todosLength = db.get('todos').value().length
20  if (n > todosLength) {
21    errorLog("invalid number passed for complete command.")
22    return
23  }
24
25  // update the todo item marked as complete
26  db.set(`todos[${n-1}].complete`, true).write()
27}
28//...

此外,更新交换语句以包含完整命令:

1[label todo]
2
3//...
4case 'complete':
5    completeTodo()
6    break
7//...

当你用 ./todo complete 2 运行此功能时,你会注意到你的 db.json 已更改为此功能,将第二个任务标记为完成:

 1[label db.json]
 2
 3{
 4  "todos": [
 5    {
 6      "title": "Take a Scotch course",
 7      "complete": false
 8    },
 9    {
10      "title": "Travel the world",
11      "complete": true
12    },
13    {
14      "title": "Rewatch Avengers",
15      "complete": false
16    }
17  ]
18}

我们需要做的最后一件事是更改 ./todo get 以仅显示已完成的任务. 我们将为此使用 emojis。

 1[label todo]
 2
 3//...
 4function getTodos() {
 5  const todos = db.get('todos').value()
 6  let index = 1;
 7  todos.forEach(todo => {
 8    let todoText = `${index++}. ${todo.title}`
 9    if (todo.complete) {
10      todoText += ' ✔ ️' // add a check mark
11    }
12    console.log(chalk.strikethrough(todoText))
13  })
14  return
15}
16//...

当你现在键入./todo get时,你会看到这一点。

1[secondary_label Output]
21. Take a Scotch course
32. Travel the world ✔ ️
43. Rewatch Avengers

结论

你已经在 Node.js 中写了两个 CLI 应用程序。

一旦您的应用程序工作,将文件放入一个bin文件夹. 这样,npm 就可以知道如何在分发时使用可执行的文件。

本文的重点是研究CLI应用程序是如何用瓦尼利诺德耶构建的,但在现实世界中工作时,使用库会更有成效。

以下是有用的库列表,可帮助您编写出色的 CLI 应用程序,您可以将其发布到 npm。

  1. vopral - 完整的交互式 CLI 框架
  2. meow - CLI 帮助库
  3. commanderjs - CLI 图书馆
  4. minimist - 参数解析
  5. yargs - 参数解析

並不提到像 chalk這樣的圖書館,它們幫助了我們用顏色。

作为一个额外的练习,尝试在 CLI 中添加一个删除命令。

Published At
Categories with 技术
Tagged with
comments powered by Disqus