终极目标:掌握和使用node

本博客目的:记录node学习的进度和心得

内容:Nodejs中的fs模块的使用。

fs模块

​ fs模块用于文件操作,是node内置的模块。

​ 有大量的方法:(这些方法可以通过vscode中ctrl+鼠标点击打开源码,查看相关参数等)

image-20200510161928651

​ 为了举例说明相关操作,我们创建一个文件夹,里面先使用npm init –yes强制生成一个package.json文件。然后创建入口文件app.js。

image-20200510162258361

​ 首先,我们使用这个fs模块时,先导入image-20200510162537977

  1. fs.stat 检测是文件还是目录

    判断html是文件还是目录(先手动创建html文件夹):

    image-20200510162708192

    结果(命令行输入node app.js):

    image-20200510162727095

    当判断package.json是文件还是目录:

    image-20200510162848535

    image-20200510162807784

  2. fs.mkdir 创建目录

    通常用于如果判断一个目录是否存在,如果不存在则用这个方法创建目录。

    例如用这个方法创建css目录:

    image-20200510163338487

    如果目录以存在则报错说已存在,不存在则创建。上面的mode参数和callback参数可以不写。

  3. fs.writeFile 创建写入文件

    例如我们可以在html文件夹里面创建index.html文件:

    image-20200510163728992

    结果(写入成功):

    image-20200510163846590

    如果对同一个文件写入,则内容替换:

    image-20200510163945228

    image-20200510163953989

  4. fs.appendFile 追加文件

    在之前的文件基础上进行追加,如果之前文件不存在,则是创建。

    例如在css文件夹里追加base.css,因为一开始不存在,则创建;之后继续再对这个文件追加其他信息,则在这个文件基础上,追加内容:

    image-20200510164212024

    image-20200510164347535

  5. fs.readFile 读取文件

    例如读取之前我们创建好的html下的index.html:

    image-20200510164609301

    image-20200510164721861

    有16进制的buffer数据(原来的data);

    需要通过toString()方法把buffer转换成string类型。

    如果路径写错了,会报错。

  6. fs.readdir读取目录

    例如可以读取当前目录下目录和文件。

    image-20200510164949174

    image-20200510165014086

    image-20200510165028700

  7. fs.rename 重命名 移动文件

    有两个功能:功能:1、表示重命名 2、移动文件

    image-20200510165212013

    把aaa.css修改为index.css。

    image-20200510165406301

    把css下的aaa.css移动到html/index.css下。

  8. fs.rmdir 删除目录(remove)

    image-20200510165444859

    如果目录里面有文件,这样删除会报错。

    所以需要把当前目录下的文件删除后,才能删除这个目录。

  9. fs.unlink 删除文件

    删除文件:

    image-20200510165507108

应用例子1:

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
30
31
32
// 判断服务器上面有没有upload目录。如果没有创建这个目录,如果有的话不做操作。   

const fs=require('fs');
var path='./upload';

fs.stat(path,(err,data)=>{
if(err){
//执行创建目录
mkdir(path);
return;
}
if(!data.isDirectory()){
//首先删除文件,再去执行创建目录
fs.unlink(path,(err)=>{
if(!err){
mkdir(path);
}else{
console.log('请检测传入的数据是否正确');
}
})
}
})

//创建目录的方法
function mkdir(dir){
fs.mkdir(dir,(err)=>{
if(err){
console.log(err);
return;
}
});
}

注意:upload如果事先是一个文件时,需要先删除文件,再去执行创建目录。直接mkdir(path)在有同名文件的时候,不会执行。

这个例子也可以使用第三方包mkdirp,不仅能创建文件目录,还能创建层级目录。

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
30
/*
1、https://www.npmjs.com/package/mkdirp

2、cnpm i mkdirp --save / npm i mkdirp --save

3、var mkdirp = require('mkdirp');

4、看文档使用

*/

var mkdirp = require('mkdirp');
//单层级目录
// mkdirp('./upload', function (err) {
// if (err) {
// console.error(err);
// }
// });



//单层级目录
// mkdirp('./uploadDir');

//多层级目录
mkdirp('./upload/aaa/xxxx', function (err) {
if (err) {
console.error(err);
}
});

应用例子2:wwwroot文件夹下面有images css js 以及index.html , 找出 wwwroot目录下面的所有的目录,然后放在一个数组中

image-20200511154027457

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
const fs=require('fs');

//错误的写法 注意:fs里面的方法是异步
/*
var path='./wwwroot';
var dirArr=[];
fs.readdir(path,(err,data)=>{
if(err){
console.log(err);
return;
}
for(var i=0;i<data.length;i++){
fs.stat(path+'/'+data[i],(error,stats)=>{
if(stats.isDirectory()){
dirArr.push(data[i]);
}
})
}
console.log(dirArr);
})
console.log(dirArr);
*/


//打印出 3个3
/*
for(var i=0;i<3;i++){
setTimeout(function(){
console.log(i);
},100)
}
*/




//1、改造for循环 递归实现 2、nodejs里面的新特性 async await


var path='./wwwroot';
var dirArr=[];
fs.readdir(path,(err,data)=>{
if(err){
console.log(err);
return;
}
(function getDir(i){
if(i==data.length){ //执行完成
console.log(dirArr);
return;
}
fs.stat(path+'/'+data[i],(error,stats)=>{
if(stats.isDirectory()){
dirArr.push(data[i]);
}
getDir(i+1)//自执行
})
})(0)
})

注意:错误写法中,由于fs是异步的,对于异步操作:

