使用 Node.js fs 模块做一个命令行 todo 工具

191 阅读3分钟

todo 工具功能介绍:

  • 增:添加 todo
  • 删:删除 todo
  • 改:修改 todo
  • 查:列出所有的 todo
  • 将 todo 标记为“未完成”或“已完成”
  • 安装:npm i node-todo-dongyarn add node-todo-dong 完整代码:Gitee | GitHub

一、创建 add 和 clear 命令

Commander.js 可以用来帮助我们开发命令行界面,这个 todo 工具用的是 Commander.js v3.0.2

yarn add commander@3.0.2

使用 CRM 学习法,把代码从文档中抄过来运行、修改,得到如下代码:

// cli.js

const program = require('commander');
const pkg = require('./package.json');

// 添加参数
program
  .version(pkg.version)

// 添加命令 add
program
  .command('add')
  .description('add a task')
  .action((...args) => {
    const words = args.slice(0, -1).join(' ')
    console.log('words:', words)
  });

// 添加命令 clear
program
  .command('clear')
  .description('clear all task')
  .action(() => {
    console.log('清除成功')
  });

program.parse(process.argv);

image.png

二、实现 add 功能

1. cli 调用 index 中的 add 方法

// index.js

module.exports.add = (title) => {
  console.log(title)
}
// cli.js(部分代码)

const api = require('./index.js');

// 添加命令 add
program
  .command('add')
  .description('add a task')
  .action((...args) => {
    const words = args.slice(0, -1).join(' ')
    api.add(words)
  });

image.png

2. 确定数据保存的位置

建议将 todo 数据保存在用户(home)目录中,但是用户也可能把用户目录重置到了其他地方,所以我们还要获取用户设置的 home 变量。

// index.js

const userHomeDir = require('os').homedir();
const home = process.env.home || userHomeDir;  // 优先把数据存到用户自己设置的 home 中

module.exports.add = (title) => {
  console.log(title)
  console.log('userHomeDir:', userHomeDir)
  console.log('home:', home)
}

image.png

3. 实现 add 功能

// index.js

const userHomeDir = require('os').homedir();
const home = process.env.home || userHomeDir;
const path = require('path')
const dbPath = path.join(home, '.todo')
const fs = require('fs')

module.exports.add = (title) => {
  // 读取文件内容,如果文件不存在,就创建 .todo 文件
  fs.readFile(dbPath, { flag: 'a+' }, (err, data) => {
    if (err) throw err
    let list
    try {
      list = JSON.parse(data.toString())
    } catch (err) {
      list = []
    }
    // 往 list 中添加一个任务
    list.push({ title, done: false })
    const content = JSON.stringify(list) + '\n'
    // 把任务写到 .todo 文件中
    fs.writeFile(dbPath, content, err => {
      if (err) throw err
      console.log('创建成功')
    })
  })
}

image.png

4. 优化代码

把对数据的读写操作封装到 db.js 中:

// db.js

const userHomeDir = require('os').homedir();
const home = process.env.home || userHomeDir;
const path = require('path')
const dbPath = path.join(home, '.todo')
const fs = require('fs')

const db = {
  read(path = dbPath) {
    return new Promise((resolve, reject) => {
      fs.readFile(path, { flag: 'a+' }, (err, data) => {
        if (err) throw err
        let list
        try {
          list = JSON.parse(data.toString())
        } catch (err) {
          list = []
        }
        resolve(list)
      })
    })
  },
  write(list, path = dbPath) {
    return new Promise((resolve, reject) => {
      const content = JSON.stringify(list) + '\n'
      fs.writeFile(path, content, err => {
        if (err) throw err
        resolve('success')
      })
    })
  }
}

module.exports = db
// index.js

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

module.exports.add = async (title) => {
  // 读取文件内容,如果文件不存在,就创建 .todo 文件
  const list = await db.read()
  // 往 list 中添加一个任务
  list.push({ title, done: false })
  // 把任务写到 .todo 文件中
  const result = await db.write(list)
  console.log(result)
}

三、实现 clear 功能

1. cli 调用 index 中的 clear 方法

// index.js(部分代码)

module.exports.clear = () => {
  console.log('清除成功')
}
// cli.js(部分代码)

const program = require('commander');
const api = require('./index.js');

// 添加命令 clear
program
  .command('clear')
  .description('clear all task')
  .action(() => {
    api.clear()
  });

2. 实现 clear 功能

// index.js(部分代码)

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

module.exports.clear = async () => {
  const result = await db.write([])
  console.log(result)
}

image.png

四、实现与命令行交互功能

Inquirer.js 可以实现与命令行进行交互,这个 todo 工具使用的是 inquirer v 8.2.4。

yarn add inquirer

1. 展示 todo 列表

// index.js(部分代码)

