【JS】【自学总结】基于 node 写一个 todo 命令行工具

最近开源了一个 Vue 组件,还不够完善,欢迎大家来一起完善它,也希望大家能给个 star 支持一下,谢谢各位了。

github 地址:https://github.com/qq44924588...

前言

最后公司官网要用 node 重构,所以这段时间开始在学习 node 系列知识,一般对于新东西的上手,就是做一个 todo 的功能,所以这里也是基于 node 写一个 todo 命令行工具。

最终成品:https://github.com/qq44924588...

命令行相关的库

commander.js

node.js 命令行界面的完整解决方案是Ruby中Commandernode.js中的实现。
为commandline程序提供强大的参数解析能力.是TJ所写的一个工具包,其作用是让node命令行程序的制作更加简单。

具体用法请参考 github: https://github.com/tj/command...

inquirer.js

inquirer.js 一个用户与命令行交互的工具

开始通过npm init 创建package.json的时候就有大量与用户的交互(当然也可以通过参数来忽略输入);而现在大多数工程都是通过脚手架来创建的,使用脚手架的时候最明显的就是与命令行的交互,如果想自己做一个脚手架或者在某些时候要与用户进行交互,这个时候就不得不提到inquirer.js了。

具体用法请参考 github: https://github.com/SBoudrias/...

初始化

1.运行yarn init -y,添加package.json文件

2.运行yarn add commander,引入commander库

3.创建cli.js

使用 commander 创建子命令,这里指写一个子命令的使用方式,具体可以参考 github 上的项目

const program = require('commander');

program

.command('add')

.description('add a task')

.action(() => {

console.log('添加任务');

});

4.运行node cli add,node + 文件名 + 子命令,输出添加任务

5.对于数据的保存这里没有使用数据库存,简单的使用了文件,为此,我们需要封装文件相关的读和写操作,创建 db.js,内容如下

const homedir = require('os').homedir()

// 优先获取用户设置的 home

const home = process.env.HOME || homedir

const p = require('path')

const fs = require('fs')

const dbPath = p.join(home, '.todo')

const db = {

read (path = dbPath) {

// 读取之前的任务

return new Promise((resolve, reject) => {

fs.readFile(path, {

flag: 'a+'

}, (error, data) => {

if (error) return reject(error)

let list

try{

list = JSON.parse(data.toString())

} catch(error2) {

list = []

}

resolve(list)

})

})

},

write (list, path = dbPath) {

return new Promise((resolve, reject) => {

const string = JSON.stringify(list)

fs.writeFile(path, string, (error) => {

if (error) return reject(error)

resolve()

})

})

}

}

module.exports = db

这里我们在 home环境变量路径创建了一个 .todo 的文件,用来保存我们输入的数据。

对于 node Api 的查询,这里推荐一个网站 https://devdocs.io/,可以很方便查到对应方法使用方式,如下图所示:

【JS】【自学总结】基于 node 写一个 todo 命令行工具

6.任务的增删改都可以看作是一个接口,为此创建 index.js 用来封闭这些操作,这也叫作 面向接口编程,这里我们举例 添加方法的实现方式:

const db = require('./db.js')

module.exports.add = async (title) => {

// 读取之前的任务

const list = await db.read()

// 往里面添加一个 title 任务

list.push({title, done: false})

// 存储任务到文件

await db.write(list)

}

测试

输入node cli.js add 买水

查看:cat ~/.todo

输出[{"title":"买水","done":false}]

7.显示所有任务

在 cli.js 添加一条子命令 list

program

.command('list')

.description('show all todo list')

.action((...args) => {

api.showAll().then(res => {

// console.log('显示任务完毕!')

}, () => {

console.log('显示任务失败!')

})

});

在 index.js 中实现 showAll() 方法

const inquirer = require('inquirer')

module.exports.showAll = async () => {

//读取之前的任务

const list = await db.read()

//对任务进行操作

inquirer

.prompt({

type: 'list',

name: 'index',

message: '请选择你想操作的任务?',

choices: [{name: '退出', value: '-1'}, ...list.map((task, index) => {

return {name: `${task.done ? '[x]' : '[_]'} ${index + 1} - ${task.title}`, value: index.toString()}

}), {name: '+ 创建任务', value: '-2'}]

})

.then(answer => {

const index = parseInt(answer.index)

if(index >= 0) {

//选中了一个任务

inquirer.prompt({

type: 'list',

name: 'action',

choices: [

{name: '退出', value: 'quit'},

{name: '已完成', value: 'markAsDone'},

{name: '未完成', value: 'markAsUndone'},

{name: '改标题', vaule: 'updateTitel'},

{name: '删除', value: 'remove'}

]

}).then(answer2 => {

switch (answer2.action) {

case 'markAsDone':

list[index].done = true

db.write(list)

break

case 'markAsUndone':

list[index].done = false

db.write(list)

break

case 'updateTitle':

inquirer.prompt({

type: 'input',

name: 'title',

message: '新的标题',

default: list[index].title

}).then(answer => {

list[index].title = answer.title

db.write(list)

})

break

case 'remove':

list.splice(index, 1)

db.write(list)

break

}

})

}else if(index === -2) {

//创建任务

inquirer.prompt({

type: 'input',

name: 'title',

message: '输入任务标题',

}).then(answer => {

list.push({

title: answer.title,

done: false

})

db.write(list)

})

}

});

}