​ 例如我们后面循环异步打印i,最后的输出结果是三个3,这是和JS事件循环机制有关。js事件处理器(DOM事件、timer、AJax)在(主)线程空闲时间不会优先运行处理,即先执行主线程的for循环的i赋值与迭代,当主线程结束后,才处理异步进程,导致最后运行这些异步事件的时候,输出的都是i最后的值。

​ 因此,我们的使用fs模型的第一块代码,由于i最后为真实data的length,而data[length]里面是为空,导致最后输出的数组为空数组。

​ 所以解决的方法有:

​ 1、改造for循环,用递归实现(但是可读性比较差,通常用方法2)

​ 写成匿名立即执行函数,相当于加一层闭包,i以函数参数形式传递给内层函数 。

​ 2、nodejs里面的新特性(8.x之后) async await

nodejs里面的新特性(8.x之后) async await

​ 首先,先介绍一下ES6的常用语法,主要有:

  1. let const (声明变量与常量)

  2. 箭头函数 (()=>{},this指向上下文)

  3. 对象、属性的简写(属性名称与变量名一致时,

    image-20200513150812774

    image-20200513151039781

  4. 模板字符串(image-20200513150700789

  5. Promise。主要用来处理异步(涉及JS事件循环)

    例子1:

    image-20200513151538108

    image-20200513151548147

    会报错说,name没有定义,因为setTimeout是异步方法,在运行打印及执行getData的主进程中,setTimeout还没执行(在异步进程队列中),因而里面name是取不到的。

    例子2:

    之前ES5通常的解决上述问题的方法是设置回调函数。

    image-20200513151851571

    image-20200513151900580

    打印函数是嵌套在setTimeout的外层,相当于提供了一个钩子,当setTimeout异步执行里面程序,1秒后,name赋值为张三,并且使用了callback,即正确打印了name。

    例子3:

    在ES6后,有了Promise的API,方便我们处理异步。

    Promise来处理异步 resolve 成功的回调函数(与.then()配合) reject失败的回调函数(与.catch()配合)

    所以,上例子2可以改写为:

    image-20200513152817994

    结果也是一秒后打印:”张三“

    Async与Await和promise的使用

    ​ async 是“异步”的简写,而await 可以认为是async wait 的简写。所以应该很好理解async用于申明一个异步的function ,而await 用于等待一个异步方法执行完成。
    简单理解:
    async 是让方法变成异步。
    await 是等待异步方法执行完成。

    详细说明:
    async 是让方法变成异步(用于声明),在终端里用node 执行这段代码,你会发现输出了Promise {‘您好nodejs’ },这时候会发现它返回的是封装的Promise。

    image-20200513153441050

    如果我们直接向要’您好nodejs‘的字符串而不是promise,可以使用await获取数据。

    await 在等待async 方法执行完毕,其实await 等待的只是一个表达式,这个表达式在官方文档里说的是Promise 对象,但是它也可以接受普通值。注意:await 必须在async 方法中才可以使用因为await 访问本身就会造成程序停止堵塞,所以必须在异步方法中才可以使用。

    但下面的写法是错误的:

    image-20200513153829055

    所以应该写成:

    image-20200513154140330

    所以,通常使用async/await来处理异步时,通常async 会将其后的函数(函数表达式或Lambda)的返回值封装成一个Promise 对象,而await 会等待这个Promise 完成,并将其resolve 的结果返回出来。

    image-20200513154919186

    小结:async和await可以方便我们处理node里的异步:异步方法封装成promise,然后async声明,最后使用await调用结果。

    所以,之前的应用例子2:wwwroot文件夹下面有images css js 以及index.html , 找出 wwwroot目录下面的所有的目录,然后放在一个数组中。

    【使用node的async和await来做:】

    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
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    //1、定义一个isDir的方法判断一个资源到底是目录还是文件
    //定义一个异步的方法,然后再外部通过await获取异步方法里面的数据

    async function isDir(path) {
    return new Promise((resolve,reject) => {
    fs.stat(path, (error, stats) => {
    if (error) {
    console.log(error);
    reject(error)
    return;
    }
    if (stats.isDirectory()) {
    resolve(true);
    } else {
    resolve(false);
    }
    })
    })
    }

    //2、获取wwwroot里面的所有资源 循环遍历

    function main(){//不是 async function main()
    var path='./wwwroot'
    var dirArr=[];
    fs.readdir(path,async (err,data)=>{ //注意是包装await的外层方法用async声明,而不是main()
    if(err){
    console.log(err);
    return;
    }
    for(var i=0;i<data.length;i++){
    if(await isDir(path+'/'+data[i])){
    dirArr.push(data[i]);
    }
    }
    console.log(dirArr);

    })
    }

    main();

image-20200513160740951

知识补充:【后续要理解async/await与promise的区别与联系;执行顺序问题,阻塞问题】

以流的方式读写文件

如果我们的文件比较大的时候,读取数据时,直接读取会存在问题,建议以流的形式读取。类似地,当要把一个大文件写入目录的时候,也以流的方法写入。

流的方式:就是一点一点(以一定量大小)操作

  1. fs.createReadStream 从文件流中读取数据

    image-20200513162652628

    image-20200513162817519

    最后发现读取了6次。

  2. fs.createWriteStream 写入文件

    image-20200513163106375

    注意:

    image-20200513163222764

    才能出发finish事件。

  3. 管道流

    ​ 管道提供了一个输出流到输入流的机制。通常我们用于从一个流中获取数据并将数据传递到另外一个流中。

    image-20200513163509010

    如上面的图片所示,我们把文件比作装水的桶,而水就是文件里的内容,我们用一根管子(pipe)连接两个桶使得水从一个桶流入另一个桶,这样就慢慢的实现了大文件的复制过程。

    ​ 以下实例我们通过读取一个文件内容并将内容写入到另外一个文件中。

image-20200513163623342