手写 fs 核心方法
背景
fs 是 Node 里用来进行文件操作的核心模块,这篇文章的目的是学习并手写一些常用的 api。
这次手写的方法是 writeFile、readFile、appendFile、copyFile,开始手写之前,我们要先了解以下几个基础 Api:
- fs.open:打开一个文件
 - fs.close:关闭一个文件
 - fs.read:读取文件
 - fs.write:写入文件
 
为什么要先了解这些 Api 呢,这就好比把大象放进冰箱需要几步?
- 打开冰箱 (打开文件)
 - 把大象放进冰箱 (读取文件/写入文件)
 - 关闭冰箱 (关闭文件)
 
fs.open
fs.open(path[, flags[, mode]], callback)
打开一个文件。对文件进行操作之前都要先打开文件。
参数解读:
- path:文件路径
 - flags:文件系统标志,默认值:'r'。意思是要对文件进行什么操作,常见的有以下几种:
 - r:打开文件用于读取
 
- w:打开文件用于写入
 
- a:打开文件用于追加
 
- mode:文件操作权限,默认值:0o666(可读写)。
 - callback:回调函数。函数上携带的参数如下:
 - err:如果失败,则值为错误原因
 
- fd(number):文件描述符,读取、写入文件时都要用到这个值
 
fs.close
fs.close(fd, callback)
关闭一个文件。文件打开并操作完成后都要关闭文件,以释放内存。
参数解读
- fd:要关闭的文件描述符
 - callback:文件关闭时的回调
 - err
 
fs.read
fs.read(fd, buffer, offset, length, position, callback)
读取文件。readFile 就是基于此方法实现的。
参数解读
- fd:要读取的文件描述符
 - buffer:数据要被写入的 buffer(将读取到的文件内容写入到此 buffer 内)
 - offset:buffer 中开始写入的偏移量(从 buffer 的第几个索引开始写入)
 - length:读取的字节数(从文件中读取几个字节)
 - postion:指定从文件中开始读取的位置(从文件的第几个字节开始读)
 - callback:回调函数
 - err
 
- bytesRead:实际读取的字节数
 
fs.write
写入文件。writeFile 基于此方法实现。
fs.write(fd, buffer[, offset[, length[, position]]], callback)
- fd:要被写入的文件描述符
 - buffer:将指定 buffer 的内容写入到文件中去
 - offset:指定 buffer 的写入位置(从 buffer 的第 offset 个索引读取内容写入到文件中去)
 - length:指定要写入的字节数
 - postion:文件的偏移量(从文件的第 position 个字节开始写入)
 