const db = require('./db.js');
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 ? '[√]' : '[_]'} ${index + 1} - ${task.title}`, value: index.toString() }
      }),
      { name: '+ 创建任务', value: '-2' }
    ]
  }).then(answer => {
    console.log(answer)
  });
}

image.png

2. 展示操作列表

// index.js(部分代码)

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

module.exports.showAll = async () => {
  const list = await db.read()
  // 展示 todo 列表
  inquirer.prompt({
    // ...
  }).then(answer => {
    const index = parseInt(answer.index)
    // 选择任务
    if (index >= 0) {
      // 展示操作列表
      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 => {
        console.log(answer)
      })
    }
    // 创建任务
    if (index === -2) {
      console.log('创建任务')
    }
  });
}

image.png

3. 执行操作

// index.js(部分代码)

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

module.exports.showAll = async () => {
  const list = await db.read()
  // 展示 todo 列表
  inquirer.prompt({
    // ...
  }).then(answer => {
    const index = parseInt(answer.index)
    // 选择任务
    if (index >= 0) {
      // 展示操作列表
      inquirer.prompt({
        // ...
      }).then(answer => {
        // 执行操作
        switch (answer.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
        }
      })
    }
    // 创建任务
    if (index === -2) {
      console.log('创建任务')
    }
  });
}

image.png

4. 创建任务

// index.js(部分代码)

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

module.exports.showAll = async () => {
  const list = await db.read()
  // 展示 todo 列表
  inquirer.prompt({
    // ...
  }).then(answer => {
    const index = parseInt(answer.index)
    // 选择任务
    if (index >= 0) {
      // 展示操作列表
      inquirer.prompt({
        // ...
      }).then(answer => {
        // 执行操作
        // ...
      })
    }
    // 创建任务
    if (index === -2) {
      inquirer.prompt({
        type: 'input',
        name: 'title',
        message: '请输入任务标题'
      }).then(answer => {
        list.push({ title: answer.title, done: false })
        db.write(list)
      })
    }
  });
}

image.png

5. 优化代码

// index.js(部分代码)

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

module.exports.showAll = async () => {
  const list = await db.read()
  showTodoList(list).then(answer => {
    const index = parseInt(answer.index)
    if (index >= 0) {
      showActionList().then(answer => {
        doAction(list, index, answer.action)
      })
    }
    if (index === -2) {
      createTask(list)
    }
  });
}

// 展示 todo 列表
function showTodoList(list) {
  return inquirer.prompt({
    type: 'list',
    name: 'index',
    message: '请选择你想操作的任务',
    choices: [
      { name: '退出', value: '-1' },
      ...list.map((task, index) => {
        return { name: `${task.done ? '[√]' : '[_]'} ${index + 1} - ${task.title}`, value: index.toString() }
      }),
      { name: '+ 创建任务', value: '-2' }
    ]
  })
}

// 展示操作列表
function showActionList() {
  return inquirer.prompt({
    type: 'list',
    name: 'action',
    message: '请选择操作',
    choices: [
      { name: '退出', value: 'quit' },
      { name: '已完成', value: 'markAsDone' },
      { name: '未完成', value: 'markAsUndone' },
      { name: '改标题', value: 'updateTitle' },
      { name: '删除', value: 'remove' },
    ]
  })
}

// 执行操作
function doAction(list, index, action) {
  const actions = { markAsDone, markAsUndone, updateTitle, remove }
  actions[action] && actions[action](list, index)
}
function markAsDone(list, index) {
  list[index].done = true
  db.write(list)
}
function markAsUndone(list, index) {
  list[index].done = false
  db.write(list)
}
function updateTitle(list, index) {
  inquirer.prompt({
    type: 'input',
    name: 'title',
    message: '新的标题',
    default: list[index].title
  }).then(answer => {
    list[index].title = answer.title
    db.write(list)
  })
}
function remove(list, index) {
  list.splice(index, 1)
  db.write(list)
}

// 创建任务
function createTask(list) {
  inquirer.prompt({
    type: 'input',
    name: 'title',
    message: '请输入任务标题'
  }).then(answer => {
    list.push({ title: answer.title, done: false })
    db.write(list)
  })
}

五、发布至 npm

配置 package.json:

{
  "name": "node-todo-dong",
  "bin": {            // 指定命令行使用的命令
    "t": "cli.js"
  },
  "files": ["*.js"],  // 告诉 npm 哪些文件是有用的
  "version": "0.0.1",
  "description": "使用 Node.js fs 模块做一个命令行 todo 工具",
  "main": "index.js",
  "repository": "https://gitee.com/barrydong/node-todo.git",
  "author": "BarryDong",
  "license": "MIT",
  "dependencies": {
    "commander": "3.0.2",
    "inquirer": "^8.2.4"
  }
}

给 cli.js 头部加上 Node.js Shebang:

#!/usr/bin/env node

给 cli.js 添加可执行权限:

chmod +x cli.js

检查 npm 源是否是原始源,如果不是,需要切换到 npm 源:

nrm use npm

使用命令行登录 npm:

npm adduser

登录成功后发布:

npm publish