Skip to content

fs模块

fs模块是File System的缩写,模拟了Linux环境,提供了Nodejs对文件系统交互的各种功能API。通过fs模块,即可执行增、删、改、查等操作。

  • 大多数操作文件API具有同步和异步的形式
    • 要使用基于同步和回调的方式require('node:fs')
    • 要使用基于Promise的方式require('node:fs/promises')
  • 文件夹是一种特殊的文件

读取文件

常用的读取文件API

  • read:读取文件流内容,搭配文件描述符使用

    js
    const fs = require("node:fs");
    const { resolve } = require("node:path");
    
    const filePath = resolve(__dirname, "./data.json");
    
    // 1.创建文件描述符
    fs.open(filePath, (err, fd) => {
      if (err) {
        console.error(err);
        return;
      }
    
      // 2.通过Buffer文件流读取文件
      fs.read(fd, Buffer.alloc(1024), 0, 1024, 0, (err, bytesRead, buffer) => {
        if (err) {
          console.error(err);
          return;
        }
    
        console.log(buffer.slice(0, bytesRead).toString());
        // 3.用完后记得关闭文件
        fs.close(fd)
      });
    });

    TIP

    文件描述符

    POSIX系统中会把一切看做文件,当进程打开现有文件或者创建文件时,内核会向进程返回一个文件描述符File Descriptor,文件描述符就是内核为了高效管理已被打开的文件所创建的索引,用来指向被打开的文件,所有执行I/O操作的系统调用都会通过文件描述符。

    文件描述符由fs.open()创建的对象FileHandle提供。

    open操作和close操作通常是搭配一起使用,有打开文件,用完就要关闭文件,不然容易造成闭包内存泄漏。

    js
    const fs = require('node:fs')
    
    fs.open('./a.txt',(err,fd)=>{
      if(err)return 
      // fd参数就是该文件的文件描述符
      // Nodejs提供了一系列的根据文件描述符去操作文件的API
      // fs.fchmod
      // fs.fstat
      // fs.fsync
      // ...
    
      fs.close(fd)
    })

    TIP

    Nodejs模块支持支持以标准POSIX函数为模型的方式与文件系统交互,也就是导入语句前的node声明。

  • readFile:回调读取文件

    js
    const fs = require("node:fs");
    const { resolve } = require("node:path");
    
    const filePath = resolve(__dirname, "./data.json");
    
    fs.readFile(filePath, { encoding: "utf8" }, (err, data) => {
      if (err) {
        console.error(err);
        return;
      }
    
      console.log(data);
    });

    TIP

    常见的文件编码encoding选项

    读取文件时,默认读到的是一些Buffer二进制数据,需要传递文件编码选项告诉它转成什么样的数据,在读取到Buffer二进制数据时,都可以这么操作。常见的有

    • gbk
    • utf-8
    • gb18030
    • gb2312

    TIP

    需要注意的是,代码在执行时,Nodejs会动态拼接命令执行时所在的目录,容易出现路径拼接错误, 在使用fs模块的API时,最好手动拼接好绝对路径,就像上面提供的🌰 resolve 所做的一样。

  • readFileSync:同步读取文件

    js
    const fs = require("node:fs");
    const { resolve } = require("node:path");
    
    const filePath = resolve(__dirname, "./data.json");
    
    // 读文件的文件系统标志默认为 "r"
    const fileContent = fs.readFileSync(filePath, { encoding: "utf8", flag: "r" });
    
    console.log(fileContent);

    TIP

    文件系统Flag

    以下标志在 flag 选项接受字符串的任何地方可用。

    • 'a':打开文件进行追加。如果文件不存在,则创建该文件。
    • 'ax':类似于 'a' 但如果路径存在则失败。
    • 'a+':打开文件进行读取和追加。如果文件不存在,则创建该文件。
    • 'ax+':类似于 'a+' 但如果路径存在则失败。
    • 'as':以同步模式打开文件进行追加。如果文件不存在,则创建该文件。
    • 'as+':以同步模式打开文件进行读取和追加。如果文件不存在,则创建该文件。
    • 'r':打开文件进行读取。如果文件不存在,则会发生异常。
    • 'rs':打开文件以同步模式读取。如果文件不存在,则会发生异常。
    • 'r+':打开文件进行读写。如果文件不存在,则会发生异常。
    • 'rs+':以同步模式打开文件进行读写。指示操作系统绕过本地文件系统缓存。这主要用于在 NFS 挂载上打开文件,因为它允许跳过可能过时的本地缓存。它对 I/O 性能有非常实际的影响,因此除非需要,否则不建议使用此标志。这不会将 fs.open() 或 fsPromises.open() 变成同步阻塞调用。如果需要同步操作,应该使用类似 fs.openSync() 的东西。
    • 'w':打开文件进行写入。创建(如果它不存在)或截断(如果它存在)该文件。
    • 'wx':类似于 'w' 但如果路径存在则失败。
    • 'w+':打开文件进行读写。创建(如果它不存在)或截断(如果它存在)该文件。
    • 'wx+':类似于 'w+' 但如果路径存在则失败。
  • open(打开文件描述符)

    js
    const fs = require("fs");
    const { resolve } = require("path");
    
    const filePath = resolve(__dirname, "./data.json");
    
    fs.open(filePath, (err, fd) => {
      if (err) {
        console.error(err);
        return;
      }
    
      // fd是文件描述符文件参数
      console.log(fd);
      // 用完需要关闭,不然容易造成内存泄漏
      fs.close(fd)
    });
  • stat(读取文件或文件目录信息,可以用stat方法,或者用文件描述符的方式去读取fstat)

    js
    const fs = require("fs");
    const { resolve } = require("path");
    
    const filePath = resolve(__dirname, "./data.json");
    // stat
    fs.stat(filePath, (err, stats) => {
      if (err) {
        console.error(err);
        return;
      }
    
      console.log(stats);
    });
    
    // fstat
    fs.open(filePath, (err, fd) => {
      if (err) {
        console.error(err);
        return;
      }
    
      fs.fstat(fd, (err, stats) => {
        if (err) {
          console.error(err);
          return;
        }
    
        console.log(stats);
        fs.close(fd)
      });
    });

