实现一个简单的 node 应用之 todo list
前言
学习目标:实现一个简单的 node 应用:todo list。
功能主要有:
- 添加新任务
- 清空任务列表
- 展示所有任务
- 操作任务
- 修改任务标题
- 修改任务状态
- 删除单个任务
一、环境安装
- node
- npm
二、项目初始化
- 新建文件夹
mkdir node-todo-1
- 进入文件目录
cd node-todo-1
- 初始化包文件
npm init -y
- 新建文件
touch index.js
三、commander.js
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
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
四、回调的分离
将 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() 可以获取系统的根目录路径。
-
process.env.HOME 可以获取进程中设置的 HOME 变量的对应路径。
-
path 模块是路径模块,path.join(...path) 可以拼接多个路径。
-
fs 模块是文件模块,
-
文件读取:fs.readFile(filePath, options, (err, data) => {}) devdocs.io/node~14_lts…
-
文件写入:fs.writeFile(filePath, data, (err) => {}) devdocs.io/node~14_lts…
-
最后,尝试一下:
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);
}
最后,尝试一下:
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([]);
}
最后,尝试一下:
5. 展示所有任务(功能 3)
process.argv 表示用户输入在终端的参数个数,官网描述更好:
当用户仅输入 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);
})
}
});
}
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 字段表示,以后用户在终端中可直接运行的命令。
-
files 字段表示,需要打包上传的文件。这里是 cli.js、index.js、db.js 三个文件,当然如果说,你的目录里就只有这三个 js 文件的话,那么该字段就可以直接写成: "files": ["*.js"]
3. 发布到 NPM
-
npm login (此步骤需要填写用户名和密码,以及邮箱。)
-
npm publish
-
npm logout
4. 下载使用
-
打开终端,全局安装:
npm i -g cpc-node-todo-1
-
安装好后,通过 cpc-todo 来调用这个 node 应用,注意这个 cpc-todo 就是打包之前 package.json 中设置的 bin 字段的内容。
-
如果不想用了,通过以下命令来卸载即可: npm un -g cpc-todo-1
5. 包的更新
略。
七、单元测试
1. jest
npm install --save-dev jest
待更新...
添加我的微信:enjoy_Mr_cat,共同成长,卷卷群里等你 🤪。