淦
划水完毕,进入正题。
readFile 方法实现
我们先看以下原生方法的使用
fs.readFile('../a.js', function(err, file) {
  console.log(file) // 输出的是一个存储文件二进制的 buffer 对象
})
fs.readFile('../a.js', { encoding: 'utf8' }, function(err, file) {
  console.log(file) // 输出的是一个字符串
})
第一个参数是文件路径,第二个参数是读取完成后的回调,回调内可以获取文件内容。
然后开始实现这个方法。
按照把大象装进冰箱的步骤来实现:
1. 打开冰箱
const readFile = (path, options, cb) => {
  // 尝试获取 cb,如果获取不到则抛出错误
  // 由于 options 是非必填参数,所以它有可能是回调函数
  cb = maybeCallback(cb || options)
  
  // 获取 options,如果未穿 options,则取默认参数
  options = getOptions(options, { flag: 'r' })
  // 打开文件,
  // 如果打开失败则直接调用 cb,传入失败原因
  fs.open(path, options.flag, 0o666, (err, fd) => {
    if (err) {
      cb(err)
      return
    }
  })
}
const maybeCallback = cb => {
  if (typeof cb === 'function') {
    return cb
  }
  throw new TypeError('Callback must be a function')
}
const getOptions = (options, defaultOptions = null) => {
  if (
    options === null ||
    options === undefined ||
    typeof options === 'function'
  ) {
    return defaultOptions
  }
  return Object.assign({}, defaultOptions, options)
}
2. 把大象放入冰箱(读取文件)
// 申请一个 10 字节的 buffer
const readBuf = Buffer.alloc(10)
// 文件读取的位置
let pos = 0
// 读取 fs.open 打开的文件(fd)
// 将文件内容读取到 readBuf 内 (buffer)
// 从 readBuf 的第 0 个字节开始读入 (offset)
// 读取的长度为 readBuf.length (length)
// 从文件的第 pos 个字节开始读取 (postion)
fs.read(fd, readBuf, 0, readBuf.length, pos, (err, bytesRead) => {
  if (err) {
    cb(err)
    return
  }
  
  console.log(readBuf) // 文件里的内容已写入到 readBuf
  cb(readBuf)
})
现在我们已经将内容读取到 readBuf 内,并通过 cb 传给函数调用者,但是存在这一个严重的问题,那就是我们只读取了 10 字节的内容,显然文件的内容是非常有可能比 10 字节要多。
那有没有办法一次性将文件的内容全部读取出来呢?
I'm sorry~~~ 木有办法,因为我们并拿不到文件的长度,所以就老老实实一点一点的将内容读出来吧
所以我们改进代码如下:
// 申请一个 10 字节的 buffer
const readBuf = Buffer.alloc(10)
// 文件读取的位置
let pos = 0
// 返回值,读取到的文件内容
let retBuf = Buffer.alloc(0)
const next = () => {
  // 读取 fs.open 打开的文件(fd)
  // 将文件内容读取到 readBuf 内 (buffer)
  // 从 readBuf 的第 0 个字节开始读入 (offset)
  // 读取的长度为 readBuf.length (length)
  // 从文件的第 pos 个字节开始读取 (postion)
  fs.read(fd, readBuf, 0, readBuf.length, pos, (err, bytesRead) => {
    if (err) {
      cb(err)
      return
    }
    // bytesRead 为实际读取到的文件字节数
    // 当读取不到内容时(bytesRead = 0),则代表文件读取完毕
    if (!bytesRead) {
      cb(
        null,
        options.encoding ?
          retBuf.toString(options.encoding) :
          retBuf
      )
      return;
    }
    
    // 计算下次读取文件的位置
    pos += bytesRead
    // 将读取到的内容合并到 retBuf 内
    retBuf = Buffer.concat([retBuf, readBuf])
    // 递归调用
    next()
  })
}
next()
我们将读取文件的操作封装成一个方法,然后递归调用,直到读取不到内容为止。
3. 关闭冰箱
这一步超简单
if (!bytesRead) {
  fs.close(fd, () => {})
  cb(
    null,
    options.encoding ?
      retBuf.toString(options.encoding) :
      retBuf
  )
  return;
}
完整代码
const readFile = (path, options, cb) => {
  // 尝试获取 cb,如果获取不到则抛出错误
  // 由于 options 是非必填参数,所以它有可能是回调函数
  cb = maybeCallback(cb || options)
  // 获取 options,如果未穿 options,则取默认参数
  options = getOptions(options, { flag: 'r' })
  // 打开文件,
  // 如果打开失败则直接调用 cb,传入失败原因
  fs.open(path, options.flag, 0o666, (err, fd) => {
    if (err) {
      cb(err)
      return
    }
    // 申请一个 10 字节的 buffer
    const readBuf = Buffer.alloc(10)
    // 文件读取的位置
    let pos = 0
    // 返回值,读取到的文件内容
    let retBuf = Buffer.alloc(0)
    // 读取 fs.open 打开的文件(fd)
    // 将文件内容读取到 readBuf 内 (buffer)
    // 从 readBuf 的第 0 个字节开始读入 (offset)
    // 读取的长度为 readBuf.length (length)
    // 从文件的第 pos 个字节开始读取 (postion)
    const next = () => {
      fs.read(fd, readBuf, 0, readBuf.length, pos, (err, bytesRead) => {
        if (err) {
          cb(err)
          return
        }
        // bytesRead 为实际读取到的文件字节数
        // 当读取不到内容时(bytesRead = 0),则代表文件读取完毕
        if (!bytesRead) {
          fs.close(fd, () => {})
          cb(
            null,
            options.encoding ?
              retBuf.toString(options.encoding) :
              retBuf
          )
          return;
        }
        // 计算下次读取文件的位置
        pos += bytesRead
        // 将读取到的内容合并到 retBuf 内
        retBuf = Buffer.concat([retBuf, readBuf])
        // 递归调用
        next()
      })
    }
    next()
  })
}
fs.writeFile 实现
原生方法的使用:
// 将字符串 1 以 utf8 编码的形式写入 w-test1.js 内
fs.writeFile('./w-test1.js', '1', { encoding: 'utf8' },(err) => {})
// 直接写入二进制数据,即将 buffer 写入到文件
fs.writeFile('./w-test1.js', Buffer.from('1'), (err) => {})
不多解释了,想象着大象就搞得定,这里直接把代码贴出来(代码里的注释还是很详细的)。
const writeFile = (path, data, options, cb) => {
  cb = maybeCallback(cb || options)
  options = getOptions(options, {
    encoding: 'utf8',
    mode: 0o666, // 可读写的文件权限
    flag: 'w'  // 以写文件的形式打开文件
  })
  // 打开文件
  fs.open(path, options.flag, fs.mode, (err, fd) => {
    // 判断下写入的类型是否为 buffer
    // 如果是字符串则转成 buffer
    const buf = Buffer.isBuffer(data) ?
      data :
      Buffer.from(data, options.encoding)
    
    // 这里我们假设一次只写入 4 个字节
    // 如果要写入的字节少于 4 个,则取最小值
    const writtenLen = buf.length 4 ? buf.length : 4
    // 文件写入的位置
    let pos = 0
    const next = () => {
      // 将内容写入到 fs 打开的文件(fd)
      // 要被写入的 buffer 为 buf
      // 从 buffer 的第 pos 个文章开始写入
      // 写入的长度 为 writtenLen
      // 从文件的第 pos 个文章开始写入
      fs.write(fd, buf, pos, writtenLen, pos, (err, bytesWritten) => {
        if (err) {
          return cb(err)
        }
        if (!bytesWritten) {
          next()
        }
        pos += bytesWritten
      })
    }
    next()
  })
}
fs.appendFile
追加文件
与 writeFile 不同的是,writeFile 会覆盖之前的内容,而 appendFile 是追加内容。
原生方法的使用方式如下:
fs.appendFile('./w-test2.js', '1', (err) => {console.log(err)})
实现原理很简单,改变 flags 的属性为追加状态即可
const appendFile = (path, data, options, cb) => {
  cb = maybeCallback(cb || options)
  options = getOptions(options, {
    encoding: 'utf8',
    mode: 0o666,
    flag: 'a'
  })
  writeFile(path, data, options, cb)
}
最后一个方法 copyFile
原生方法的使用方式如下:
fs.copyFile('源文件.txt', '目标文件.txt', callback);
代码先不解释了 xdm,已经快一点了,我得先睡了,先贴代码,有时间再补解释
const copyFile = (source, target, cb) => {
  cb = maybeCallback(cb)
  const BUFFER_LEN = 4
  const buf = Buffer.alloc(BUFFER_LEN)
  let pos = 0
  fs.open(target, 'r', 0o666, (err, rfd) => {
    if (err) {
      return cb(err)
    }
    fs.open(source, 'w', 0o666, (err, wfd) => {
      if (err) {
        return cb(err)
      }
      const next = () => {
        fs.read(rfd, buf, 0, BUFFER_LEN, pos, (err, bytesRead) => {
          if (err) {
            return cb(err)
          }
          fs.write(wfd, buf, 0, bytesRead, pos, () => {
            console.log('写入')
            pos += bytesRead
            if (!bytesRead) {
              return
            }
            next()
          })
        })
      }
      next()
    })
  })
}
最后
下一期会手写文件流(ReadStream/WriteStream),敬请期待。
如果这篇文章对你有帮助,希望点赞鼓励一下笔者~~~

                  
                  
                  
                  
本文详细介绍了如何手动实现Node.js中FS模块的核心方法,包括readFile、writeFile、appendFile和copyFile等,通过逐步拆解这些方法的工作原理,帮助读者深入理解文件操作的底层机制。
          
      
          
                
                
                
                
              
                
                
                
                
                
              
                
                
              
            
                  
被折叠的  条评论
		 为什么被折叠?
		 
		 
		
    
  
    
  
            


            