readFile(readFileSync)(path[, options], callback)
第一个参数是文件路径、第二个参数是配置的参数例如编码格式,第三个是回调函数(同步方法是没有回调函数的)
//是把整个文件作为一个整体 fs.readFile('./1.txt',{encoding:'utf8'},function (err,data) { console.log(err); console.log(data); }); //同步方法是没有回调函数的 let result=fs.readFileSync('./1.txt',{encoding: 'utf8'}); console.log(result);复制代码
writeFile(writeFileSync)(file, data[, options], callback)
第一个参数是文件路径、第二个参数是写入的内容,第三个是参数配置,第四个是回调函数。
fs.writeFile('./2.txt','123',{encoding: 'utf8'},(err) => { console.log('write OK'); }) fs.writeFileSync('./3.txt','456');复制代码
简单的实现拷贝
const fs=require('fs'); function copy(src,dest,cb) { fs.readFile(src,(err,data) => { fs.writeFile(dest,data,cb); }); } copy('3.txt','4.txt',() => { console.log('拷贝完成'); });复制代码
appendFile(追加文件内容,不能使用writeFile,因为writeFile存在的话会先清空在写入,不存在会创建)
fs.writeFile('./4.txt','789',{flag:'a'}); fs.appendFile('./4.txt','789');复制代码
flags(配置参数选项)
linux权限
编码问题(gbk->utf8转换过程中会出现一个BOM头)
let path = require('path'); let result = fs.readFileSync(path.resolve(__dirname,'./text.txt')); console.log(stripBOM(result).toString()) function stripBOM(content){ if(Buffer.isBuffer(content)){ if(content[0] === 0xef && content[1] === 0xbb && content[2] === 0xbf ){ return content.slice(3); } }else{ if (content.charCodeAt(0) === 0xFEFF) { content = content.slice(1); } } return content; }复制代码
open(filename,flags,[mode],callback)
//fd file descriptor 文件描述符 是一个数字或者说索引 fs.open('./5.txt','r',(err,fd) => { console.log(fd); fs.open('./4.txt','r',(err,fd) => { console.log(fd); }); });复制代码
read(fd, buffer, offset, length, position, callback((err, bytesRead, buffer)))
- 第一个参数是文件描述符
- 第二个参数用来存放读取的内容的buffer
- 第三个参数存放内容的buffer的起始索引
- 第四个参数存放内容的长度
- 第五个参数读取文件的起始索引
- 第六个参数是回调函数(bytesRead是实际读取的字节数)
fs.open('./6.txt','r',0666,(err,fd) => { let buffer=Buffer.alloc(6);//[0,1,2,3,4,5] fs.read(fd,buffer,0,3,3,(err,bytesRead) => { fs.read(fd,buffer,3,3,6,(err,byteRead) => { console.log(buffer.toString()); }); });});复制代码
write(fd, buffer[, offset[, length[, position]]], callback)
- 第一个参数是文件描述符
- 第二个参数用来存放要写入的内容的buffer
- 第三个参数存放要写入的内容的buffer的起始索引
- 第四个参数存放要写入的内容的长度
- 第五个参数写入文件的起始索引
- 第六个参数是回调函数
fs.open('./6.txt','r+',0666,(err,fd) => { let buffer=Buffer.from('珠峰培训');//[0,1,2,3,4,5,6,7,8,9,10,11] //fd buffer offset fs.write(fd,buffer,3,6,3,(err,bytesWritten) => { console.log(err); // 先同步缓存在进行关闭 fs.fsync(fd,(err) => { fs.close(fd,(err) => { console.log('关闭文件'); }); }); }); });复制代码
fsync(fd,[callback]) (同步磁盘缓存)
close(fd,[callback])(关闭文件)
read和write实现的copy
function copy(src,dest) { fs.open(src,'r',(err,readFd)=> { fs.open(dest,'w',(err,writeFd) => { let buffer=Buffer.alloc(BUFFER_SIZE); let readed=0; let writed=0; function next() { fs.read(readFd,buffer,0,BUFFER_SIZE,readed,(err,bytesRead) => { readed+=bytesRead; bytesRead&&fs.write(writeFd,buffer,0,bytesRead,writed,(err,bytesWritten) => { writed+=bytesWritten; next(); }); }); }; next(); }); });}复制代码
目录操作
- mkdir (创建目录的时候要求父目录必须存在)
fs.mkdir('a/b/c',err => { console.log(err); console.log('创建成功'); });复制代码
- access (判断文件是否存在)
fs.access('b',(err) => { console.log(err); });复制代码
同步创建目录
function mkpSync(dir) { let parts=dir.split(path.sep);//['a','b','c'] for (let i=1;i<=parts.length;i++){ // a a/b a/b/c let current=parts.slice(0,i).join(path.sep); try { fs.accessSync(current); } catch (err) { fs.mkdirSync(current); } } }复制代码
异步创建目录
function mkpAsync(dir,callback) { let parts=dir.split(path.sep);//[a,b,c] let index=1; function next() { if (index>parts.length) return callback(); let current=parts.slice(0,index).join(path.sep);//a index++; fs.access(current,(err) => { if (err) { fs.mkdir(current,next); } else { next(); } }); } next();}复制代码
终极await/async实现创建目录
function promisify(fn) { return function (...args) { return new Promise((resolve,reject) => { fn.call(null,...args,err=>err? reject():resolve()); }); }}async function mkp(dir) { let parts=dir.split(path.sep);//['a','b','c'] for (let i=1;i<=parts.length;i++){ // a a/b a/b/c let current=parts.slice(0,i).join(path.sep); try { await promisify(fs.access)(current); } catch (err) { await promisify(fs.mkdir)(current); } }}复制代码
删除目录(要删除目录需要先把目录中的内容读出来)
读取目录内容、路径拼接、判断文件类型(不同方法处理)
- 删除目录(fs.rmdirSync)
- 删除文件(fs.unlinkSync)
let files = fs.readdirSync('a'); files = files.map(file => path.join('a',file)); files.forEach(file=>{ let statObj = fs.statSync(file); if(statObj.isDirectory()){ fs.rmdirSync(file); }else{ fs.unlinkSync(file); } })复制代码
先序深度优先
// 同步 let fs = require('fs'); let path = require('path'); function removeDir(p) { let statObj = fs.statSync(p); if(statObj.isDirectory()){ let dirs = fs.readdirSync(p); dirs = dirs.map(dir => path.join(p,dir)); for(let i = 0; i{ fs.stat(p,(err,statObj)=>{ // 判断文件类型 是目录 递归 否则就删除即可 if(statObj.isDirectory()){ fs.readdir(p, function (err, dirs) { // 映射路径 dirs = dirs.map(dir => path.join(p, dir)); // 映射promise dirs = dirs.map(dir => removeDir(dir)); // 删除完儿子后 删除自己 Promise.all(dirs).then(() => { fs.rmdir(p, resolve); }); }); }else{ fs.unlink(p,resolve); } }); }); } function removeDir(p,callback) { fs.stat(p,function (err,statObj) { if (statObj.isDirectory()){ fs.readdir(p,function (err,dirs) { dirs = dirs.map(dir=>path.join(p,dir)); // 我们希望 可以同时删除这些目录 if(dirs.length == 0) return fs.rmdir(p,callback); // 先预定一个函数 所有儿子都删除了的函数回调 let index = 0; function all() { index++; if (index === dirs.length) fs.rmdir(p, callback); // if (index === dirs.length) fs.rmdir(p, ()=>callback()); } dirs.forEach(dir=>{ removeDir(dir, all); }); }); }else{ fs.unlink(p,callback) } }); } // async和await let fs = require('fs'); let path = require('path'); let util = require('util'); let stat = util.promisify(fs.stat); let readdir = util.promisify(fs.readdir); let rmdir = util.promisify(fs.rmdir); let unlink = util.promisify(fs.unlink); async function removeDir(p) { let statObj = await stat(p); if(statObj.isDirectory()){ let dirs = await readdir(p); dirs = dirs.map(dir=>path.join(p,dir)); dirs = dirs.map(dir => removeDir(dir)); await Promise.all(dirs); await rmdir(p); }else{ // 要等待文件删除后 才让promise执行完 所以需要await await unlink(p); } } // 回调写法 串行 let fs = require('fs'); let path = require('path'); function removeDir(p,callback) { fs.stat(p,(err,statObj)=>{ if(statObj.isDirectory()){ fs.readdir(p,function (err,dirs) { // 异步怎么递归? // next函数用来递归的 dirs = dirs.map(dir => path.join(p, dir)); // 标识先删除第一个 function next(index) { if (index === dirs.length) return fs.rmdir(p, callback) let file = dirs[index]; // 删除目录后将下一次的删除继续传递 removeDir(file, ()=>next(index+1)); } next(0); }) }else{ // 文件删除执行callback即可 fs.unlink(p,callback); } }); }复制代码
先序广度优先
let fs = require('fs'); let path = require('path'); function removeDir(p) { let arr = [p]; let index = 0; let current; while (current = arr[index++]) { let statObj = fs.statSync(current); if (statObj.isDirectory()) { let dirs = fs.readdirSync(current); arr = [...arr, ...dirs.map(dir => path.join(current, dir))]; } } for (let i = arr.length - 1; i >= 0; i--) { let statObj = fs.statSync(arr[i]); if (statObj.isDirectory()) { fs.rmdirSync(arr[i]) }else{ fs.unlinkSync(arr[i]) } } }复制代码
可读流、可写流
流有两种模式 一种是暂停模式 一种是流动模式
createReadStream
let rs = fs.createReadStream('./1test.js',{ flags:'r', // 读取的方式 // encoding:null,// 编码 buffer autoClose:true, start:0, end:9, // 包后 highWaterMark:2 // 最高水位线 }); let arr = [] rs.on('data',function (data) { rs.pause(); // 暂停 暂停触发data事件 arr.push(data); setTimeout(() => { rs.resume(); }, 1000); }); rs.on('error',function (err) { console.log(err); }); rs.on('end',function () { console.log(Buffer.concat(arr).toString()); });复制代码
createWriteStream
写 (第一次会真的往文件里写) 后面会写到缓存中。highWaterMark只是一个标识而已,一般配合着读取来用。当写入的内容超过highWaterMark的时候会暂停一下。
let fs = require('fs'); let ws = fs.createWriteStream('2.txt',{ flags:'w', encoding:'utf8', autoClose:true, start:0, highWaterMark:3 }); let flag = ws.write('1'); console.log(flag); // true flag = ws.write('1'); console.log(flag); // true flag = ws.write('1'); console.log(flag); // false ws.on('drain',function () { console.log('抽干') }); // 抽干方法必须当前的写入的内容(内存+文件) 已经大于等于了highWater,才会触发drain,当内容全部写入后 会执行drain方法 ws.end('我死了');//会将缓存区的内容 清空后再关闭文件 ws.write('ok');// write after end不能再结束后继续写入复制代码
手动实现的可读流(createReadStream)
let EventEmitter = require('events'); let fs = require('fs'); class ReadStream extends EventEmitter { constructor(path, options = {}) { super(); // 默认参数配置 this.path = path; this.autoClose = options.autoClose || true; this.flags = options.flags || 'r'; this.encoding = options.encoding || null; this.start = options.start || 0; this.end = options.end || null; this.highWaterMark = options.highWaterMark || 64 * 1024; // 应该有一个读取文件的位置 可变的(可变的位置) this.pos = this.start; // 控制当前是否是流动模式 this.flowing = null; // 构建读取到的内容的buffer this.buffer = Buffer.alloc(this.highWaterMark); // 当创建可读流 要将文件打开 this.open(); // 异步执行 // 判断是否绑定了新的事件 this.on('newListener', (type) => { if(type === 'data'){ // 用户监听了data事件,就开始读取吧 this.flowing = true; this.read();// 开始读取文件 } }); } read(){ // 这时候文件还没有打开呢,等待着文件打开后再去读取 if(typeof this.fd !== 'number'){ // 等待着文件打开,再次调用read方法 return this.once('open',()=>this.read()); } // 开始读取了 // 文件可能有10个字符串 // start 0 end 4 // 每次读三个 3 // 0-2 // 34 let howMuchToRead = this.end ? Math.min(this.highWaterMark,this.end - this.pos+1) :this.highWaterMark // 文件描述符 读到哪个buffer里 读取到buffer的哪个位置 // 往buffer里读取几个,读取的位置 // 想读三个 文件只有2个 fs.read(this.fd, this.buffer,0,howMuchToRead,this.pos,(err,bytesRead)=>{ if (bytesRead>0){ // 读到内容了 this.pos += bytesRead; // 保留有用的 let r = this.buffer.slice(0, bytesRead); r = this.encoding ? r.toString(this.encoding) : r; // 第一次读取 this.emit('data', r); // 流动模式 if (this.flowing) { this.read(); } }else{ this.emit('end'); this.destroy(); } }); } pipe(dest){ this.on('data',(data)=>{ let flag = dest.write(data); if(!flag){ this.pause(); } }); dest.on('drain',()=>{ this.resume(); }); this.on('end',()=>{ this.destroy(); }); } destroy() { // 判断文件是否打开 (将文件关闭掉) // 文件已经打开了 if (typeof this.fd === 'number') { fs.close(this.fd, () => { this.emit('close'); }); return; } this.emit('close'); } open() { // 打开文件的逻辑 fs.open(this.path, this.flags, (err, fd) => { if (err) { this.emit('error', err); if (this.autoClose) { this.destroy(); // 销毁 关闭文件(触发close事件) } return; } this.fd = fd; this.emit('open'); // 触发文件开启事件 }); } pause(){ this.flowing = false; } resume(){ this.flowing = true; this.read(); // 继续读取 } } module.exports = ReadStream;复制代码
手动实现的可写流(createWriteStream)
let fs = require('fs');let EventEmitter = require('events');class WriteStream extends EventEmitter{ constructor(path,options ={}){ super(); this.path = path; this.flags = options.flags || 'w'; this.mode = options.mode || 0o666; this.highWaterMark = options.highWaterMark || 16*1024; this.start = options.start || 0; this.autoClose = options.autoClose|| true; this.encoding = options.encoding || 'utf8'; // 是否需要触发drain事件 this.needDrain = false; // 是否正在写入 this.writing = false; // 缓存 正在写入就放到缓存中 this.buffer = []; // 算一个当前缓存的个数 this.len = 0; // 写入的时候也有位置关系 this.pos = this.start; this.open(); } // 0 [1 2] write(chunk, encoding = this.encoding,callback){ chunk = Buffer.isBuffer(chunk)?chunk:Buffer.from(chunk); this.len += chunk.length;// 每次调用write就统计一下长度 this.needDrain = this.highWaterMark <= this.len; // this.fd if(this.writing){ this.buffer.push({chunk,encoding,callback}); }else{ // 当文件写入后 清空缓存区的内容 this.writing = true; // 走缓存 this._write(chunk,encoding,()=>this.clearBuffer()); } return !this.needDrain; // write 的返回值必须是true / false } _write(chunk,encoding,callback){ if (typeof this.fd !== 'number') { return this.once('open', () => this._write(chunk, encoding, callback)); } // fd是文件描述符 chunk是数据 0 写入的位置和 长度 , this.pos偏移量 fs.write(this.fd, chunk,0,chunk.length,this.pos,(err,bytesWritten)=>{ this.pos += bytesWritten; this.len -= bytesWritten; // 写入的长度会减少 callback(); }); } clearBuffer(){ let buf = this.buffer.shift(); if(buf){ this._write(buf.chunk, buf.encoding, () => this.clearBuffer()); }else{ this.writing = false; this.needDrain = false; // 触发一次drain 再置回false 方便下次继续判断 this.emit('drain'); } } destroy(){ if(typeof this.fd === 'number'){ fs.close(this.fd,()=>{ this.emit('close'); }); return } this.emit('close'); } open(){ fs.open(this.path,this.flags,this.mode,(err,fd)=>{ if(err){ this.emit('error'); this.destroy(); return } this.fd = fd; this.emit('open'); }); }}module.exports = WriteStream;复制代码
继承Stream的可读流
//我的流如果继承了 Readable接口 就必须要重写一个_read的方法,并且有push方法 class MyReadStream extends Readable{ // read _read constructor(){ super(); this.index = 0; } _read(){ if(this.index == 5){ return this.push(null); // 读取完毕了 } this.push(this.index+++''); // push方法也是Readble实现的 } } let rs = new MyReadStream(); rs.on('data',function (data) { console.log(data); }) rs.on('end',function () { console.log('end'); })复制代码
继承Stream的可写流
let { Writable } = require('stream'); // 流的模块 let fs = require('fs'); // 我的流如果继承了 Readable接口 就必须要重写一个_write的方法 class MyWriteStream extends Writable { // write _write constructor() { super(); } _write(chunk,encoding,clearBuffer) { fs.appendFile('1.txt',chunk,function () { clearBuffer(); }) } } let ws = new MyWriteStream(); ws.write('hello','utf8',function () { console.log('ok'); }); ws.write('hello', 'utf8', function () { console.log('ok'); });复制代码
readable
let fs = require('fs'); let rs = fs.createReadStream('./1.txt',{ autoClose:true, start:0, flags:'r', encoding:'utf8', highWaterMark:3// 默认先在杯子里 填 3滴水 }) // 暂停模式先把水杯 给你填满,自己去喝 喝多少取决于你自己 // 1).readable 当杯子里的水 是空的时候 会触发readable事件(还会将杯子里的水在填入 highWaterMark个) // 2).如果当前杯子里的水 小于hightWaterMark 会再次读取highWaterMark个 // 3) 行读取器 rs.on('readable',()=>{ let r = rs.read(1); console.log(rs._readableState.length);// 查看剩余的数量 setTimeout(()=>{ console.log(rs._readableState.length); },5000) });复制代码
lineReader
// 行读取器 没读完一行 就把这一行的内容 发射出来 let EventEmitter = require('events'); let fs = require('fs'); class LineReader extends EventEmitter { constructor(path) { super(); this.path = path; let RETURN = 13; let LINE = 10; this.arr = []; // 存放内容的 // \r 13 windows 怎么表示是新的一行 就用\r // \n 10 mac 没有\r 只有\n this._rs = fs.createReadStream(this.path); // 64k // 判断用户监听了newLine事件 let r ; this.on('newListener', (type) => { if (type === 'newLine') { this._rs.on('readable', () => { let current; // 当前读出来的内容 while (current = this._rs.read(1)) { switch (current[0]) { case RETURN: r = Buffer.concat(this.arr).toString(); this.emit('newLine', r); this.arr = []; // 如果下一个是换行 我就抛弃掉如果不是换行 我就留到数组里 let next = this._rs.read(1); if (next[0] !== LINE) { this.arr.push(current); } break; case LINE: r = Buffer.concat(this.arr).toString(); this.emit('newLine', r); this.arr = []; default: this.arr.push(current); } } }); this._rs.on('end', () => { let r = Buffer.concat(this.arr).toString(); this.emit('newLine', r); }) } }) } } // 行读取器 let lineReader = new LineReader('./1.txt'); lineReader.on('newLine', (data) => { console.log(data, '-------------');// 123 // 456 // 789 }); 复制代码
转化流和双工流
双工流需要重写_write和_read方法。转化流重写_transform,应用场景在压缩。
let {Duplex,Transform} = require('stream'); class MyDuplex extends Duplex{ // 可能是没关系 也可能是有关系 _read(){ this.push('hello'); this.push(null); } _write(chunk,encoding,clearBuffer){ console.log(chunk); clearBuffer(); } } let my = new MyDuplex(); my.on('data',function (data) { console.log(data); }) my.write('hello') my.write('hello') // 转化流 用的比较多的地方 压缩 class MyTransfer extends Transform{ _transform(chunk,encoding,clearBuffer){ // 参数和可写流是一样的 let str = chunk.toString().toUpperCase(); this.push(str); clearBuffer(); } } let my = new MyTransfer(); // 会箭头可读流中的内容 把内容写入到可写流中 process.stdin.pipe(my).pipe(process.stdout); // 应用场景压缩复制代码