写入文件

常用的写文件API

写入文件会大量使用到文件系统Flag

准备工作,定义几个生成文件名、文件路径、文件内容的方法,提供几个方法调用:

js
const fs = require("node:fs");
const { resolve } = require("node:path");

// 从26个字母随机截取任一字符作为文件名
const generateRandomFilename = () => String.fromCharCode(65 + Math.floor(Math.random() * 26)).toLocaleLowerCase();

// 生成1-100长度内的随机字符串
const generateContent = () =>
  Array.from({ length: Math.floor(Math.random() * 100) }, () =>
    String.fromCharCode(65 + Math.floor(Math.random() * 26))
  ).join("");

// 生成文件路径,每次调用均生成不同的文件名
const generateFilePath = () => resolve(__dirname, `./${generateRandomFilename()}.txt`);
  • write:文件流写入,传入文件描述符

    js
    fs.open(generateFilePath(), "w", (err, fd) => {
      if (err) return;
    
      fs.write(fd, generateContent(), (err) => {
        if (err) return;
    
        fs.close(fd);
      });
    });
  • writeFile:写入字符串或文件流

    js
    fs.writeFile(generateFilePath(), generateContent(), { flag: "w" }, (err) => {
      if (err) {
        console.log(err);
      }
    });
  • writeSync:同步写入

    js
    try {
      fs.writeFileSync(generateFilePath(), generateContent(), { flag: "w" });
    } catch (err) {
      console.log(err);
    }

复制、移动、重命名、删除

基础代码:

js
const fs = require("node:fs");
const { resolve, extname, basename } = require("node:path");

const filepath = resolve(__dirname, `../data/data.json`);
// 文件后缀
const fileExt = extname(filepath);
// 文件名
const fileName = basename(filepath, fileExt);
  • copyFile

    js
    fs.copyFile(filepath, resolve(__dirname, `../data/${fileName}-copy${fileExt}`), (err) => {
      if (err) {
        console.error(err);
        return;
      }
      console.log("复制文件成功");
    });
  • cp

    js
    fs.cp(filepath, resolve(__dirname, `../data-move/${fileName}${fileExt}`), (err) => {
      if (err) {
        console.error(err);
        return;
      }
    
      console.log("移动文件成功");
    });
  • rename

    js
    fs.rename(filepath, resolve(__dirname, `../data/${fileName}-rename${fileExt}`), (err) => {
      if (err) {
        console.error(err);
        return;
      }
    
      console.log("重命名文件成功");
    });
  • mkdir

    js
    fs.mkdir(resolve(__dirname, `../data-dir`), (err) => {
      if (err) {
        console.error(err);
        return;
      }
    
      console.log("创建文件夹成功");
    });
  • rm

    js
    fs.rm(filepath, { recursive: true }, (err) => {
      if (err) {
        console.error(err);
        return;
      }
    
      console.log("删除文件成功");
    });

