Node.js fs模块(一)_qfc_128220的博客-CSDN博客
在前面一节中,我们介绍了fs.open,fs.read,fs.write,fs.close等方法的使用,但是我们发现实操起来比较繁琐,主要难点在于 读写文件前,需要先打开文件获取fd,并且需要设置好文件操作符,以及文件权限位,否则很容易导致读写过程发生错误,并且读写完文件后,也要人为关闭文件描述符fd,所以整体上很公式化。另外还有需要人为准备缓冲区,并控制缓冲区大小。
所以 Node.js 也提供了整合型方法,来实现一个方法实现文件操作,而内敛了fs.open,fs.close等繁琐操作。
目录
fs.readFile(path, options, callback)
fs.readFileSync(path, options)
fs.writeFile & fs.writeFileSync
fs.writeFile(file, data, options, callback)
fs.writeFileSync(file, data, options)
fs.copyFile(src, dest, callback)
fs.readFile & fs.readFileSync
fs提供了一步到位的读取文件数据方法,有异步读取fs.readFile和同步读取fs.readFileSync
fs.readFile(path, options, callback)
形参解析
path | 要读取的文件所在路径,也可以是文件描述符fd | ||||
options |
| ||||
callback |
|
可以发现读取操作简单了很多,我们再来测试下options.encoding对callback(data)的影响
可以发现如果不指定options.encoding,则表示options.encoding为null,则data打印了buffer对象
如果指定了encoding为utf8,则data打印字符串,说明encoding作用于data数据格式。如果我们不在意data的数据格式的话,一般会省略options参数的书写,因为options.flag默认是r,和读取操作匹配。此时就更加精简了。
另外需要注意的是fs.readFile的path参数如果是路径的话,则fs.readFile底层自动执行fs.open,当读取完成后,自动执行fs.close,但是如果fs.readFile的path参数传入的是fd的话,则其底层不会执行fs.open和fs.close,即需要我们人为关闭fd,所以建议直接使用路径,而不是fd。
fs.readFileSync(path, options)
模拟实现fs.readFile
const fs = require('fs') // fs虽然是Node.js的内置模块,但是需要require引入,原因是节省内存
const path = require('path')
const { promisify } = require('util')
const open = promisify(fs.open)
const read = promisify(fs.read)
async function readFile(path, options, callback) {
let fd
try {
fd = await open(path, options.flag, 0o666) // 自动打开fd
const size = 2 // 每次读取的数据字节数
let start = 0 // 从文件的哪个位置开始读取
const arr = [] // 缓存每次读取出来的buffer
while (true) {
const { bytesRead, buffer } = await read(fd, Buffer.alloc(size), 0, size, start)
if (bytesRead === 0) {
break
}
start += size
arr.push(buffer)
}
let buf = Buffer.concat(arr)
if (options.encoding) {
buf = buf.toString(options.encoding)
}
callback(null, buf)
} catch (err) {
callback(err, null)
} finally {
fs.close(fd) // 自动关闭fd
}
}
readFile(path.resolve('test.txt'), { encoding: 'utf8', flag: 'r' }, (err, buffer) => {
console.log(buffer);
})
console.log(111);
程序编写难度不是很大,主要难度在于一些工具方法的熟悉程度,如Buffer类常用方法的使用,以及fs.open,fs.read,fs.close,以及async await的用法,以及fs.readFile的形参含义和内部逻辑理解。以上只是完成了fs.readFile主干逻辑的模拟,其他一些校验逻辑没有写。
关于fs.readFile性能的思考
在模拟fs.readFile实现的过程中,我们发现,其中有一个size设计,它用来控制了缓冲区大小,以及每次从文件中读取的字节个数,如果每次读取结束时返回的bytesRead不为0,表示文件数据还没有读取完,则要继续下一次读取,最终导致了多次的fs.read调用。
而实际的fs.readFile逻辑中,也有类似size的设计
默认读取操作对应的缓冲区大小为64KB,也有512KB的。
这种固化缓冲区大小的操作,有两方面的考量:
1、被读取的文件数据有多少我们不知道,无法做到一次性准确读取完文件的所有数据
2、防止大文件读取时,创建超大缓冲区,造成服务器内存紧张,进而导致内存溢出
也就是说对于fs.readFile来说,会不断读取文件中数据,并传入缓冲区,当缓冲区满了,则取出缓冲数据,继续下一次读取数据,只有当文件数据读取完了(bytesRead为0),才停止读取动作。
所以fs.readFile会触发多次I/O操作,并且会长期占用内存去缓存每次读取完的数据,所以fs.readFile并不是一个性能友好,也不是一个内存友好的方法。
fs.writeFile & fs.writeFileSync
fs提供了一步到位的写入数据到文件的方法,包括异步写入fs.writeFile和同步写入fs.writeFileSync
fs.writeFile(file, data, options, callback)
形参解析
file参数,该参数可以传入一个路径(不要求一定存在),也可以传入文件描述符fd
data参数,要写入的数据,可以是字符串,也可以是Buffer对象
options.encoding参数,指定写入的data数据的编码,只有data是字符串才有效,需要注意data字符串的实际编码必须要指定的options.encoding相同,否则会发生乱码,默认为utf-8
options.mode参数,文件权限位,默认0o666,可读可写
options.flag参数,文件操作符,fs.writeFile的该参数默认为w,即覆盖写入
callback参数,错误优先的回调函数,err形参指向写入过程的错误信息
fs.writeFileSync(file, data, options)
fs.writeFileSync是同步写入,各形参含义和fs.write相同,无特殊需求可以省略options参数,提高代码简洁度。
模拟实现fs.writeFile
const fs = require('fs') // fs虽然是Node.js的内置模块,但是需要require引入,原因是节省内存
const path = require('path')
const { promisify } = require('util')
const open = promisify(fs.open)
const write = promisify(fs.write)
async function writeFile(file, data, options, callback) {
let fd
try {
fd = await open(file, options.flag, options.mode)
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data, options.encoding || 'utf8')
await write(fd, buf, 0, buf.length, 0)
callback(null)
} catch (error) {
callback(error)
} finally {
fs.close(fd)
}
}
writeFile(path.resolve('test.txt'), 'abcdefg', { encoding: 'utf8', flag: 'w', mode: 0o666 }, err => {
console.log(err);
})
由于写入的数据都在缓冲区中了,所以不需要顾及缓冲区的设计,只管从缓冲区buffer对象中拿数据就行了,所以性能上没有需要额外考虑的。
fs.appendFile
其实逻辑和fs.writeFile设计一致,fs.writeFile只需要改动options.flag参数值为'a',将相当于fs.appendFile了。
也就是说fs.appendFile的options.flag默认值是'a' , 而fs.writeFile的options.flag默认值是'w'
这就是二者的差别
如果将fs.writeFile的options.flag改为'a',则效果和fs.appendFile一致
另外 , 也有fs.appendFileSync同步追加数据到文件的方法
fs.copyFile
fs.copyFile(src, dest, callback)
文件复制是非常常见的一种文件操作,fs模块也提供了专门的文件复制方法fs.copyFile
形参解析
src:目标文件路径
dest:拷贝文件路径
callback:回调函数,无参
操作简单明了,将src的文件,拷贝一份到dest的文件中,dest文件可以不存在。
模拟实现fs.copyFile
const fs = require('fs') // fs虽然是Node.js的内置模块,但是需要require引入,原因是节省内存
const path = require('path')
const { promisify } = require('util')
const open = promisify(fs.open)
const read = promisify(fs.read)
const write = promisify(fs.write)
async function copyFile(src, dest, callback) {
let rfd
let wfd
try {
rfd = await open(src, 'r', 0o666)
const size = 2
let start = 0
const arr = []
while (true) {
const { bytesRead, buffer } = await read(rfd, Buffer.alloc(size), 0, size, start)
if (bytesRead === 0) {
break
}
start += size
arr.push(buffer)
}
let buf = Buffer.concat(arr)
wfd = await open(dest, 'w', 0o666)
await write(wfd, buf, 0, buf.length, 0)
callback()
} catch (err) {
callback(err)
} finally {
fs.close(rfd)
fs.close(wfd)
}
}
copyFile(path.resolve('test.txt'), path.resolve('test_cpp.txt'), () => {
console.log('拷贝完成');
})
fs.copyFile其实就是fs.readFile和fs.writeFile的结合,即先读取src数据,再将数据写入dest