Node.js 高级进阶之 fs 文件模块学习

人所缺少的不是才干而是志向,不是成功的能力而是勤劳的意志。 —— 部尔卫javascript

前言

文件操做是开发过程当中并不可少的一部分。Node.js 中的 fs 模块是文件操做的封装,它提供了文件读取、写入、改名、删除、遍历目录、连接等 POSIX 文件系统操做。与其它模块不一样的是,fs 模块中全部的操做都提供了异步和同步的两个版本,具备 sync 后缀的方法为同步方法,不具备 sync 后缀的方法为异步方法java

做者简介:koala,专一完整的 Node.js 技术栈分享,从 JavaScript 到 Node.js,再到后端数据库,祝您成为优秀的高级 Node.js 工程师。【程序员成长指北】做者,Github 博客开源项目 github.com/koala-codin…node

文章概览

  • 计算机中关于系统和文件的一些常识linux

    -- 权限位 modegit

    -- 标识位 flag程序员

    -- 文件描述符 fsgithub

  • Node.js 中 fs 模块的 api 详细讲解与对应 Demo面试

    -- 常规文件操做数据库

    -- 高级文件操做后端

    -- 文件目录操纵

  • Node.js 中 fs 模块的 api 对应 demo

  • fs 模块的应用场景及实战训练(大小文件实现拷贝)

面试会问

说几个fs模块的经常使用函数?什么状况下使用fs.open的方式读取文件?用fs模块写一个大文件拷贝的例子(注意大文件)?

文件常识

计算机中的一些文件知识,文件的权限位 mode、标识位 flag、文件描述符 fd等你有必要了解下。这些内容对于你接下来学习 fs 的 api ,记忆和使用都会有不少帮助。

权限位 mode

由于 fs 模块须要对文件进行操做,会涉及到操做权限的问题,因此须要先清楚文件权限是什么,都有哪些权限。

文件权限表:

在上面表格中,咱们能够看出系统中针对三种类型进行权限分配,即文件全部者(本身)、文件所属组(家人)和其余用户(陌生人),文件操做权限又分为三种,读、写和执行,数字表示为八进制数,具有权限的八进制数分别为 421,不具有权限为 0。

为了更容易理解,咱们能够随便在一个目录中打开 Git,使用 Linux 命令 ls -al 来查目录中文件和文件夹的权限位

drwxr-xr-x 1 koala 197121 0 Jun 28 14:41 core
-rw-r--r-- 1 koala 197121 293 Jun 23 17:44 index.md
复制代码

在上面的目录信息当中,很容易看出用户名、建立时间和文件名等信息,但最重要的是开头第一项(十位的字符)。

第一位表明是文件仍是文件夹,d 开头表明文件夹,- 开头的表明文件,然后面九位就表明当前用户、用户所属组和其余用户的权限位,按每三位划分,分别表明读(r)、写(w)和执行(x),- 表明没有当前位对应的权限。

权限参数 mode 主要针对 Linux 和 Unix 操做系统,Window 的权限默认是可读、可写、不可执行,因此权限位数字表示为 0o666,转换十进制表示为 438。

标识位 flag

Node.js 中,标识位表明着对文件的操做方式,如可读、可写、便可读又可写等等,在下面用一张表来表示文件操做的标识位和其对应的含义。

符号 含义
r 读取文件,若是文件不存在则抛出异常。
r+ 读取并写入文件,若是文件不存在则抛出异常。
rs 读取并写入文件,指示操做系统绕开本地文件系统缓存。
w 写入文件,文件不存在会被建立,存在则清空后写入。
wx 写入文件,排它方式打开。
w+ 读取并写入文件,文件不存在则建立文件,存在则清空后写入。
wx+ 和 w+ 相似,排他方式打开。
a 追加写入,文件不存在则建立文件。
ax 与 a 相似,排他方式打开。
a+ 读取并追加写入,不存在则建立。
ax+ 与 a+ 相似,排他方式打开。

