实现一个简单的 node 应用之 todo list

前言

学习目标:实现一个简单的 node 应用:todo list。

功能主要有:

  • 添加新任务
  • 清空任务列表
  • 展示所有任务
  • 操作任务
    • 修改任务标题
    • 修改任务状态
    • 删除单个任务

一、环境安装

  1. node
  2. npm

二、项目初始化

  1. 新建文件夹 mkdir node-todo-1
  2. 进入文件目录 cd node-todo-1
  3. 初始化包文件 npm init -y
  4. 新建文件 touch index.js

三、commander.js

官网链接:github.com/tj/commande…

1. 安装依赖

yarn add commander

2. 选项

// index.js
const { program } = require('commander');
program
  .option('-a --add', 'add an item')
  .option('-d, --delete', 'delete an item');

program.parse(process.argv);

.option(flags, desc) 用于定义选项

3. 看看效果

-h 是 --help 的缩写,默认用于显示帮助列表。

node index.js -h

Inkedimage-20210716220142724_LI.jpg

4. 命令

看着文档,复制粘贴,摸索一下。

// index.js
const { program } = require('commander');
// 选项
program
  .option('-a --add', 'add an item')
  .option('-d, --delete', 'delete an item');
// 命令
program
  .command('add') // 在终端中输入:node index add task1 task2 task3
  .argument('<tasks...>', 'taskNameList') // 多参数处理
  .description('The cmd is used to add a task or more tasks.') // 命令描述
  .action((tasks) => { 
    console.log(tasks); // tasks is arguments list
    // 将参数列表合并处理成字符串
    const words = tasks.join(' ');
    console.log(words);
  })
program.parse(process.argv);

.command(nameAndArgs) 用于定义命令,命令名字和输入的参数可以写在一起!

.argument(arg) 用于定义参数,注意多参数的情况要用'<args...>'的形式!

.description(desc) 是命令的描述

.action(fn) 是输入命令后执行的回调

  • 执行看看,node index add task1 task2 task3

    image-20210716224055264.png

四、回调的分离

将 index.js 重命名为 cli.js,区别在于,将 .action() 的内容分离到 index.js 中统一管理。

index.js 中存放各种 api。(添加新任务、清除任务列表、展示所有任务等等)

1. Linux 命令

cat ~/.todo 表示:查看根目录下 .todo 文件中的内容

rm ~/.todo 表示:删除跟目录下的 .todo 文件

2. 添加新任务(功能 1)

  • 引入index.js,使用其中的方法。
// cli.js
const { program } = require('commander');
const api = require('./index')

// 选项
program
  .option('-a --add', 'add an item')
  .option('-d, --delete', 'delete an item');
// 命令
program
  .command('add') // 在终端中输入:node index add task1 task2 task3
  .argument('<tasks...>', 'taskNameList') // 多参数处理
  .description('The cmd is used to add a task or more tasks.') // 命令描述
  .action((tasks) => { 
    const words = tasks.join(' ');
    api.add(words); // 执行add方法,添加新任务到数据库!
  })
program
  .command('clear')
  .description('The cmd is used to clear all tasks.')
  .action((tasks) => { 
    // 将参数列表合并处理成字符串
    const words = tasks.join(' ');
    console.log(words);
  })
program.parse(process.argv);
  • add() 方法是 index.js 中的方法,它定义了触发 add Commander 命令时的处理逻辑。
    • 读取数据库文件 fs.readFile()
    • 添加一个新任务
    • 将新任务写入文件 fs.writeFile()
// index.js
const homedir = require('os').homedir(); // 获取home目录
const home = process.env.HOME || homedir; // 先从系统变量中获取
const path = require('path');
const dbPath = path.join(home, '.todo'); // 数据库路径(拼接而来的)
const fs = require('fs');

