一次性文件操作
readFile、writeFile、appendFile、copyFile 都是一次性的操作,例如 copyFile 会将文件内容一次性获取并放到内存中,然后再一次性写入另一个文件,这些都 不适用于大内存的文件操作;
文件操作 API 通常最后一个参数是一个回调函数,Nodejs 中的回调函数通常都是 error-first 错误优先风格的
(err, …args) => {}
,即第一个参数是错误信息,后面还是要处理的数据,当没有错误的时候 err 为 null;
常用 API
主要的文件操作就是文件读写、拷贝、监控:Nodejs 中几乎所有文件 API 操作都有 「同步」 和 「异步」 两种方式,同步 API 名称比异步 API 名称多个 Sync,如 readFile 对应的同步 API 是 readFileSync;
API | 描述 |
---|---|
fs.readFile | 从指定文件中读取数据 |
fs.writeFile | 向指定文件中写入数据 |
fs.appendFile | 向指定文件中追加数据 |
fs.copyFile | 将某个文件中的数据拷贝到另一个文件 |
fs.watchFile | 监听指定文件,当文件内容发生修改,触发回调函数(没有对应的同步 API) |
基本使用
-
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) // 报错 })
-
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!世界 }) } } )
-
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 }) } )
-
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('拷贝成功') })
-
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 页面,并实时更新;
-
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 // 监听更新 })
-
index.md
### 标题一 * 列表项2 * 列表项3 * 列表项4 * 列表项5 ### 标题二
-
执行命令
node mdToHtml.js index.md
大文件读写操作
前面的 API 是将文件中的数据 一次性 的 读取/写入 到内存中,这种方式对于大体积的文件来说,显然不合理:所以需要一个可以 「边读边写」 或 「边写边读」 的操作方式,这就需要将文件的打开、读取、写入、关闭看作各自独立的环节;
常用 API
API | 描述 |
---|---|
fs.open | 打开文件 |
fs.close | 关闭文件 |
fs.read | 将磁盘文件中的数据写入到 buffer 中 |
fs.write | 将 buffer 里的数据写入到磁盘文件中 |
基本使用
-
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) })
-
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('关闭成功')) })
-
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 } ) })
-
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 }) })
缺点
相对于 readFile 和 writeFile 一次性读写,这种方式会减轻内存的消耗,提高代码执行性能;
读写操作存在 多次 I/O 操作,会导致 CPU 和 内存资源 的浪费;
建议使用 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 | 删除文件和目录 |
基本使用
-
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('有操作权限') } })
-
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()) // 是否目录 })
-
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 创建成功') // 进入这里 } })
-
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 删除成功') } })
-
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' ] */)
-
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('不会进入到这里') } })
-
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'))
异步递归创建目录
-
回调方式
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('创建完成'))
-
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');
异步递归删除目录
-
串行删除
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('删除完成'))
-
并发删除
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('异步并发删除'));
webpack🤚 面试题
上一篇