上面表格就是这些标识位的具体字符和含义,可是 flag 是不常用的,不容易被记住,因此在下面总结了一个加速记忆的方法。

  • r:读取
  • w:写入
  • s:同步
  • +:增长相反操做
  • x:排他方式

r+ 和 w+ 的区别,当文件不存在时,r+ 不会建立文件,而会抛出异常,但 w+ 会建立文件;若是文件存在,r+ 不会自动清空文件,但 w+ 会自动把已有文件的内容清空。

文件描述符 fs

操做系统会为每一个打开的文件分配一个名为文件描述符的数值标识,文件操做使用这些文件描述符来识别与追踪每一个特定的文件,Window 系统使用了一个不一样但概念相似的机制来追踪资源,为方便用户,NodeJS 抽象了不一样操做系统间的差别,为全部打开的文件分配了数值的文件描述符。

在 Node.js 中,每操做一个文件,文件描述符是递增的,文件描述符通常从 3 开始,由于前面有 0、一、2 三个比较特殊的描述符,分别表明 process.stdin(标准输入)、process.stdout(标准输出)和 process.stderr(错误输出)。

文件操做

完整性读写文件操做

文件读取-fs.readFile

fs.readFile(filename,[encoding],[callback(error,data)]

文件读取函数

  1. 它接收第一个必选参数filename,表示读取的文件名。
  2. 第二个参数 encoding 是可选的,表示文件字符编码。
  3. 第三个参数callback是回调函数,用于接收文件的内容。 说明:若是不指定 encoding ,则callback就是第二个参数。 回调函数提供两个参数 err 和 data , err 表示有没有错误发生,data 是文件内容。 若是指定 encoding , data是一个解析后的字符串,不然将会以 Buffer 形式表示的二进制数据。

demo:

const fs = require('fs');
const path = require('path');
const filePath = path.join(__dirname,'koalaFile.txt')
const filePath1 = path.join(__dirname,'koalaFile1.txt')
// -- 异步读取文件
fs.readFile(filePath,'utf8',function(err,data){
    console.log(data);// 程序员成长指北
});

// -- 同步读取文件
const fileResult=fs.readFileSync(filePath,'utf8');
console.log(fileResult);// 程序员成长指北
复制代码

文件写入fs.writeFile

fs.writeFile(filename,data,[options],callback)
复制代码

文件写入操做

  1. 第一个必选参数 filename ,表示读取的文件名
  2. 第二个参数要写的数据
  3. 第三个参数 option 是一个对象,以下
encoding {String | null} default='utf-8'
mode {Number} default=438(aka 0666 in Octal)
flag {String} default='w'
复制代码

这个时候第一章节讲的计算机知识就用到了,flag值,默认为w,会清空文件,而后再写。flag值,r表明读取文件,w表明写文件,a表明追加。

demo:

// 写入文件内容(若是文件不存在会建立一个文件)
// 写入时会先清空文件
fs.writeFile(filePath, '写入成功:程序员成长指北', function(err) {
    if (err) {
        throw err;
    }
    // 写入成功后读取测试
    var data=fs.readFileSync(filePath, 'utf-8');
    console.log('new data -->'+data);
});

// 经过文件写入而且利用flag也能够实现文件追加
fs.writeFile(filePath, '程序员成长指北追加的数据', {'flag':'a'},function(err) {
	    if (err) {
	        throw err;
	    }
	    console.log('success');
	    var data=fs.readFileSync(filePath, 'utf-8')
	    // 写入成功后读取测试
	    console.log('追加后的数据 -->'+data);
	});
复制代码

文件追加-appendFile

fs.appendFile(filename, data, [options], callback)
复制代码
  1. 第一个必选参数 filename ,表示读取的文件名
  2. 第二个参数 data,data 能够是任意字符串或者缓存
  3. 第三个参数 option 是一个对象,与write的区别就是[options]的flag默认值是”a”,因此它以追加方式写入数据.

说明:该方法以异步的方式将 data 插入到文件里,若是文件不存在会自动建立

demo:

// -- 异步另外一种文件追加操做(非覆盖方式)
// 写入文件内容(若是文件不存在会建立一个文件)
fs.appendFile(filePath, '新数据程序员成长指北456', function(err) {
    if (err) {
        throw err;
    }
    // 写入成功后读取测试
    var data=fs.readFileSync(filePath, 'utf-8');
    console.log(data);
});
// -- 同步另外一种文件追加操做(非覆盖方式)

fs.appendFileSync(filePath, '同步追加一条新数据程序员成长指北789');
复制代码

拷贝文件-copyFile

fs.copyFile(filenameA, filenameB,callback)
复制代码
  1. 第一个参数原始文件名
  2. 第二个参数要拷贝到的文件名 demo:
// 将filePath文件内容拷贝到filePath1文件内容
fs.copyFileSync(filePath, filePath1);
let data = fs.readFileSync(filePath1, 'utf8');

console.log(data); // 程序员成长指北
复制代码

删除文件-unlink

fs.unlink(filename, callback)
复制代码
  1. 第一个参数文件路径你们应该都知道了,后面我就不重复了
  2. 第二个回调函数 callback

demo:

// -- 异步文件删除
fs.unlink(filePath,function(err){
	if(err) return;
});
// -- 同步删除文件
fs.unlinkSync(filePath,function(err){
    if(err) return;
});
复制代码

指定位置读写文件操做(高级文件操做)

接下来的高级文件操做会与上面有些不一样,流程稍微复杂一些,要先用fs.open来打开文件,而后才能够用fs.read去读,或者用fs.write去写文件,最后,你须要用fs.close去关掉文件。

特殊说明:read 方法与 readFile 不一样,通常针对于文件太大,没法一次性读取所有内容到缓存中或文件大小未知的状况,都是屡次读取到 Buffer 中。 想了解 Buffer 能够看 NodeJS —— Buffer 解读。(注意这里换成个人文章)

文件打开-fs.open

fs.open(path,flags,[mode],callback)
复制代码

第一个参数:文件路径 第二个参数:与开篇说的标识符 flag 相同 第三个参数:[mode] 是文件的权限(可选参数,默认值是0666) 第四个参数:callback 回调函数

demo:

fs.open(filePath,'r','0666',function(err,fd){
   console.log('哈哈哈',fd); //返回的第二个参数为一个整数,表示打开文件返回的文件描述符,window中又称文件句柄
})
复制代码

demo 说明:返回的第二个参数为一个整数,表示打开文件返回的文件描述符,window中又称文件句柄,在开篇也有对文件描述符说明。

文件读取-fs.read

fs.read(fd, buffer, offset, length, position, callback);
复制代码

六个参数

  1. fd:文件描述符,须要先使用 open 打开,使用fs.open打开成功后返回的文件描述符;
  2. buffer:一个 Buffer 对象,v8引擎分配的一段内存,要将内容读取到的 Buffer;
  3. offset:整数,向 Buffer 缓存区写入的初始位置,以字节为单位;
  4. length:整数,读取文件的长度;
  5. position:整数,读取文件初始位置;文件大小以字节为单位
  6. callback:回调函数,有三个参数 err(错误),bytesRead(实际读取的字节数),buffer(被写入的缓存区对象),读取执行完成后执行。

demo:

const fs = require('fs');
let buf = Buffer.alloc(6);// 建立6字节长度的buf缓存对象

// 打开文件
fs.open('6.txt', 'r', (err, fd) => {
  // 读取文件
  fs.read(fd, buf, 0, 3, 0, (err, bytesRead, buffer) => {
    console.log(bytesRead);
    console.log(buffer);

    // 继续读取
    fs.read(fd, buf, 3, 3, 3, (err, bytesRead, buffer) => {
      console.log(bytesRead);
      console.log(buffer);
      console.log(buffer.toString());
    });
  });
});

// 3
// <Buffer e4 bd a0 00 00 00>

// 3
// <Buffer e4 bd a0 e5 a5 bd>
// 你好

复制代码

文件写入-fs.write

fs.write(fd, buffer, offset, length, position, callback);
复制代码

六个参数

  1. fd:文件描述符,使用fs.open 打开成功后返回的;
  2. buffer:一个 Buffer 对象,v8 引擎分配的一段内存,存储将要写入文件数据的 Buffer;
  3. offset:整数,从 Buffer 缓存区读取数据的初始位置,以字节为单位;
  4. length:整数,读取 Buffer 数据的字节数;
  5. position:整数,写入文件初始位置;
  6. callback:写入操做执行完成后回调函数,有三个参数 err(错误),bytesWritten(实际写入的字节数),buffer(被读取的缓存区对象),写入完成后执行。

demo:

文件关闭-fs.close

fs.close(fd,callback)
复制代码
  1. 第一个参数:fd 文件open时传递的文件描述符
  2. 第二个参数 callback 回调函数,回调函数有一个参数 err(错误),关闭文件后执行。

demo:

// 注意文件描述符fd
fs.open(filePath, 'r', (err, fd) => {
  fs.close(fd, err => {
    console.log('关闭成功');// 关闭成功
  });
});
复制代码

目录(文件夹)操做

一、fs.mkdir 建立目录

fs.mkdir(path, [options], callback)
复制代码
  1. 第一个参数:path 目录路径
  2. 第二个参数[options],recursive 默认值: false。 mode Windows 上不支持。默认值: 0o777。 可选的 options 参数能够是指定模式(权限和粘滞位)的整数,也能够是具备 mode 属性和 recursive 属性(指示是否应建立父文件夹)的对象。
  3. 第三个参数回调函数,回调函数有一个参数 err(错误),关闭文件后执行。

demo:

fs.mkdir('./mkdir',function(err){
  if(err) return;
  console.log('建立目录成功');
})
复制代码

注意: 在 Windows 上,在根目录上使用 fs.mkdir() (即便使用递归参数)也会致使错误:

fs.mkdir('/', { recursive: true }, (err) => {
  // => [Error: EPERM: operation not permitted, mkdir 'C:\']
});
复制代码

二、fs.rmdir删除目录

fs.rmdir(path,callback)
复制代码
  1. 第一个参数:path目录路径
  2. 第三个参数回调函数,回调函数有一个参数 err(错误),关闭文件后执行。 demo:
const fs = require('fs');
fs.rmdir('./mkdir',function(err){
  if(err) return;
  console.log('删除目录成功');
})
复制代码

注意:在文件(而不是目录)上使用 fs.rmdir() 会致使在 Windows 上出现 ENOENT 错误、在 POSIX 上出现 ENOTDIR 错误。

三、fs.readdir读取目录

fs.readdir(path, [options], callback)
复制代码
  1. 第一个参数:path 目录路径
  2. 第二个参数[options]可选的 options 参数能够是指定编码的字符串,也能够是具备 encoding 属性的对象,该属性指定用于传给回调的文件名的字符编码。 若是 encoding 设置为 'buffer',则返回的文件名是 Buffer 对象。 若是 options.withFileTypes 设置为 true,则 files 数组将包含 fs.Dirent 对象。
  3. 第三个参数回调函数,回调函数有两个参数,第一个 err(错误),第二个返回 的data 为一个数组,包含该文件夹的全部文件,是目录中的文件名的数组(不包括 '.''..')。

demo:

const fs = require('fs');
fs.readdir('./file',function(err,data){
  if(err) return;
  //data为一个数组
  console.log('读取的数据为:'+data[0]);
});
复制代码

实战训练:

只讲文件相关 Api 显得很枯燥,下面说一些 fs 在 Node.js 中的具体应用

「示例:fs 模块如何实现文件拷贝」

文件拷贝例子包括小文件拷贝和大文件拷贝(以前讲的 fs 模块也能够实现文件拷贝)

小文件拷贝

小文件拷贝除了上面 fs 本身提供的 api 咱们本身也能够经过读写完成一个拷贝例子,以下:

// 文件拷贝 将data.txt文件中的内容拷贝到copyData.txt
// 读取文件
const fileName1 = path.resolve(__dirname, 'data.txt')
fs.readFile(fileName1, function (err, data) {
    if (err) {
        // 出错
        console.log(err.message)
        return
    }
    // 获得文件内容
    var dataStr = data.toString()

    // 写入文件
    const fileName2 = path.resolve(__dirname, 'copyData.txt')
    fs.writeFile(fileName2, dataStr, function (err) {
        if (err) {
            // 出错
            console.log(err.message)
            return
        }
        console.log('拷贝成功')
    })
})
复制代码

咱们使用 readFile 和 writeFile 实现了一个 copy 函数,那个 copy 函数是将被拷贝文件的数据一次性读取到内存,一次性写入到目标文件中,这种针对小文件还好。

大文件拷贝

若是是一个大文件几百M一次性读取写入不现实,因此须要屡次读取屡次写入,接下来使用文件操做的高级方法对大文件和文件大小未知的状况实现一个 copy 函数。固然除了这种方式还有我在以前的文章讲过的stream模块也能够实现,并且性能更好,可是这里就再也不重复说明,本篇主要讲fs模块。

demo:

// copy 方法
function copy(src, dest, size = 16 * 1024, callback) {
  // 打开源文件
  fs.open(src, 'r', (err, readFd) => {
    // 打开目标文件
    fs.open(dest, 'w', (err, writeFd) => {
      let buf = Buffer.alloc(size);
      let readed = 0; // 下次读取文件的位置
      let writed = 0; // 下次写入文件的位置

      (function next() {
        // 读取
        fs.read(readFd, buf, 0, size, readed, (err, bytesRead) => {
          readed += bytesRead;

          // 若是都不到内容关闭文件
          if (!bytesRead) fs.close(readFd, err => console.log('关闭源文件'));

          // 写入
          fs.write(writeFd, buf, 0, bytesRead, writed, (err, bytesWritten) => {
            // 若是没有内容了同步缓存,并关闭文件后执行回调
            if (!bytesWritten) {
              fs.fsync(writeFd, err => {
                fs.close(writeFd, err => return !err && callback());
              });
            }
            writed += bytesWritten;

            // 继续读取、写入
            next();
          });
        });
      })();
    });
  });
}
复制代码

在上面的 copy 方法中,咱们手动维护的下次读取位置和下次写入位置,若是参数 readed 和 writed 的位置传入 null,NodeJS 会自动帮咱们维护这两个值。

如今有一个文件 6.txt 内容为 “你好”,一个空文件 7.txt,咱们将 6.txt 的内容写入 7.txt 中。

const fs = require('fs');

// buffer 的长度
const BUFFER_SIZE = 3;

// 拷贝文件内容并写入
copy('6.txt', '7.txt', BUFFER_SIZE, () => {
  fs.readFile('7.txt', 'utf8', (err, data) => {
    // 拷贝完读取 7.txt 的内容
    console.log(data); // 你好
  });
});
复制代码

在 NodeJS 中进行文件操做,屡次读取和写入时,通常一次读取数据大小为 64k,写入数据大小为 16k。

你们好,我是koala,在作一个一个Node.js高级进阶路线,今天就分享这么多,若是对分享的内容感兴趣,能够关注公众号「程序员成长指北」,或者加入技术交流群,你们一块儿讨论。

加入咱们一块儿学习吧!

node学习交流群

交流群满100人不能自动进群, 请添加群助手微信号:【coder_qi】备注node,自动拉你入群。

相关文章
相关标签/搜索