module.exports.add = (taskContent) => {
  // 1.读取文件
  fs.readFile(dbPath, {flag: 'a+'}, (err, data) => {
    if (err)  { 
      console.log(err); 
    } else {
      let list;
      try {
        // 此处的 data.toString() 应是一个JSON字符串,需要转换为真的数组!
        list = JSON.parse(data.toString());
      } catch (error) {
        // 如果报错,说明没有这样的数据,就创建一个新的数组!
        list = [];
      }
      // 2.添加一个任务
      const task = {
        title: taskContent,
        completed: false
      }
      list.push(task); // 将新建的任务推进 list 中
      // 3.将任务存储到文件
      const string = JSON.stringify(list); // 将 list 转换为 JSON 字符串
      // 将数据写入文件中
      fs.writeFile(dbPath, string, (err) => {
        if (err) {
          console.log(err);
          return;
        }
      })
    }
  })
}

Node.js 内置了很多模块,可以获取宿主环境中的某些信息。文档查阅:devdocs.io/

  • os 模块是操作系统模块,os.homedir() 可以获取系统的根目录路径。 image-20210717181717304.png

  • process.env.HOME 可以获取进程中设置的 HOME 变量的对应路径。 image-20210717181939813.png

  • path 模块是路径模块,path.join(...path) 可以拼接多个路径。 image-20210717182129247.png

  • fs 模块是文件模块,

最后,尝试一下:

image-20210717182629993.png

3. 方法封装之面向接口编程

在“添加任务”的三个步骤中,希望一个步骤就是一条执行语句,而非现在这样一堆代码冗在那里!

也就是说先设计好接口,然后封装代码,以后使用某个功能时,只需要调用对应的接口即可。

  • 数据库中存放读写操作:
    • 注意 fs.readFile() 以及 fs.writeFile() 都是异步操作,因此不可以直接 return 结果。
    • 利用 Promise 对象改写异步操作,
    • 并且在出错时,直接 return reject(err);(直接返回失败的理由,不执行下面的代码。)
// db.js
const homedir = require('os').homedir();
const home = process.env.HOME || homedir;
const path = require('path');
const dbPath = path.join(home, '.todo');
const fs = require('fs');

const db = {
  // 1. 读取文件
  read (path = dbPath) {
    return new Promise ((resolve, reject) => {
      fs.readFile(path, {flag: 'a+'}, (err, data) => {
        if (err) return reject(err);

        let list;
        try {
          list = JSON.parse(data.toString());
        } catch (error) {
          list = [];
        }
        resolve(list);
      })
    })
  },
  // 2. 写入文件
  write (list, path = dbPath) {
    return new Promise((resolve, reject) => {
      const string = JSON.stringify(list);
      fs.writeFile(path, string, (err) => {
        if (err) return reject(err);
        resolve();
      })
    })
  }
}

module.exports = db;
  • 接口调用
// index.js
const db = require('./db');

module.exports.add = async (taskContent) => {
  // 1.读取文件
  const list = await db.read();
  // 2.添加一个任务
  list.push({title: taskContent, completed: false});
  // 3.将任务写入文件
  await db.write(list);
}

最后,尝试一下:

image-20210717205834780.png

4. 清除任务列表(功能 2)

直接写入一个空的数组即可:

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

// 选项
program
  .option('-a --add', 'add an item')
  .option('-d, --delete', 'delete an item');
// 命令
// 命令1:添加新任务
program
  .command('add') // 在终端中输入:node index add task1 task2 task3
  .argument('<tasks...>', 'taskNameList') // 多参数处理
  .description('The cmd is used to add a task or more tasks.') // 命令描述
  .action((tasks) => { 
    const words = tasks.join(' ');
    api.add(words)
      .then(() => {
          console.log('添加成功!');
        })
      .catch(err => {
        console.log('添加失败!错误原因:' + err);
      });
  })
// 命令2:清空任务列表
program
  .command('clear')
  .description('The cmd is used to clear all tasks.')
  .action(() => { 
    api.clear()
      .then(() => {
            console.log('清除成功!');
          })
      .catch(err => {
        console.log('清除失败!错误原因:' + err);
      });
  })
program.parse(process.argv);
// index.js
const db = require('./db');

// 添加新任务
module.exports.add = async (taskContent) => {
  // 1.读取文件
  const list = await db.read();
  // 2.添加一个任务
  list.push({title: taskContent, completed: false});
  // 3.将任务写入文件
  await db.write(list);
}

