一次性文件操作

  • readFilewriteFileappendFilecopyFile 都是一次性的操作,例如 copyFile 会将文件内容一次性获取并放到内存中,然后再一次性写入另一个文件,这些都 不适用于大内存的文件操作

  • 文件操作 API 通常最后一个参数是一个回调函数,Nodejs 中的回调函数通常都是 error-first 错误优先风格的 (err, …args) => {},即第一个参数是错误信息,后面还是要处理的数据,当没有错误的时候 errnull

常用 API

主要的文件操作就是文件读写、拷贝、监控:Nodejs 中几乎所有文件 API 操作都有 「同步」「异步」 两种方式,同步 API 名称比异步 API 名称多个 Sync,如 readFile 对应的同步 APIreadFileSync

API 描述
fs.readFile 从指定文件中读取数据
fs.writeFile 向指定文件中写入数据
fs.appendFile 向指定文件中追加数据
fs.copyFile 将某个文件中的数据拷贝到另一个文件
fs.watchFile 监听指定文件,当文件内容发生修改,触发回调函数(没有对应的同步 API

基本使用

  1. readFile

    • path: fs.PathOrFileDescriptor
    • options: ({ encoding: BufferEncoding; flag?: string; }) | BufferEncoding
    • callback: (err: NodeJS.ErrnoException, data: string)
    const fs = require('fs')
    const path = require('path')
    
    // 文件操作通常建议使用绝对路径
    // 默认读取的数据是 buffer,通过指定字符编码转化读取的数据
    fs.readFile(path.resolve('data.txt'), 'utf-8', (err, data) => {
      if (err === null) {
        console.log(data) // data 中的内容
      }
    })
    
    // 如果文件不存在,则会报错
    fs.readFile(path.resolve('data1.txt'), 'utf-8', (err, data) => {
      console.log(err) // 报错
    })
    
  2. writeFile

    • path: fs.PathOrFileDescriptor,
    • data: string | NodeJS.ArrayBufferView,
    • options?: ({ encoding: BufferEncoding; flag?: string; }) | BufferEncoding,
    • callback: (err: NodeJS.ErrnoException, data: string)
    const fs = require('fs')
    
    // 1.所谓写入,就是用新的内容替换原有的内容
    fs.writeFile('data.txt', 'Hi', err => {
      if (!err) {
        fs.readFile('data.txt', 'utf-8', (err, data) => {
          console.log(data) // Hi
        })
      }
    })
    
    // 2.如果写入的文件不存在,会创建该文件
    fs.writeFile('data1.txt', 'Hi', err => {
      if (!err) {
        fs.readFile('data1.txt', 'utf-8', (err, data) => {
          console.log(data) // Hi
        })
      }
    })
    
    // 3.
    fs.writeFile(
      'data.txt',
      'Hello!',
      {
        mode: 438, // 默认值 `0o666`(八进制表示) 的十进制表示(可读可写不可执行)
        flag: 'r+',
        encoding: 'utf8' // 与 `utf-8` 等效
      },
      err => {
        if (!err) {
          fs.readFile('data.txt', 'utf-8', (err, data) => {
            console.log(data) // Hello!世界
          })
        }
      }
    )
    
  3. appendFile

    • path: fs.PathOrFileDescriptor
    • data: string | Uint8Array
    • options?: ({ encoding: BufferEncoding; flag?: string; }) | BufferEncoding
    • callback: (err: NodeJS.ErrnoException, data: string)
    const fs = require('fs')
    
    fs.appendFile('data.txt', 'Hello', err => {
      // 回调函数仅有 err
      fs.readFile('data.txt', 'utf8', (err, data) => {
        console.log(data) // 你好世界Hello
      })
    })
    
    // 同样可以接收用于配置的第三个参数
    fs.appendFile(
      'data.txt',
      'Hello',
      {
        flag: 'w' // 现在这个 appendFile 等效于默认的 writeFile
      },
      err => {
        fs.readFile('data.txt', 'utf8', (err, data) => {
          console.log(data) // Hello
        })
      }
    )
    
  4. copyFile

    • src: fs.PathLike
    • dest: fs.PathLike
    • callback: fs.NoParamCallback
    const fs = require('fs')
    
    // 第二个参数是目标文件的路径
    // 如果目标文件不存在,则会创建文件
    fs.copyFile('data.txt', 'data2.txt', (err) => {
      // 回调函数仅接收 err
      if (err === null) console.log('拷贝成功')
    })
    
  5. watchFile

    • filename: fs.PathLike,
    • options: fs.WatchFileOptions & { bigint?: false;},
    • listener: (curr: fs.Stats, prev: fs.Stats) => void
    const fs = require('fs')
    
    // watchFile 通过定时轮询文件,检查文件是否发生变化
    // interval 表示轮询文件的时间间隔 默认 `5007`
    // 回调函数接收 current 和 previous 分别包含文件变化前后的相关信息
    fs.watchFile('data.txt', { interval: 200 }, (current, previous) => {
      if (current.mtime !== previous.mtime) {
        // mtime 表示最新修改时间
        console.log('文件内容被修改')
      }
    })
    // 调用 API 修改文件
    // 也可以手动打开文件去修改内容
    fs.writeFile('data.txt', 'Hello', err => {
      console.log('写入内容')
      setTimeout(() => {
        fs.writeFile('data.txt', 'Hello', err => {
          console.log('写入内容相同')
        })
      }, 1000)
    })
    // watchFile 监听任务会一直持续,控制台不会退出
    // 需要手动停止监听,当删除了所有监听器,程序就会停止运行
    // 第二个参数可以指定要删除的监听器(watchFile 的回调函数),如果不指定则删除指定文件的全部监听器
    setTimeout(() => {
      fs.unwatchFile('data.txt')
    }, 3000)
    

案例:md 转 html

  • marked 依赖:将 markdown 内容转化成 html 的工具;

  • browser-sync 依赖:开启一个 Web 站点打开 html 页面,并实时更新;

  1. mdToHtml.js

    const fs = require('fs')
    const path = require('path')
    const marked = require('marked') // 将 md-->html
    const browserSync = require('browser-sync') // 使用 browser-sync 来实时显示 Html 内容
    
    const temp = `
        <!DOCTYPE html>
        <html lang="en">
            <head>
                <meta charset="UTF-8">
                <title></title>
                <style>
                    .markdown-body {
                        box-sizing: border-box;
                        min-width: 200px;
                        max-width: 1000px;
                        margin: 0 auto;
                        padding: 45px;
                    }
                    @media (max-width: 750px) {
                        .markdown-body {
                            padding: 15px;
                        }
                    }
                    {{style}}
                </style>
            </head>
            <body>
                <div class="markdown-body">
                    {{content}}
                </div>
            </body>
        </html>
    `
    
    // 通过命令行 md 文档名称获取 md 绝对路径
    let mdPath = path.join(__dirname, process.argv[2])
    // 获取 css 绝对路径
    let cssPath = path.resolve('github.css')
    //生成 html 所在路径
    let htmlPath = mdPath.replace(path.extname(mdPath), '.html')
    
    fs.watchFile(mdPath, (curr, prev) => {
        // 监听 md 文档内容的变经,然后更新 html 内容
        if (curr.mtime !== prev.mtime) {
            fs.readFile(mdPath, 'utf-8', (err, data) => {
                let htmlStr = marked(data)
                fs.readFile(cssPath, 'utf-8', (err, data) => {
                    let retHtml = temp.replace('{{content}}', htmlStr).replace('{{style}}', data)
                    // 将上述的内容写入到指定的 html 文件中,用于在浏览器里进行展示
                    fs.writeFile(htmlPath, retHtml, (err) => console.log('html 生成成功了'))
                })
            })
        }
    })
    
    // 开启服务 显示 html 内容
    browserSync.init({
        server: {
            baseDir: __dirname, // 服务的根目录
            index: path.basename(htmlPath) // 指定首页的文件名
        },
        watch: true // 监听更新
    })
    
  2. index.md

    ### 标题一
      * 列表项2
      * 列表项3
      * 列表项4
      * 列表项5
    
    ### 标题二
    
  3. 执行命令 node mdToHtml.js index.md

大文件读写操作

前面的 API 是将文件中的数据 一次性读取/写入 到内存中,这种方式对于大体积的文件来说,显然不合理:所以需要一个可以 「边读边写」「边写边读」 的操作方式,这就需要将文件的打开、读取、写入、关闭看作各自独立的环节;

常用 API

API 描述
fs.open 打开文件
fs.close 关闭文件
fs.read 将磁盘文件中的数据写入到 buffer
fs.write buffer 里的数据写入到磁盘文件中

基本使用

  1. open

    • path: fs.PathLike
    • flags: fs.OpenMode
    • mode: fs.Mode
    • callback: (err: NodeJS.ErrnoException, fd: number) => void
    const fs = require('fs')
    const path = require('path')
    
    fs.open(path.resolve('data.txt'), 'r', (err, fd) => {
      console.log(fd)
    })
    
  2. close

    • fd: number
    • callback?: fs.NoParamCallback
    const fs = require('fs')
    const path = require('path')
    
    fs.open(path.resolve('index.md'), 'r', (err, fd) => {
      // fd:文件描述符,用于追踪文件资源
      console.log(fd) // 第一次打开的文件的文件描述符是 3
      fs.close(fd, err => console.log('关闭成功'))
    })
    
  3. read

    • fd: number,
    • options: { buffer: TBuffer, offset: number, length: number, position: fs.ReadPosition }
    • callback: (err: NodeJS.ErrnoException, bytesRead: number, buffer: Buffer)
    const fs = require('fs')
    // 定义一个 Buffer 用于存储文件读取的数据
    const buf = Buffer.alloc(10)
    
    // A.txt 内容:1234567890
    fs.open('A.txt', 'r', (err, readFd) => {
      // 读取操作只会读取一次,并不会持续读取到读完所有数据
      fs.read(
        readFd, // 用于指定读取的文件
        {
          buffer: buf, // 数据写入缓冲区 buf
          offset: 0, // 获取 buf 数据的偏移量
          length: 3, // 读取 buf 数据的字节数
          position: 0 // 一般不需要指定(为 null/-1 自动更新当前文件位置)
        },
        (err, bytesRead, buffer) => {
          // bytesRead 实际读取的字节数
          // buffer 最终读取的数据
          console.log(bytesRead) // 3
          console.log(buffer) // <Buffer 31 32 33 00 00 00 00 00 00 00>
          console.log(buffer.toString()) // 123
        }
      )
    })
    
  4. write

    • fd: number,
    • buffer: Buffer,
    • offset: number,
    • length: number,
    • position: number, // 写入数据的起始位置
    • callback: (err: NodeJS.ErrnoException, written: number, buffer: Buffer) => void
    const fs = require('fs')
    // 定义一个已有数据的 Buffer,作为写入文件的数据
    const buf = Buffer.from('1234567890')
    
    fs.open('B.txt', 'w', (err, writeFd) => {
      fs.write(writeFd, buf, 1, 3, 0, (err, bytesWritten, buffer) => {
        // bytesWritten 实际写入的字节数
        // buffer 指向写入的数据源
        console.log(bytesWritten) // 3
        console.log(buffer === buf) // true
      })
    })
    

缺点

  1. 相对于 readFilewriteFile 一次性读写,这种方式会减轻内存的消耗,提高代码执行性能;

  2. 读写操作存在 多次 I/O 操作,会导致 CPU内存资源 的浪费;

  3. 建议使用 stream 流的方式进行大文件的读写操作

案例:文件拷贝

// 将 A 文件内容拷贝到 B 文件
// A.txt 内容:1234567890abcdefghigklmn

// 01 打开 A 文件,利用 read 将数据保存到 buffer 暂存起来
// 02 打开 B 文件,利用 write 将 buffer 中的数据写入到 B 文件中
// 数据完全拷贝
const fs = require('fs')
const buf = Buffer.alloc(10)

const BUFFER_SIZE = buf.length // 每次读取数据的字节数
fs.open('A.txt', 'r', (err, readFd) => {
  fs.open('B.txt', 'w', (err, writeFd) => {
    function next() {
      // position 指定为 null 自动更新读取文件的起始位置
      fs.read(readFd, buf, 0, BUFFER_SIZE, null, (err, bytesRead, buffer) => {
        if (bytesRead === 0) {
          // 内容读取完毕,关闭文件
          fs.close(readFd, () => { })
          fs.close(writeFd, () => { })
          console.log('拷贝完成')
          return
        }

        // 不指定 position  自动更新写入文件的起始位置
        fs.write(writeFd, buf, 0, bytesRead, (err, bytesWritten) => {
          // 再次读取数据
          next()
        })
      })
    }
    // 首次启动读取
    next()
  })
})

目录操作

常用 API

API 描述
fs.access 判断用户是否具有当前文件或目录的操作权限
fs.stat 获取目录及文件信息
fs.mkdir 创建目录
fs.rmdir 删除目录
fs.readdir 读取目录中的内容
fs.unlink 删除文件
fs.rm 删除文件和目录

基本使用

  1. access

    • path: fs.PathLike,
    • mode?: number,
    • callback: (err: NodeJS.ErrnoException) => void)
    const fs = require('fs')
    
    // 常用于判断目录或文件是否存在
    // windows 环境下一般对文件都具有可读可写不可执行的权限
    fs.access('data.txt', err => {
      // 仅接收 err
      if (err) {
        console.log(err)
      } else {
        console.log('有操作权限')
      }
    })
    
  2. stat

    • path: fs.PathLike,
    • callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void)
    const fs = require('fs')
    
    fs.stat('data.txt', (err, stats) => {
      // 回调返回一个 `fs.stats` 类,该对象提供有关文件的信息
      console.log(stats.size) // 内容字节数
      console.log(stats.isFile()) // 是否文件
      console.log(stats.isDirectory()) // 是否目录
    })
    
  3. mkdir

    • path: fs.PathLike,
    • options: fs.MakeDirectoryOptions & { recursive: true },
    • callback: (err: NodeJS.ErrnoException, path?: string) => void
    const fs = require('fs')
    
    // 默认情况创建的是路径最后部分,前提是保证父级目录全部存在
    // 假设下例 a/b 不存在
    fs.mkdir('a/b/c', err => {
      if (err) {
        console.log(err) // 进入这里
      } else {
        console.log('c 创建成功')
      }
    })
    
    // recursive 表示递归,默认为 false,开启后递归目录创建
    fs.mkdir('a/b/c', { recursive: true }, err => {
      if (err) {
        console.log(err)
      } else {
        console.log('a b c 创建成功') // 进入这里
      }
    })
    
  4. rmdir

    • path: fs.PathLike,
    • callback: fs.NoParamCallback
    const fs = require('fs')
    
    // 默认情况下删除的是路径的最后部分
    // 如果删除的不是目录类型或者路径不存在,则会报错,windows 环境下报 `ENOENT` 错误
    fs.rmdir('a/b/c', err => {
      if (err) {
        console.log(err)
      } else {
        console.log('c 删除成功')
      }
    })
    
    // 默认情况下删除非空目录(目录下存在其它目录或文件)则会报错
    fs.rmdir('a', err => {
      if (err) {
        console.log(err) // 报 `ENOTEMPTY` 错误
      } else {
        console.log('a 删除成功')
      }
    })
    
    // 同 mkdir 一样,rmdir 也提供一个 recursive 选项用于递归删除
    // 不过官方 v16.0.0 已弃用这个选项,而推荐使用 fs.rm()
    fs.rmdir('a', { recursive: true }, err => {
      if (err) {
        console.log(err)
      } else {
        console.log('a 删除成功')
      }
    })
    
  5. readdir

    • path: fs.PathLike,
    • callback: (err: NodeJS.ErrnoException, files: string[]) => void)
    const fs = require('fs')
    /*
      示例目录:
      └─ a
          ├─ b
          │   └─ b.txt
          └─ a.txt
    */
    // 仅读取当前目录下一层文件列表,不会递归读取
    fs.readdir('a', (err, files) => console.log(files) /* [ 'a.txt', 'b' ] */)
    fs.readdir('a/b', (err, files) => console.log(files) /* [ 'b.txt' ] */)
    
  6. unlink

    • path: fs.PathLike,
    • callback: fs.NoParamCallback
    const fs = require('fs')
    
    // 删除的是 path 的最后部分,如果文件不存在则报错
    fs.unlink('a/a.txt', err => {
      if (err) {
        console.log(err)
      } else {
        console.log('文件删除成功')
      }
    })
    
    // 如果删除的文件是目录类型,则报错
    fs.unlink('a', err => {
      if (err) {
        console.log(err) // 报错不允许操作
      } else {
        console.log('不会进入到这里')
      }
    })
    
  7. rm

    • path: fs.PathLike,
    • options: fs.RmOptions,
    • callback: fs.NoParamCallback
    const fs = require('fs')
    
    // force: true,如果 path 不存在,异常是否被忽略,默认 false
    // recursive: true,是否递归删除,默认 false
    fs.rm('a', { force: true, recursive: true }, err => {
      if (err) {
        console.log(err)
      } else {
        console.log('a 删除成功')
      }
    })
    

