NodeJS之fs文件系统模块的理解

2,851 阅读7分钟

fs文件系统模块是NodeJS核心模块之一,用于对系统文件及目录进行读写操作,官方文档讲解了fs模块主要提供三种API机制以供开发者使用。

  • 基于Promise的方式 fs/promises API 提供了返回 promise 的异步的文件系统方法。

注意:Promise API 使用底层的 Node.js 线程池在事件循环线程之外执行文件系统操作。 这些操作不是同步的也不是线程安全的。 对同一文件执行多个并发修改时必须小心,否则可能会损坏数据。

  • 基于callback回调方式 回调的 API 异步地执行所有操作,不会阻塞事件循环,然后在完成或错误时调用回调函数。

注意:回调的 API 使用底层 Node.js 线程池在事件循环线程之外执行文件系统操作。 这些操作不是同步的也不是线程安全的。 对同一文件执行多个并发修改时必须小心,否则可能会损坏数据。

  • 基于同步的方式 同步的 API 同步地执行所有操作,阻塞事件循环,直到操作完成或失败。

本篇文章以回调的API为例,分析总结fs模块常用文件操作API的用法及注意事项。

1. 权限相关

1. 1 fs.access(path[, mode], callback)

测试用户对 path 指定的文件目录的权限。

参数:

  • path: string | Buffer | url
  • mode: fs.constants.F_OK(存在性) | fs.constants.R_OK(可读性) | fs.constants.W_OK(可写性)
  • callback: 回调函数,传入值为可能错误的err
import fs from 'fs';
import path from 'path';

const rootDir = path.resolve();
const testPath = path.join(rootDir, 'files', 'abc.text');

fs.access(testPath, fs.constants.R_OK, (err) => {
  if (err) {
    console.log(err)
  } else {
    console.log('存在文件')
  }
})

注意:path不存在或者没有权限都将调用回调函数并传入err对象。

1. 2 fs.chmod(path, mode, callback)

异步地更改文件的权限。

参数:

1. 3 fs.fchmod(fd, mode, callback)

设置文件的权限

参数:

注意:此方法与fs.chmod()方法唯一不同点在于参数fd,通过文件描述符修改文件权限。其它参数一致。

1. 4 fs.chown(path, uid, gid, callback)

异步地更改文件的所有者和群组

参数:

  • path:string | Buffer | url
  • uid: integger
  • gid: integger
  • callback: 回调函数
1. 5 fs.fchown(fd, uid, gid, callback)

设置文件的所有者

参数:

  • fd:integger
  • uid: integger
  • gid: integger
  • callback: 回调函数

注意:次方法与fs.chown()方法的区别主要在第一个参数的区别。fd为文件描述符。

2. 新建相关

2.1 fs.mkdir(path[, options], callback)

异步地创建目录。

参数:

  • path: string | Buffer | url
  • options: integer | Object
  • callback: Function(err, path?)
fs.mkdir(path.join(path.resolve(), 'cssFiles'), {recursive: true}, (err,path) => {
  if (err) {
    return;
  }
  // path仅在options recursive: true时存在
  console.log(path)
})

3. 文件信息

3. 1 fs.stat(path[, options], callback)

获取文件状态信息

参数:

  • path: string | Buffer | url
  • options: Object
  • callback:回调函数
import fs from 'fs';
import path from 'path';

const rootDir = path.resolve();
const testPath = path.join(rootDir, 'files', 'text.txt');

fs.stat(testPath,(err,stats) => {
  if (err) {
    console.log(err);
    return;
  }
  console.log(stats);
  {
      dev: 4271079436,
      mode: 33206,
      nlink: 1,
      uid: 0,
      gid: 0,
      rdev: 0,
      blksize: 4096,
      ino: 281474980389124,
      size: 1360,
      blocks: 8,
      atimeMs: 1628750429976.6963,
      mtimeMs: 1628750410741.5435,
      ctimeMs: 1628750410741.5435,
      birthtimeMs: 1628750410738.552,
      atime: 2021-08-12T06:40:29.977Z,
      mtime: 2021-08-12T06:40:10.742Z,
      ctime: 2021-08-12T06:40:10.742Z,
      birthtime: 2021-08-12T06:40:10.739Z
    }
})
3. 2 fs.fstat(fd[, options], callback)