// 清空任务列表
module.exports.clear = async (title) => {
  await db.write([]);
}

最后,尝试一下:

image-20210717215643573.png

5. 展示所有任务(功能 3)

process.argv 表示用户输入在终端的参数个数,官网描述更好:

image-20210717214843313.png

当用户仅输入 node cli.js 两项参数时,展示所有任务:

// cli.js
// 用户直接调用 node cli.js
if (process.argv.length === 2) {
  void api.showAll();
} else {
  program.parse(process.argv);
}
// inidex.js
// ...
// 展示所有任务
module.exports.showAll = async () => {
  // 1. 读出之前的任务
  const list = await db.read();
  // 2. 打印直接的任务
  list.forEach((task, index) => {
    console.log(`${task.completed ? '[x]' : '[_]'} ${index + 1} -> ${task.title}`);
  });
}

五、inquirer

inquirer 是一个用户与命令行交互工具。github.com/SBoudrias/I…

1. 安装依赖

yarn add inquirer

当展示出所有的任务后,实际上需要上下移动光标,然后执行后续的操作,因此就要借助 inquirer 库来实现这个目标!

2. 操作任务(功能 4)

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



// 添加新任务
module.exports.add = async (taskContent) => {
  // 1.读取文件
  const list = await db.read();
  // 2.添加一个任务
  list.push({title: taskContent, completed: false});
  // 3.将任务写入文件
  await db.write(list);
}

// 清空任务列表
module.exports.clear = async () => {
  await db.write([]);
}

// 展示所有事项
module.exports.showAll = async () => {
  // 1. 读出之前的任务
  const list = await db.read();
  // 2. 打印之前的任务
  // list.forEach((task, index) => {
  //   console.log(`${task.completed ? '[x]' : '[_]'} ${index + 1} -> ${task.title}`);
  // });
	
  // 发起询问
  inquirer
    .prompt({
        type: 'list',
        name: 'index',
        message: '你想要执行哪一项任务?',
        choices: [
          { name: '+ 添加任务', value: '-2'},
          { name: '- 退出', value: '-1'},
          ...list.map((task, index) => {
            return { name: `${task.completed ? '[x]' : '[_]'} ${index + 1} -> ${task.title}`, value: index }
          })
        ],
      })
    .then((answer) => {
      const index = parseInt(answer.index);
      if (index >= 0) {
        // 选中了一个任务
        inquirer.prompt({
          type: 'list',
          name: 'action',
          message: '请选择操作',
          choices: [
            {name: '退出', value: 'quit'},
            {name: '已完成', value: 'completed'},
            {name: '未完成', value: 'incomplete'},
            {name: '改标题', value: 'updateTitle'},
            {name: '删除', value: 'remove'},
          ]
        }).then(answer => {
          console.log(answer.action);
          switch (answer.action) {
            case 'completed':
              list[index].completed = true;
              db.write(list)
              break;
            case 'incomplete':
              list[index].completed = 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,
            completed: false
          })
          db.write(list);
        })
      }
    });
}

image-20210718142731832.png

3. 代码优化

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

// 1. 添加新任务
module.exports.add = async (taskContent) => {
  // 1.读取文件
  const list = await db.read();
  // 2.添加一个任务
  list.push({title: taskContent, completed: false});
  // 3.将任务写入文件
  await db.write(list);
}

// 2. 清空任务列表
module.exports.clear = async () => {
  await db.write([]);
}

// 3.2.2 添加新任务
function askForAddNewTask (list) {
  inquirer.prompt({
    type: 'input',
    name: 'title',
    message: '请添加新任务标题',
  }).then(answer => {
    list.push({
      title: answer.title,
      completed: false
    })
    db.write(list);
    console.log('添加成功!');
  })
}

// 3.2.1.1 设置已完成状态
async function setCompletedState (list, index) {
  list[index].completed = true;
  await db.write(list);
  console.log('当前任务已完成!');
}
// 3.2.1.2 设置未完成状态
async function setIncompleteState (list, index) {
  list[index].completed = false;
  await db.write(list);
  console.log('当前任务待完成...');
}
// 3.2.1.3 修改标题
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);
    console.log('标题更新成功!');
  });
}
// 3.2.1.4 移除任务
async function removeTask (list, index) {
  list.splice(index, 1);
  await db.write(list);
  console.log('删除成功!');
}

