4
点赞再看,微信搜索大迁世界,B站关注【前端小智】这个没有大厂背景,但有着一股向上积极心态人。本文 GitHub https://github.com/qq44924588... 上已经收录,文章的已分类,也整理了很多我的文档,和教程资料。

最近开源了一个 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/,可以很方便查到对应方法使用方式,如下图所示:

clipboard.png

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 node-todos@0.0.2,运行t --version,输出0.0.2

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

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


交流

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


王大冶
68.1k 声望105k 粉丝