然后运行 node cli.js list 就可以显示所有任务了,这里列出这个方法主要是分享一下如何来优化这段冗长的代码。

优化代码

封装函数,重命名,提高代码可读性

我们先把选中一个任务也就是 index >=0 的逻辑先全部提取出来,放到函数 askForAction,然后把每个 switch case 的逻辑也都封装成一个函数

function askForAction (list, index) {

inquirer.prompt({

type: 'list', name: 'action',

message: '请选择操作',

choices: [

{ name: '退出', value: 'quit'},

{ name: '已完成', value: 'markAsDone'},

{ name: '未完成', value: 'markAsUnDone'},

{ name: '改标题', value: 'updateTitle'},

{ name: '删除', value: 'remove'},

]

}).then(answer => {

switch (answer.action) {

case 'markAsDone':

markAsDone(list, index)

break;

case 'markAsUnDone':

markAsUnDone()

break;

case 'updateTitle':

updateTitle()

break;

case 'remove':

remove()

break;

}

})

}

上面的 case 条件与我们函数又一致,所以可以在进一步优化,如下所示:

function askForAction (list, index) {

// 选中一个任务

const actions = {

markAsDone,

markAsUnDone,

updateTitle,

updateTitle,

remove

}

inquirer.prompt({

type: 'list', name: 'action',

message: '请选择操作',

choices: [

{ name: '退出', value: 'quit'},

{ name: '已完成', value: 'markAsDone'},

{ name: '未完成', value: 'markAsUnDone'},

{ name: '改标题', value: 'updateTitle'},

{ name: '删除', value: 'remove'},

]

}).then(answer => {

const action = actions[actions.action]

action && action(list, index)

})

}

在重构中,大部分的 switch 是可以被优化成更多简单的模式。

发布到 npm

配置 package.json

{

"name": "hi-node-todo",

"bin": {

"t": "cli.js"

},

"files": [

"*.js"

],

"version": "0.0.1",

"main": "index.js",

"license": "MIT",

"dependencies": {

"commander": "^6.2.1",

"inquirer": "^7.3.3"

}

}

bin 的作用

很多包都有一个或多个可执行的文件希望被放到PATH中。(实际上,就是这个功能让npm可执行的)。要用这个功能,给`package.json`中的bin字段一个命令名到文件位置的`map`。初始化的时候npm会将他链接到`prefix/bin`(全局初始化)或者`./node_modules/.bin/`(本地初始化)。

{ "bin" : { "npm" : "./cli.js" } }

当你初始化npm,它会创建一个符号链接到cli.js脚本到/usr/local/bin/npm。如果你只有一个可执行文件,并且名字和包名一样。那么你可以只用一个字符串,比如

{ "name": "my-program" , "version": "1.2.5" , "bin": "./path/to/program" }

// 等价于

{ "name": "my-program" , "version": "1.2.5" , "bin" : { "my-program" : "./path/to/program" } }

files 的作用

files是一个包含项目中的文件的数组。如果命名了一个文件夹,那也会包含文件夹中的文件。(除非被其他条件忽略了)你也可以提供一个.npmignore文件,让即使被包含在files字段中得文件被留下。其实就像.gitignore一样。

{ "files": [ "bin/", "templates/", "test/" ]}

main 的作用

main字段是一个模块ID,它是一个指向你程序的主要项目。就是说,如果你包的名字叫foo,然后用户安装它,然后require("foo"),然后你的main模块的exports对象会被返回。这应该是一个相对于根目录的模块ID。对于大多数模块,它是非常有意义的,其他的都没啥。

{ "main": "bin/index.js"}

发布:

  1. npm adduser
  2. npm publish

用户使用:

  1. yarn global add node-todos
  2. t list

版本升级: 增加查看版本的功能

cli.js

const pkg = require('./package.json')

program

.version(pkg.version)

测试,输入node cli.js --version,输出0.0.1

重新发布,直接npm publish

用户使用时,更新包yarn global add [email protected],运行t --version,输出0.0.2

完~,我是小智,我们下期见!

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug。


交流

文章每周持续更新,可以微信搜索「 大迁世界 」第一时间阅读和催更(比博客早一到两篇哟),本文 GitHub https://github.com/qq449245884/xiaozhi 已经收录,整理了很多我的文档,欢迎Star和完善,大家面试可以参照考点复习,另外关注公众号,后台回复福利,即可看到福利,你懂的。

【JS】【自学总结】基于 node 写一个 todo 命令行工具

以上是 【JS】【自学总结】基于 node 写一个 todo 命令行工具 的全部内容, 来源链接: utcz.com/a/96412.html

回到顶部