使用文件描述符获取文件描述信息

注意:次方法与fs.stat()方法区别在于第一个参数fd,文件描述符

4. 打开访问相关

4.1 fs.open(path[, flags[, mode]], callback)

异步地打开文件

import fs from 'fs';
import path from 'path';

const rootDir = path.resolve();
const testPath = path.join(rootDir, 'files', 'text.txt');

fs.open(testPath, (err, fd) => {
  if (err) {
    console.log(err);
    return;
  }
  console.log(fd); // 3
})
4.2 fs.opendir(path[, options], callback)

异步地打开目录

import fs from 'fs';
import path from 'path';

const rootDir = path.resolve();
const testDir = path.join(rootDir, 'files');
const testPath = path.join(rootDir, 'files', 'text.txt');

fs.opendir(testDir, async(err, files) => {
  if (err) {
    console.log(err);
    return;
  }
  for await (const f of files) {
    console.log(f)
  }
})
// 打印结果:
// Dirent { name: 'abc.css', [Symbol(type)]: 1 }
// Dirent { name: 'text.txt', [Symbol(type)]: 1 }
4.3 fs.close(fd[, callback])

关闭文件描述符。

5. 读取相关

5.1 fs.read(fd)

从 fd(文件描述符) 指定的文件中读取数据 此方法有两种使用方式:

  • fs.read(fd, buffer, offset, length, position, callback)
  • fs.read(fd, [options,] callback)
import fs from 'fs';
import path from 'path';
import {Buffer} from 'buffer';

const rootDir = path.resolve();
const testDir = path.join(rootDir, 'files');
const testPath = path.join(rootDir, 'files', 'text.txt');

fs.stat(testPath, (err1, stat) => {
  if (err1) {
    return;
  }
  fs.open(testPath, (err2, fd) => {
    if (err2) {
      return;
    }
    const len = stat.size;
    const offset = 0;
    const filePosition = 0;
    // 创建一个与文件大小相同的缓冲区
    const readBuffer = Buffer.alloc(len);
    // 读取文件全部内容
    fs.read(fd, readBuffer, offset, len, filePosition, (err3, bytesRead, buffer) => {
      if (err3) {
        return;
      }
      console.log(bytesRead)
      console.log(buffer.toString());
    })
  })
})

5.2 fs.readFile(path[, options], callback)

异步地读取文件的全部内容。

参数:

  • path: string | Buffer | url
  • options: Object | string
  • callback:Function(err, data)

参数说明:

  • 如果未指定编码,则返回原始缓冲区。
  • 如果 options 是字符串,则它指定编码:如 utf8, base64
import fs from 'fs';
import path from 'path';

const rootDir = path.resolve();
const testDir = path.join(rootDir, 'files');
const testPath = path.join(rootDir, 'files', 'text.txt');

fs.readFile(testPath, 'utf8', (err, data) => {
  if (err) {
    return;
  }
  console.log(data)
})

read()与readFile()对比说明

  • fs.readFile()方法是对fs.read()方法的进一步封装,以方便的读取文件的全部内容。

  • 相比fs.readFile()方法,使用fs.read()方法读取文件的全部内容就要复杂的多。首先要用fs.stat或fs.fstat判断文件的大小,然后使用fs.open()创建文件描述符,最后再使用fs.read()方法读取文件内容。

  • fs.read更适合读取部分文件数据内容。

5.3 fs.readdir(path[, options], callback)

异步读取目录的内容

参数:

  • path: string | Buffer | url
  • options: string | Object
  • callback: Function(err, files)
import fs from 'fs';
import path from 'path';
import {Buffer} from 'buffer';

const rootDir = path.resolve();
const testDir = path.join(rootDir, 'files');
const testPath = path.join(rootDir, 'files', 'text.txt');

fs.readdir(testDir, (err, files) => {
  if (err) {
    return;
  }
  console.log(files)
})
// 结果
[ 'abc.css', 'text.txt' ]
Dirent { name: 'abc.css', [Symbol(type)]: 1 }
Dirent { name: 'text.txt', [Symbol(type)]: 1 }

6. 写入相关

6.1 fs.write()

将数据写入指定fd

与fs.read()相似,同样有两种使用方式

  • fs.write(fd, buffer[, offset[, length[, position]]], callback)
  • fs.write(fd, string[, position[, encoding]], callback)
