前些天公司有个需求需要用到 Node 实现,完成之后觉得自己调自己接口的感觉有点意思。此外,JD 中也总说能够熟练使用 Node 是加分项,现在以一个 ToDo 小应用作为学习 Node 开发的开始。

# 需要实现的需求

在命令行输入形如node todo add/delete/list/done/edit等命令完成对待办事项的增删改查。需求很简单,目的是通过增删基本的使用增进对 Node 的了解。

# 分析如何实现

首先,我们的命令是直接输入在命令行中的,没有图形化界面。那就需要知道 Node 是如何获取命令行中参数的。

对于如何获取参数,谷歌一下,我就知道。 以下是翻译搜索结果中第二条官方文档的解释

使用 process.argv 获取命令行中的参数。通过命令行传递参数是一个非常基本的编程任务,对于任何一个想要尝试编写命令行接口的人来说都是必要的。在 Node.js 以及和 C 相似的环境中,所有被 shell 接收的命令行参数都被传递到了一个叫 argv(即参数值的意思)的数组中。Node.js 以 process.argv 的形式把每一个运行中程序的该数组暴露出来。需要说明的是,参数数组中前两项Node命令所在的文件路径和当前执行脚本所在路径。

获取用户要进行的操作和内容之后,由于 JS 并不具备文件操作的能力,所以需要知道 Node 是如何操纵文件,这其中包含创建并写入甚至删除。这也是持久化存储的能力,需要在关闭进程后仍然能够获取之前的操作内容。

通过搜索Node.js write and read file就可以获得相关的解决方案了。但其实更可行的方式是直接在官方文档中直接搜索write/read这样的关键字。

# 编码实现

通过我们之前的分析,我们可以编写如下代码:

// 获取命令行中除前面两项路径外的参数
let argus = process.argv.slice(2);

// 获取命令行输入的动作和内容
const action = argus[0];
const content = argus[1];
const content1 = argus[2];
1
2
3
4
5
6
7

获取到要执行的动作和参数后,就可以进一步操作了,增加删除或者编辑。这里很直接的想到可以用 switch 语句,不过 if 语句也行。可编写代码如下:

// 使用数组来保存这些操作后的内容
let taskList = []
if (action === "add") {
    taskList.push([content, false]);
}
if (action === "list") {
    console.log(taskList);
}
if (action === "delete") {
    taskList.splice(content - 1, 1);
}
if (action === "done") {
    taskList[content - 1][1] = true;
}
if (action === "edit") {
    taskList[content - 1][0] = content1;
}
// 对于 edit 和 done 的动作,这里简单处理为:完成即把该项的状态标志为true,编辑则把输入的第五个参数覆盖原来的任务内容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

写到这里,可以发现的问题是,每次执行完命令后的任务列表内容消失了。这是因为写入数组的内容只是临时的存在内存中,程序执行完毕之后内存就销毁了。所以需要找个法子把数组的内容长久地存储起来,就好比新建记事本在里头写点东西,下次开机你还是看到里头内容一样。

通过搜索官方文档,我们可以发现文件系统中的 readFile & writeFile API。使用很简单,最基本的是读取内容或者写入内容的路径。

由此,可以编写如下代码

let readContent = fs.readFileSync('F:\\code\demo\mydemo')
fs.writeFileSync("F:\\Code\\Daily-code\\Demo\\todoDb", taskList);

// 实际用第一行时会发现,log出来的内容是形如 `5b 5b 65 68···` 这样的编码,
// 这是因为没有设定文件编码格式,就像HTTP中的`Accept-Language`请求头。
// 所以需要加上第二个参数`utf-8`,或者在得到readContent后使用toString方法转为字符串。
1
2
3
4
5
6

写到这里,似乎已经把小应用做完了。把前面说到的代码合并起来看看。

let fs = require("fs");
let argus = process.argv.slice(2);

const action = argus[0];
const content = argus[1];
const content1 = argus[2];
const dbPath = "F:\\Code\\Daily-code\\Demo\\todoDb";
let readContent;
let taskList = [];

readContent = fs.readFileSync(dbPath, "utf-8");
taskList = JSON.parse(readContent);

if (action === "add") {
    taskList.push([content, false]);
}
if (action === "list") {
    console.log(taskList);
}
if (action === "delete") {
    taskList.splice(content - 1, 1);
}
if (action === "done") {
    taskList[content - 1][1] = true;
}
if (action === "edit") {
    taskList[content - 1][0] = content1;
}
fs.writeFileSync(dbPath, JSON.stringify(taskList));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

再细想下会发现,还存在 bug。比如当一开始数据库文件不存在时,进行读取就会报错。所以需要一个机制来检查当前访问的路劲是否存在,不存在则建立再写入内容。相关的 API 是fs.stat,传入一个文件路径以及回调函数。回调函数中有两个参数 err & stats。可以通过对 err 参数值的判断确认当前数据库文件是否存在。如果 err 为 null,则当前路径文件存在,可读取;如果 err.code 为ENOENt则表明传入的路径不存在(错误码都是大写)。

不过也可以直接使用readFileAPI,前面说到前两个参数,还有第三个参数。即回调函数,和fs.stat的回调函数一样有err & data

选择那个方法完成判断就随个人喜好了。

# 一些优化

路径不能写死 之前的路径是直接写死的绝对路径,当脚本在别处使用的时候路径就没用了,应该使用相对路径。这时可以引入 path 对象,利用其中的join方法—将当前模块的目录名(当前脚本所处目录)和数据库名连接。这样就能保证无论脚本文件在哪里,数据库文件都跟着在哪里。

更好的展示效果 之前的任务列表展示是直接粗暴的展示数组内容,没有经过任何处理,非程序员用户对于 false 应该不懂。可以用一个for循环将列表内容挨个处理,比如完成用X标示。

从 0 还是 1 开始 程序员都知道数组位置从 0 开始计数,但普通用户未必就知道了。所以在编辑、完成和删除的时候都有必要把 index - 1 ,这样才能保证数组中下标为 0 的那项能够获取到。

一笔写于: 9/11/2020, 2:57:23 PM
扫码添加我的微信
个人
个人号
公众号
公众号