手写创建目录、删除目录

同步递归创建目录

const fs = require('fs')
const path = require('path')

function makeDirSync(dirPath) {
  const items = dirPath.split(path.sep) // 获取当前平台的路径分隔符 `/` 或 `\`

  // 对上述的数组进行遍历,需要拿到每一项,然后与前一项进行拼接 /
  for (let index = 1; index <= items.length; index++) {
    // ['a'] => a
    // ['a', 'b'] => a/b
    // ['a', 'b', 'c'] => a/b/c
    const dir = items.slice(0, index).join(path.sep)
    try {
      // 判断是否具有操作权限(即文件是否存在)
      fs.accessSync(dir)
    } catch (err) {
      // 不存在则创建
      fs.mkdirSync(dir)
    }
  }
}
makeDirSync(path.join('a/b/c'))

异步递归创建目录

  1. 回调方式

    const fs = require('fs')
    const path = require('path')
    
    function makeDirAsync(dirPath, cb) {
      const items = dirPath.split(path.sep) // ['a', 'b', 'c']
      let index = 1
    
      function next() {
        if (index > items.length) return cb && cb()
        // ['a'] => a/
        // ['a', 'b'] => a/b/
        // ['a', 'b', 'c'] => a/b/c/
        const dir = items.slice(0, index++).join(path.sep)
    
        fs.access(dir, err => {
          if (err) {
            // 判断是否具有操作权限(即文件是否存在)
            fs.mkdir(dir, next)
          } else {
            // 不存在则创建
            next()
          }
        })
      }
    
      next()
    }
    makeDirAsync(path.join('a/b/c'), () => console.log('创建完成'))
    
  2. Promise 方式

    const fs = require('fs')
    const path = require('path')
    const { promisify } = require('util')
    
    // 将 access 和 mkdir 转化成 promise 风格
    const access = promisify(fs.access)
    const mkdir = promisify(fs.mkdir)
    
    async function makeDirAsync(dirPath, cb) {
      const items = dirPath.split(path.sep) // ['a', 'b', 'c']
    
      for (let index = 1; index <= items.length; index++) {
        // ['a'] => a/
        // ['a', 'b'] => a/b/
        // ['a', 'b', 'c'] => a/b/c/
        const dir = items.slice(0, index).join(path.sep)
    
        try {
          await access(dir)
        } catch (err) {
          await mkdir(dir)
        }
      }
      cb && cb()
    }
    
    makeDirAsync(path.join('a/b/c'), () => console.log('创建成功'))
    