目录操作

通过手写删除嵌套文件夹,掌握操作目录的常用API,学习异步串行、异步并行的解决方法。

同步删除:

js
const fs = require("fs");
const path = require("path");

// 同步删除文件夹
function rmdirSync(dir) {
  const statObj = fs.statSync(dir);

  if (statObj.isFile()) {
    // 如果当前为文件,直接删除
    fs.unlinkSync(dir);
  } else {
    // 否则是目录,需要递归删除
    let dirs = fs.readdirSync(dir);
    dirs = dirs.map((file) => path.join(dir, file));

    dirs.forEach((dir) => rmdirSync(dir));

    // 最后需要把最外层的给删除掉
    fs.rmdirSync(dir);
  }
}

rmdirSync(path.resolve(__dirname, "../a"));

异步删除:

js
const fs = require("fs");
const path = require("path");

// 异步删除文件夹
function rmdir(dir, cb) {
  fs.stat(dir, (err, statObj) => {
    if (statObj.isFile()) {
      fs.unlink(dir, cb);
    } else {
      fs.readdir(dir, (err, dirs) => {
        // 异步串行删除
        dirs = dirs.map((file) => path.join(dir, file));

        let index = 0;

        function next() {
          if (dirs.length === index) {
            // 最后一个,说明此时并没有子节点,需要删除自己
            return fs.rmdir(dir, cb);
          }
          rmdir(dirs[index++], next);
        }

        next();

        // 异步并行删除 
        if (dirs.length === 0) { 
          return fs.rmdir(dir, cb); 
        } 

        dirs = dirs.map((file) => path.join(dir, file)); 
 
        let index = 0; 

        function done() { 
          if (++index === dirs.length) { 
            fs.rmdir(dir, cb); 
          } 
        } 

        dirs.forEach((file) => rmdir(file, done)); 
      });
    }
  }); 
}

rmdir(path.resolve(__dirname, "../a"), () => {
  console.log("删完了");
});

文件夹实际上就是一颗树的结构:

aa

删除文件夹要先删除文件夹下的内容,才能把整个文件夹删除,对于树来说,也就是需要将子节点删除,然后再移除掉整个节点。这种方式实际上就是树的广度遍历,先遍历每层的子节点并移除,最后移除父节点。

树的遍历

树的遍历分为广度优先遍历和深度优先遍历,其中深度优先遍历又能根据二叉树的情况分为前序、中序、后序遍历。

广度优先遍历:从根节点开始,遍历下一层中所有的子节点,然后再遍历下一层的所有子节点,以此类推,直到遍历所有的节点。

深度优先遍历:从根节点开始,尽可能深地搜索树的分支。递归地深度优先遍历每个子节点,重复 递归步骤,直至所有节点都被访问。

实战:递归目录扫描

递归扫描目录的内容,并统计不同类型文件的数量和大小:

js
const fs = require("node:fs");
const path = require("node:path");

const filepath = path.resolve(__dirname, "./a");

// 文件类型统计
const stats = {
  totalFiles: 0,
  totalDirs: 0,
  byExtension: {},
};

const scanDirectory = (dirpath) => {
  const dirs = fs.readdirSync(dirpath);
  stats.totalDirs++;

  // 遍历每个目录,判断是否为目录,目录继续递归,否则计数++
  for (const item of dirs) {
    const itemPath = path.resolve(dirpath, item);

    try {
      const stat = fs.statSync(itemPath);

      if (stat.isDirectory()) {
        scanDirectory(itemPath);
      } else if (stat.isFile()) {
        stats.totalFiles++;
        stats.totalSize += stat.size;

        const ext = path.extname(item).toLowerCase() || "(无拓展名)";
        if (!stats.byExtension[ext]) {
          stats.byExtension[ext] = {
            count: 0,
            size: 0,
          };
        }
        stats.byExtension[ext].count++;
        stats.byExtension[ext].size += stat.size;
      }
    } catch (error) {
      console.log(error);
    }
  }
};

scanDirectory(filepath);

console.log(stats);
/*
{
  totalFiles: 3,
  totalDirs: 3,
  byExtension: { '(无拓展名)': { count: 3, size: 0 } }
}
*/

如有转载或 CV 的请标注本站原文地址