// 3.2.1 后续操作
function askForNextAction (list, index) {
  const actions = {
    setCompletedState,
    setIncompleteState,
    updateTitle,
    removeTask
  }
  inquirer.prompt({
    type: 'list',
    name: 'action',
    message: '请选择操作',
    choices: [
      {name: '退出', value: 'quit'},
      {name: '已完成', value: 'setCompletedState'},
      {name: '未完成', value: 'setIncompleteState'},
      {name: '改标题', value: 'updateTitle'},
      {name: '删除', value: 'removeTask'},
    ]
  }).then(answer => {
    const currentAction = actions[answer.action];
    currentAction && currentAction(list, index);

    // switch (answer.action) {
    //   case 'setCompletedState':
    //     setCompletedState(list, index);
    //     break;
    //   case 'setIncompleteState':
    //     setIncompleteState(list, index);
    //     break;
    //   case 'updateTitle':
    //     updateTitle(list, index);
    //     break;
    //   case 'removeTask':
    //     removeTask(list, index);
    //     break;
    // }
  })
}

// 3.2 打印之前的任务 + 后续操作
function displayTasks (list) {
  inquirer
  .prompt({
      type: 'list',
      name: 'index',
      message: '你想要执行哪一项任务?',
      choices: [
        { name: '+ 添加任务', value: '-2'},
        { name: '- 退出', value: '-1'},
        ...list.map((task, index) => {
          return { name: `${task.completed ? '[x]' : '[_]'} ${index + 1} -> ${task.title}`, value: index }
        })
      ],
    })
  .then((answer) => {
    const index = parseInt(answer.index);
    if (index >= 0) {
      // 3.2.1 选中了一个任务,执行后续操作
      askForNextAction(list, index);
    } else if (index === -2) {
      // 3.2.2 添加新任务
      askForAddNewTask(list);
    }
  });
}

// 3. 展示所有事项
module.exports.showAll = async () => {
  // 3.1 读出之前的任务
  const list = await db.read();
  // 3.2 打印之前的任务
  displayTasks(list);
}

六、代码发布

1. 设置 shebang

让用户自动执行 node,参考:zhuanlan.zhihu.com/p/262456371

在 cli.js 中的首行添加一段 shebang 代码:

// cli.js
#!/usr/bin/env node

2. 配置 package.json

{
  "name": "cpc-node-todo-1",
  "bin": {
    "cpc-todo": "./cli.js"
  },
  "files": [
    "cli.js",
    "db.js",
    "index.js"
  ],
  "version": "0.0.3",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "commander": "^8.0.0",
    "inquirer": "^8.1.2"
  }
}
  • name 字段表示,包的名称,以后通过这个名称来下载使用。

  • bin 字段表示,以后用户在终端中可直接运行的命令。

    image-20210718161116661.png

  • files 字段表示,需要打包上传的文件。这里是 cli.js、index.js、db.js 三个文件,当然如果说,你的目录里就只有这三个 js 文件的话,那么该字段就可以直接写成: "files": ["*.js"]

3. 发布到 NPM

  1. npm login (此步骤需要填写用户名和密码,以及邮箱。)

  2. npm publish

  3. npm logout

4. 下载使用

  1. 打开终端,全局安装:npm i -g cpc-node-todo-1

  2. 安装好后,通过 cpc-todo 来调用这个 node 应用,注意这个 cpc-todo 就是打包之前 package.json 中设置的 bin 字段的内容。

    image-20210718162342275.png

  3. 如果不想用了,通过以下命令来卸载即可: npm un -g cpc-todo-1

5. 包的更新

略。

七、单元测试

1. jest

www.jestjs.cn/docs/gettin…

npm install --save-dev jest

待更新...

添加我的微信:enjoy_Mr_cat,共同成长,卷卷群里等你 🤪。

posted @ 2021-12-19 11:49  见嘉于世  阅读(0)  评论(0编辑  收藏  举报  来源