同步删除目录

const fs = require('fs');
const path = require('path');

// 同步删除目录(树的先序遍历)
function rmdirSync(dir) {
  // 判断 dir 是不是一个目录
  let statObj = fs.statSync(dir);
  if (statObj.isDirectory()) {
    let dirs = fs.readdirSync(dir); // 递归只考虑两层情况就可以了
    dirs.forEach(d => rmdirSync(path.join(dir, d)));
    fs.rmdirSync(dir);
  } else {
    fs.unlinkSync(dir);
  }
}
rmdirSync('b/c');

异步递归删除目录

  1. 串行删除

    const fs = require('fs')
    const path = require('path')
    
    function removeDir(dirPath, cb) {
      // 判断路径的类型
      fs.stat(dirPath, (err, stats) => {
        if (err) return
    
        if (stats.isDirectory()) {
          // 目录 --> 继续读取文件夹下的内容
          fs.readdir(dirPath, (err, files) => {
            // files 为文件夹下的内容,内容可能是 文件夹/文件
            const dirs = files.map(file => path.join(dirPath, file))
    
            // 记录当前目录下删除的文件数
            let index = 0
            // 定义递归删除的方法
            function next() {
              // 内容全部删除,删除最外层目录
              if (index === dirs.length) return fs.rmdir(dirPath, cb)
              // 当前要删除的文件
              let current = dirs[index++]
              removeDir(current, next)
            }
            next()
          })
        } else {
          // 文件 --> 直接删除
          fs.unlink(dirPath, cb)
        }
      })
    }
    
    // 删除 a 文件夹
    removeDir('a', () => console.log('删除完成'))
    
  2. 并发删除

    const fs = require('fs');
    const path = require('path');
    
    function removeDir(dir, cb) {
      fs.stat(dir, (err, statObj) => { // 判断 a 是不是文件夹
    
        if (statObj.isDirectory()) { // 是文件夹
          fs.readdir(dir, (err, dirs) => {
            // 获取当前目录的所有的目录的集合 
            dirs = dirs.map(item => path.join(dir, item));
            // 没有子目录,直接删除当前文件夹
            if (dirs.length == 0) return fs.removeDir(dir, cb);  
    
            let index = 0;
            function done() { // Promise.all
              if (++index == dirs.length) {
                fs.removeDir(dir, cb);
              }
            }
    
            for (let i = 0; i < dirs.length; i++) { // 并发删除子目录
              let dir = dirs[i];
              removeDir(dir, done); // 每个删除完毕后 累加删除的数量
            }
          })
        } else {
          fs.unlink(dir, cb); // 删除文件即可
        }
      })
    }
    
    // 删除 a 文件夹
    removeDir('a', () => console.log('异步并发删除'));
    
打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

中午好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. 一次性文件操作
    1. 1.1. 常用 API
    2. 1.2. 基本使用
    3. 1.3. 案例:md 转 html
  2. 2. 大文件读写操作
    1. 2.1. 常用 API
    2. 2.2. 基本使用
    3. 2.3. 缺点
    4. 2.4. 案例:文件拷贝
  3. 3. 目录操作
    1. 3.1. 常用 API
    2. 3.2. 基本使用
    3. 3.3. 手写创建目录、删除目录
      1. 3.3.1. 同步递归创建目录
      2. 3.3.2. 异步递归创建目录
      3. 3.3.3. 同步删除目录
      4. 3.3.4. 异步递归删除目录