构建首个应用:一个简单的命令行文件浏览器,其功能是允许用户读取和创建文件。
需求:
- 程序需要在命令行运行。这就意味着程序要么通过node命令在执行,要么直接执行,然后如通过终端提供交互给用户输入、输出。
- 程序启动后,需要显示当前目录下列表。
- 选择某个文件,程序需要显示该文件内容。
- 选择一个目录时,程序需要显示该目录下的信息。
- 运行结束后程序退出。
根据上述需求,可以细分几个步骤。
- 创建模块
- 决定采用同步的fs还是异步的fs
- 理解什么事流(Stream)
- 实现输入输出
- 重构
- 使用fs进行文件交互
- 完成
编写首个Node程序
开始基于上述步骤来编写一个模块。模块由几个文件组成,使用任意文本编辑器。
创建模块
新建目录,命名为:file-explorer
首先定义package.json文件,这样既可以方便 NPM中注册的模块依赖进行管理,将来也能对模块进行发布。
尽管此项目仅仅用到Node.js的核心模块API(因此不会从NPM仓库中获取模块),但是,我们还是需要一个简单的package.json文件。
{
"name": "file-explorer",
"version": "0.0.1",
"description": "A command-file file explorer!"
![1.jpg](https://img.haomeiwen.com/i1666407/e791bb177381a46c.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
}
验证package.json文件是否有效,可以运行npm install。
正确不会输出任何内容,否则会抛出JSON异常的错误。
然后创建一个index.js文件。
同步还是异步
由于stido API是全局process对象的一部分,所以,我们的程序唯一的依赖就是fs模块。
/**
* Module dependencies.
*/
var fs = require("fs");
首先获取当前目录的文件列表。
fs模块是唯一一个同时提供同步和异步的API的模块。举个例子,要想获取当前目录的文件列表,可以这样:
console.log(fs.readdirSync(__dirname));
它会立刻返回内容或者当有错误发生时抛出相应异常。
下面是异步版本:
function async(err,files) {
console.log(files);
}
require("fs").readdir(".",async);
我们在之前提到过,要在单线程中创建能够处理高并发的高效程序,就得采用异步、事件驱动的程序。
未标题-1.jpg尽管这个命令行创建并非此类型创建(因为同一时间只会有一个人在读取文件),但是,为了学习node.js中最重要也是最具有挑战的部分,还是保持这种异步的代码风格。
为了获取文件列表,我们需要使用fs.readdir。我们提供的回调函数首个参数是一个错误对象(如果没有错误发生,该对象为Null),另外一个参数是一个files数组:
fs.readdir(".",function(err,files) {
console.log(files);
});
到现在,你知道了fs模块同时提供同步和异步的API来操作文件系统,接下来进入另一个基础概念——流。
理解什么是流(stream)
console.log会输出控制台。事实上,console.log内部做了这样的事情:它在指定的字符串后加上\n
(换行)字符,并将其写到stdout流中。
process全局对象中包含了三个流对象,分别对应三个UNIX标准流:
- stdin:标准输入
- stdout:标准输出
- stderr:标准错误
第一个stdin是一个可读流,而stdout和stderr都是可写流。
stdin流默认的状态是暂停的(paused)。通常,执行一个程序,程序会做一些处理,然后退出,不过,有些时候,程序需要一直处于运行状态来接收用户输入数据。
当回复那个流时,Node会观察对应的文件描述符(在UNIX下为0),随后保持事件循环的运行,同时保持程序不退出,等待事件触发。除非有IO等待,否则node.js总是会自动退出。
流的另外一个属性是它默认的编码。如果在流上设置了编码,那么会得到编码后的字符串(utf-8、ascii等)而不是原始的Buffer作为事件参数。
Steam对象和EventEmitter很像(事实上,前者继承自后者)。在Node中,你会接触到各种类型流,如TCP套接字、HTTP请求等。简而言之,当涉及持续不断地对数据进行读写时,流就出现了。
输入和输出
既然已经知道运行程序后大概是怎样的一个情形了,我们来尝试写第一部分,列出当前目录下的文件,然后等待用户输入。
var fs = require("fs");
fs.readdir(process.cwd(),function(err,files) {
console.log("");
if (!files.length) {
return console.log("\033[31m No files to show!\033[39m\n");
}
console.log(" Select which file or directory you want to see\n'");
function file(i) {
var filename = files[i];
fs.stat(__dirname + "/" + filename, function(err,stat) {
if (stat.isDirectory()) {
console.log(" " + i + " \033[36m" + filename + "/\033[39m");
} else {
console.log(" " + "\033[90m]" + filename + "\033[39m");
}
i++;
if (i == files.length) {
console.log("");
process.stdout.write(" \033[33mEnte your choick: \033[39m");
process.stdin.resume();
} else {
file(i);
}
})
}
file(0);
})
为了输出更加友好,我们首先输出一个空行:
console.log("");
如果files数组为空,告知用户当前目录没有文件。文件周围的\033[31m
和\033[39m
是为了让文本呈现为红色。例子中最后一个字符又是换行符\n,也是为了输出可读性更好。
if (!files.length) {
return console.log("\033[31m No files to show!\033[39m\n");
}
下一行则是让用户执行操作:
console.log(" Select which file or directory you want to see\n'");
紧接着,定义了一个函数,数组中每个元素都会执行该函数。这里也出现了第一种#异步流控制模式:串行执行。
function file (i) {
// ...
}
然后,先获取文件名,再查看文件名对应路径的情况。fs.stat会给出文件或者目录的元数据:
var filename = files[i];
fs.stat(__dirname + "/" + filename, function(err,stat) {
// ...
}
回调函数还给出了错误对象(如果有的话)和一个Stat对象。本例中使用到的Stat对象上的方法是isDirectory
:
if (stat.isDirectory()) {
console.log(" " + i + " \033[36m" + filename + "/\033[39m");
![QQ截图20180824014417.jpg](https://img.haomeiwen.com/i1666407/9023f7e60706dd4b.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
} else {
console.log(" " + "\033[90m]" + filename + "\033[39m");
}
如果路径所代表的是目录,我们就用别于文件的颜色标识出来。
接下来就到了流控制中的核心部分了,计数器不断递增,与此同时,检查是否还有未处理的文件:
i++;
if (i == files.length) {
console.log("");
process.stdout.write(" \033[33mEnte your choick: \033[39m");
process.stdin.resume();
} else {
file(i);
}
如果所有文件处理完毕,此时提示用户进行选择。注意,这里使用的是process.stdout.write而不是console.log,这样就无须换行,让用户可以直接在提示语后进行输入。
console.log("");
process.stdout.write(" \033[33mEnte your choice: \033[39m");
程序提示用户像stdin进行输入
`process.stdin.resume()`:等待用户输入。
`process.stdin.setEncoding('utf8')`:设置流编码为utf8,这样就能支持特殊字符了。
如果还有未处理的文件,则递归调用函数来进行处理:
file(i);
直到列出所有文件、用户输入完毕后,紧接着进行下一步串行处理。
重构
要做重构,我们从为几个常用的变量(stdin和stdout)创建快捷变量开始:
var fs = require("fs"),
stdin = process.stdin,
stdout = process.stdout
由于书写的代码都是异步的,因此,会有这样的问题:随着函数量的增长(特别是流控制层的增加),许多的函数嵌套会让程序的可读性变差。
为了避免此类问题,我们可以为每一个异步操作预先定义一个函数。
首先,我们抽离出一个读取stdin函数:
// called for each file walked in the directory
function file(i) {
var filename = files[i];
fs.stat(__dirname + "/" + filename, function (err, stat) {
if (stat.isDirectory()) { // 判定是否为一个目录还是一个文件
console.log(" " + i + " \033[36m" + filename + "/\033[39m");
} else {
console.log(" " + i + " \033[90m" + filename + "\033[39m");
}
if (++i === files.length) {
read();
} else {
file(i);
}
});
}
// read user input when files are shown
function read() {
console.log("");
stdout.write(" \033[33mEnte your choice: \033[39m");
stdin.resume();
stdin.setEncoding("utf8");
}
注意,上述代码所使用的是新的stdin的引用和stdout的引用。
读取用户输入后,接下来要做的就是根据用户输入做出相应处理。用户需要选择要读取的文件,所以,代码层面,设置了stdin的编码后,就开始监听其data事件:
function read() {
// ...
stdin.on("data",option)
}
// called with the option supplied by the user
function option(data) {
if (!files[Number(data)]) {
stdout.write(" \033[31mEnter your choice: \033[39m");
} else {
stdin.pause();
}
}
这里检查用户的输入是否匹配files数组的下标。还记得files数组是fs.readdir回调函数中的一部分吧。另外,注意的是,上述代码中,我们将utf-8编码的字符串类型data转化为Number类型来方便做检查。
如果检查通过,我们要确保再次将流暂停(回到默认状态),以便于之后做完fs操作后,程序顺利退出。
现在程序能够与用户进行交互了,将当前目录的文件列表展现给用户,下面来实现读取和显示文件内容。
用fs进行文件操作
定位到文件,读取它:
function option(data) {
var filename = files[Number(data)];
if (!filename) {
stdout.write(" \033[31mEnter your choice: \033[39m");
} else {
stdin.pause();
fs.readFile(__dirname + "/" + filename , "utf8" , function(err,data) {
console.log("");
console.log(
"\033[90m" + data.replace(/(.*)/g,' $1') + "\033[39m"
);
});
}
}
提醒:我们可以事先指定编码,这样得到的数据就是相应的字符串了:
fs.raedFile(__dirname + '/' + filename, "utf8' , function(err,data) {
// ...
})
接着,可以使用正则表达式添加一些辅助缩进后将文件内容进行输出:
data.replace(/(.*)/g , ' $1')
不过,要是选择的是目录呢?这种情况下,应当将其目录下的文件列表显示出来。
为了避免再次执行fs.stat,我们在file函数中,将Stat对象保存了下来:
// ...
var stats = [];
function file(i) {
var filename = files[i];
fs.stat(__dirname + "/" + filename , function(err,stat) {
stats[i] = stat;
//...
})
}
现在可以轻松地在option函数中进行检查操作了。
最终源码:
// 模块依赖
var fs = require('fs'),
stdin = process.stdin,
stdout = process.stdout;
// 读取当前目录下的文件内容
// 返回当前进程的目录路径
// console.log(process.cwd());
fs.readdir(process.cwd(), function (err, files) {
// 将文件保存到 files 数组中
// console.log(files);
// 空行
console.log('');
// 当没有文件的提示信息
if (!files.length) {
return console.log('没有文件');
}
// 有文件的提示信息
console.log('请选择你所看见的文件或者目录');
// 保存目录
var stats = {};
// 遍历文件--目录还是文件
// i 表示选择文件的索引
function file(i) {
var filename = files[i];
// console.log(filename);
// 返回文件或者目录的元数据
fs.stat(__dirname + '/' + filename, function (err, stat) {
stats[i] = stat;
// 判断目录或者文件
if (stat.isDirectory()) {
console.log(' \033[36m' + i + ' ' + filename + '\033[39m');
} else {
console.log(' ' + i + ' ' + filename);
}
// 遍历完毕
// console.log(files.length);
if (++i == files.length) {
read();
} else {
// 递归
file(i);
}
});
}
// 读取用户输入的信息
function read() {
// 空行
console.log('');
// 提示输入目录名称信息---不换行
stdout.write('请输入你的选择(数字):');
// 标准输入流默认是暂停的,我们要恢复它
// 等待用户输入
stdin.resume();
// 编码
stdin.setEncoding('utf8');
// 监听用户的输入
stdin.on('data', option);
}
// 用户输入的信息
function option(data) {
// 选择文件的名称
var filename = files[Number(data)];
// console.log(filename);
if (!filename) {
stdout.write('请输入你的选择(数字):');
} else {
stdin.pause();
// 读取文件的内容
if (stats[Number(data)].isDirectory()) {
fs.readdir(__dirname + '/' + filename, function (err, files) {
console.log('');
console.log('(' + files.length + ' 个文件)');
// 遍历文件名称
files.forEach(function (file) {
console.log(' - ' + file);
});
console.log('');
})
} else {
fs.readFile(__dirname + '/' + filename, 'utf8', function (err, data) {
console.log('');
// 行缩进
console.log(data.replace(/(.*)/g, ' $1'));
})
}
}
}
file(0);
})
注意点:
process.cwd()
和__dirname
的区别:
process.cwd()
:运行当前脚本的工作目录的路径process.cwd()
__dirname
:是被执行的js 文件的地址 ——文件所在目录__dirname
argv
process.argv
包含了所有Node程序运行时的参数值:
返回一个数组,第一个元素为
process.execPath
,第二个元素为当前执行的JavaScript文件路径。剩余的元素为其他命令行参数。
--
退出
要让一个应用退出,可以调用process.exit并提供一个退出代码。比如,当发生错误时,要退出程序,这个时候最好使用退出代码。
console.error("An error occurred");
process.exit();
ANSI转义码
要在文本终端下控制格式、颜色以及其他输出选项,可以使用ANSI转义码。
在文本周围添加的明显不用于输出的字符,称为非打印字符。
console.log('\033[90m' + data.replace(/(.*)/g , ' $1') + '\033[39m')
-
\033
表示转义序列开始。 -
[
表示开始颜色设置。 -
90
表示前景色为亮灰色。 -
m
表示颜色设置结束。
结尾的39
用来将颜色再设置回去。
fs
fs模块允许通过Stream API来对数据进行读写操作。与readFile及writeFile方法不同,它对内存的分配不是一次完成的。
比如,有一个大文件,文件内容上百万行逗号分隔文本组成。要完成的读取该文件进行解析,意味着一次性分配很大的内存。更好的方式应当是一次只读取一块内容,以行尾结束符("\n")来切分,然后再逐块进行解析。
fs.createReadStream
方法允许为一个文件创建一个可读的Stream对象。
来看例子:
fs.readFile("my-file.txt" , function(err,contents) {
// 对文件进行处理
})
上述例子中,回调函数必须要等到整个文件读取完毕、载入到RAM、 可用的情况下才会触发。
而下面的例子,每次会读取可变大小的内容块,并且每次读取之后会触发回调函数:
var stream = fs.createReadStream("my-file.txt");
stream.on("data",function(chunk) {
// 处理文件部分内容
});
stream.on("end",function(chunk) {
// 文件读取完毕
})
为什么这种能力很重要呢?假设有个很大的视频文件需要上传到某个Web服务。这时,你无须在读取完整的视频内容后开始上传,使用Stream就可以大大提速上传过程。
这对日志纪录的例子也一样,特别是使用可写stream。假设有个应用需要纪录网站上的访问情况,这时,为了将纪录写到文件中,让操作系统进行打开/关闭文件的操作可能就很低效(每次都要在磁盘上进行查找文件操作)。
所以,这就是一个很好的使用fs.WriteStream
的例子。打开文件操作只做一次,然后写入每个日志项时都调用.write
方法。
监视
Node允许监视文件或目录的变化。监视意味着当文件系统中文件(或者目录)发生变化时,会分发一个事件,然后触发指定的回调函数。
该功能在Node生态系统中被广泛使用。举例来说,有人喜欢用一种可以编译为CSS的语言来书写CSS样式。这个时候,就可以使用监视功能,当源文件发生改变时,就将其编译为CSS文件。
网友评论