6.2 fs.writeFile(file, data[, options], callback)
fs.writeFile(path.join(path.resolve(), 'files', 'newText.txt'), 'hello fs.wirteFile2', (err)=> {
  if (err) {
    throw err;
    return;
  }
  console.log('数据写入成功')
})

注意说明

  • 如果file不存在则新建file
  • 如果file存在则覆盖file原有数据
  • writeFile是对write的封装
6.3 fs.appendFile(path, data[, options], callback)

异步地将数据追加到文件

fs.appendFile(path.join(path.resolve(), 'files', 'newText.txt'), '这是一段新追加文本内容', (err) => {
  if (err) {
    return;
  }
  console.log('数据追加成功')
})

注意说明

  • 如果file不存在则新建file
  • 相比fs.writeFile不同,file存在则追加内容,不进行覆盖。

7. 删除复制相关

7.1 fs.rm(path[, options], callback)

异步地删除文件和目录

// options recursive: true执行递归删除
fs.rm(path.join(path.resolve(), 'cssFiles'), {recursive: true}, (err) => {
  if (err) {
    return;
  }
  console.log('删除成功')
})
7.2 fs.rmdir(path[, options], callback)

异步的删除目录

// 仅能删除空文件夹
fs.rmdir(path.join(path.resolve(), 'cssFiles'), (err) => {
  if (err) {
    console.log(err)
    return;
  }
  console.log('删除成功')
})
// 递归删除-传入参数options recursive: true
fs.rmdir(path.join(path.resolve(), 'cssFiles'), {recursive: true}, (err) => {
  if (err) {
    console.log(err)
    return;
  }
  console.log('删除成功')
})
7.3 fs.unlink(path, callback)

异步地删除文件或符号链接。

fs.unlink(path.join(testDir, 'abc.css'), (err) => {
  if (err) {
    return;
  }
  console.log('文件删除成功')
})
7.4 fs.copyFile(src, dest[, mode], callback)

异步地将 src 复制到 dest

fs.copyFile(path.join(testDir, 'text.txt'), path.join(path.resolve(), 'static', 'copyText.txt'), (err) => {
  if (err) {
    console.log(err);
    return;
  }
  console.log('复制成功')
})

注意

  • 只能复制文件,不能复制目录
  • 目标目录不存在,不会创建,报错
  • 文件类型可以不一致。

8. 重命名相关

8.1 fs.rename(oldPath, newPath, callback)

将 oldPath 处的文件异步重命名为作为 newPath 提供的路径名。

fs.rename(path.join(testDir, 'newText.txt'), path.join(testDir, 'reText.txt'), (err) => {
  if (err) {
    console.log(err);
    return;
  }
  console.log('重命名成功')
})

注意

  • 不能重命名目录,只能重命名文件
  • 如果重命名文件已存在将被覆盖

10. 监听文件变化

10.1 fs.watch(filename[, options][, listener])

监视 filename 的变化,其中 filename 是文件或目录。

10.2 fs.watchFile(filename[, options], listener)

监视filename文件的变化,只能是文件

11. 文件流相关

文件流,顾名思义就是以流(stream)的形式操作文件。文件流的方式适合于对大文件的读写操作。

  • fs继承于stream

举个例子理解:

  • 想象一下,如果把文件读取比从一个水池向一个池子里抽水,同步会阻塞程序,异步会等待结果(等待池子里面的水抽完),如果这个池子特别大怎么办?有三峡水库那么大怎么办?你要等到多久才能喝到抽的水?
  • 因此便会有了文件流,文件流就好比你一边抽一边取,不用等池子满了再用一样方便。
11.1 fs.createReadStream(path[, options])

以流的形式读取文件

11.2 fs.createWriteStream(path[, options])

以流的形式写入文件

// 流的形式读取和写入,下面以复制文件的形式距离说明

const fileSource = path.join(path.resolve(), 'files/text.txt');
const pipeDest = path.join(path.resolve(), 'files/pipeCopy.txt');
// 创建可读流
const readStream = fs.createReadStream(fileSource);
// 创建可写流
const writePipeStream = fs.createWriteStream(pipeDest);
// pipe管道消费数据流
readStream.pipe(writePipeStream);