Node基础概念与模块

160 阅读16分钟

node概念

  • 给js提供了一个运行环境,而不是一门语言
  • js两大环境:
    • 浏览器环境:BOM、DOM、window
    • node环境:提供一些模块、global

概念性知识

  • node是单线程,只有一个主线程,但有异步事件循环机制
  • 虽然有事件循环,但多个异步任务还是按照优先级顺序进行执行的,并不是立即执行
    • 如果有多个接口同时打过来,那么会一个一个处理
  • 优点:占用内存少

多线程概念

  • 当多个请求过来时,可以同时处理,适合压缩、计算等
  • 如果多个线程同时处理一个资源会出现不可控情况,所以会有锁的概念
    • 例子:10个人同时上厕所
    • 加锁等于关门

阻塞概念

  • 同步阻塞:a请求b,在b那里需要1分钟来获取数据,所以告诉a先等下,那么a在等待过程中会造成1分钟的阻塞
  • node异步非阻塞:a请求b,然后停止,b获取数据后,通过回调的方式通知a,在这个过程中a可以做其他的事

可以做什么

  • 前端学习成本小
  • 文件的读写操作,浏览器因为安全问题,在每次读写用户模块会有用户进行操作(上传弹窗),node可以通过一些api操作自己的文件
  • 做工具:webpack、gulp、rollup、vite
  • 服务端渲染(ssr),解析js,生成字符串,然后返回
  • 中间层,解决跨域问题,对后端数据进行代理、数据聚合等(多个数据合并)
  • 向后端发展的过渡

提示

  • vscode没给node加声明,所以没有提示
  • 我们可以npm i @types/node自己下载下

调试

  • 点击vscode的左侧小爬虫(Run an Debug)
  • 点击 (创建launch.json文件),选择Node,根目录下会出现(.vscode>launch.json)
  • program为入口文件地址
  • 给左侧加上断点,点击小臭虫上面的绿色三角icon,开始调试(这时候可以返回目录,不影响)

模块

  • commonjs规范,使用modules以及require
  • 内置一些核心包,不用安装,可以省略(绝对||相对)路径
  • 模块化的好处:解决命名冲突、方便复用(组件化基于模块化,所谓的高内聚低耦合)
  • node_modules内,存放第三方包,会逐级向上查找

commonjs的实现

流程

  1. 传入路径
  2. 处理路径为绝对路径、且补全后缀
  3. 拿到文件内容(字符串,还没有执行)
  4. 根据路径判断在缓存映射(一个object)中查找,存在直接返回,否则初始化实例,然后放到缓存映射中
  5. 执行实例方法,将模块所需参数传递进去,然后给返回实例(module)的exports,默认为{}
const fs = require("fs");
const vm = require("vm");
const path = require("path");

function Module(id) {
  this.id = id;
  this.exports = {};
}
//缓存
Module._cache = {};
//补全地址
Module._resolveFilename = function (filename) {
  let absPath = path.resolve(__dirname, filename);
  let isExists = fs.existsSync(absPath);
  if (isExists) {
    return absPath;
  } else {
    let keys = Object.keys(Module._extensions);
    for (let i = 0; i < keys.length; i++) {
      let newPath = absPath + keys[i];
      let flag = fs.existsSync(newPath);
      if (flag) {
        return newPath;
      }
    }
    throw new Error("module not exists");
  }
};
//后缀对应的解析方法
Module._extensions = {
  ".js"(module) {
    //拿到内容
    let content = fs.readFileSync(module.id, "utf8");
    //包裹函数
    fn = vm.compileFunction(content, [
      "exports",
      "require",
      "module",
      "__filename",
      "__dirname",
    ]);
    let exports = (thisvalue = module.exports);
    let filename = module.id;
    let dirname = path.dirname(module.id);
    //将fn改变this指向为thisvalue,然后将参数传递进去
    Reflect.apply(fn, thisvalue, [exports, req, module, filename, dirname]);
  },
  ".json"(module) {
    let content = fs.readFileSync(module.id, "utf8");
    module.exports = JSON.parse(content);
  },
};
//加载
Module.prototype.load = function () {
  //拿到后缀
  let extName = path.extname(this.id);
  //根据后缀执行函数,改变实例的exports
  Module._extensions[extName](this);
};
function req(id) {
  let filename = Module._resolveFilename(id);
  //查看是否存在缓存中
  let cacheModule = Module._cache[filename];
  if (cacheModule) {
    return cacheModule.exports;
  }
  let module = new Module(filename);
  //放到缓存中
  Module._cache[filename] = module;
  module.load(); //加载文件,给module.exports赋值
  return module.exports;
}

console.log(req("./b"));
console.log(req("./b"));
// console.log(require('./b'));
为什么module.exports可以赋值基本类型,而exports不可以
  • 因为是通过module.exports来赋值的,所以它改变实例的值
  • exports只是一个引用地址,所以把它改变为基本类型的话,外界拿不到,因为只是私有改变,外界没变

内置核心模块

  • fs 文件的读写操作
  • path 路径的处理
  • vm 运行代码

path

  • node中路径不唯一,所以需要使用path处理
  • 属性:__dirname 代表当前文件所在目录,只读,绝对路径
api
  • join(路径, 路径) 按照操作系统将路径合并,可以有n个路径,类似数组的join
  • resolve(路径, 路径) 根据执行脚本的路径,拼接路径,如果使用了/那么就回到根路径
  • extname(路径Or文件名) 获取文件后缀
  • dirname(路径) 获取倒数第二级路径
const path = require('path');
path.dirname('a/b');//a
path.dirname('a/b/c');//a/b

path.extname('a/a');//''
path.extname('a.a');//.a
path.extname('a/a.a');//.a

fs

  • api分两类,带Sync为同步,不带为异步
  • 同步api
    • 以读取为例,如果读取不到会抛出异常,阻塞线程
  • 异步api
    • 会将错误对象传递给回调
    • node中的回调大多采用错误优先
api
  • readFileSync('路径','文件格式') 一次性读取文件,格式可省略,默认为buffer,同步
  • readFile('路径','文件格式',回调) 一次性读取文件
  • existsSync(路径) 判断路径对应文件是否存在,返回布尔
  • writeFile(路径,内容,回调) 覆盖式写文件,文件不存在会自动创建
  • open('路径',权限符,回调) 打开文件
    • 常用权限符:
      • 'r' 代表要读取
      • 'w' 代表要写入
    • 回调(err,fd)
      • fd为标识符,每个操作系统不一样,但都是number,作用是记录下本次操作
      • mac以20开头,每操作一次会+1
  • read(标识符,赋值给谁,从第几个赋值,赋值几个,从第几个开始读,function(err,实际读了几个){}) 读取操作
    • 前置条件,文件已打开open
    • 赋值个数不能超过赋值容器的总数,会抛出异常
    • 注意:如果赋值个数为0,function的第二个参数一定为0,因为没有地方写了
  • write(标识位,内容(buffer),从内容的第几个开始读,读几个,写到文件的哪个位置,function(err,写入了几个){}) 写入部分文件
    • 前置:open
    • 没操作好会导致文件加载异常
//从1.txt读三个字节,写到2.txt中
const buf = Buffer.alloc(3);
fs.open(path.resolve(__dirname, "1.txt"), "r", function (err, rfd) {
  fs.open(path.resolve(__dirname, "2.txt"), "w", function (err, wfd) {
    let roffset = 0;
    let woffset = 0;
    function close() {
      fs.close(rfd);
      fs.close(wfd);
    }
    function next() {
      //读
      fs.read(rfd, buf, 0, 3, roffset, function (err, bytesRead) {
        //如果读不到了,停止递归
        if (bytesRead === 0) return close();
        //写
        fs.write(wfd, buf, 0, bytesRead, woffset, function (err, written) {
          //读了几个,就累加几个,当做下一次的起始
          roffset += bytesRead;
          //写了几个,累加
          woffset += written;
          next();
        });
      });
    }
    next();
  });
});

process

  • 进程信息
  • 是模块,可以导入
  • 挂载在global上,所以也可以直接访问
属性
  • platform
    • 返回平台信息,window是win32、mac是darwin、linux是linux
  • env
    • 环境变量,object
    • 可以直接通过process.env设置,也可以在命令行设置,mac为export 变量名=值
  • argv
    • 用户运行时,传递的参数,以空格划分,返回array
    • 第一项默认为node地址,copy后放命令行等同于node执行
方法
  • cwd()
    • 返回node运行地址,跟path.resolve()返回一致
  • chdir(路径)
    • 根据参数改变node执行的路径
  • nextTick(callback)
    • 最高优先级的异步任务,高于微任务
  • stadin
    • on('data',(chunk)=>{}),可以监听用户在命令行测操作,并且给到回调
    • write(内容),进程的输出,会在命令行显示
const path = require("path");
console.log(process.cwd()); //  a/b
console.log(path.resolve()); // a/b

process.chdir("../"); //根据参数改变node执行路径
console.log(process.cwd()); //  a
console.log(path.resolve()); // a

console.log(process.platform); //平台信息  darwin

console.log(process.env.a); //环境变量,object

console.log(process.argv);//用户运行时,传递的参数
/*
执行命令node ./index.js a=1 b=2
[
  '/Users/nvm/versions/node/v16.13.1/bin/node',   这是node所在地址,直接copy地址执行等同于node执行
  '/Users/Desktop/练习/node/index.js',
  'a=1',
  'b=2'
]
*/


//每次接收的chunk都是个buffer
process.stdin.write('先给你个内容');


process.stdin.on("data", function (chunk) {
  process.stdin.write(chunk.toString());//返回去除空格的内容
});

//ondata与stdin.write的简化语法糖
process.stdin.pipe(process.stdout);

events

  • 事件触发器,常用来做发布订阅
on
  • 事件绑定(注册、订阅)
  • 有一个需要特殊记忆,当注册的事件为newListener的时候,后续每次调用on注册事件,都会触发newListener的回调,且将event传递给它
    • newListener触发时机在注册事件前,如果想让它在注册后,可使用异步任务派发
emit
  • 事件派发(发布)
off
  • 删除事件
once
  • 绑定只执行一次的事件
例子
const EventEmitter = require("events");

function Girl() {}

Object.setPrototypeOf(Girl.prototype, EventEmitter.prototype); //继承,可以理解为class的super
const girl = new Girl();

const weep = function (data) {
  console.log("哭", data);
};
const eat = function (data) {
  console.log("吃饭", data);
};
const sleep = function (data) {
  console.log("睡觉", data);
};

girl.on("newListener", function (type) {
  console.log("绑定", type);
  /*
    异步执行,这样可以拿到所有事件
    process.nextTick(() => {
    girl.emit("放假", "newListener");
  });
  */
  girl.emit("放假", "newListener");
});

girl.on("放假", eat);
girl.on("放假", sleep);
girl.once("放假", weep); //第一次调用后,自动删除


girl.emit("放假", "第二次");
/*
绑定 放假
绑定 放假
吃饭 newListener
绑定 放假
吃饭 newListener
睡觉 newListener
吃饭 第二次
睡觉 第二次
哭 第二次
*/

实现
function EventEmitter() {
  this._events = {}; //实例属性
}
/**
 * 订阅
 * @param {*} event
 * @param {*} listener
 */
EventEmitter.prototype.on = function (event, listener) {
  //可能是直接继承的prototype,那么实例上没有_events,这个时候加一下
  if (!this._events) {
    this._events = {};
  }
  let listeners = this._events[event];
  if (!listeners) {
    this._events[event] = listeners = [];
  }
  //在push之前执行,如果不是newListener,那么执行newListener
  if (event !== "newListener") {
    this.emit("newListener", event);
  }
  listeners.push(listener);
};
/**
 * 发布
 * @param {*} event 事件名
 * @param  {...any} args 参数
 */
EventEmitter.prototype.emit = function (event, ...args) {
  if (!this._events) {
    this._events = {};
  }
  let listeners = this._events[event];
  listeners &&
    listeners.forEach((listener) => {
      listener(...args);
    });
};
/**
 * 移除
 * @param {string} event
 * @param  {function} listener
 */
EventEmitter.prototype.off = function (event, listener) {
  if (!this._events) {
    this._events = {};
  }
  let listeners = this._events[event];
  this._events[event] = listeners.filter((l) => {
    //如果是once,那么它本身会有一个l属性
    return l !== listener && listener.l !== l;
  });
};
/**
 * 只执行一次,那么我们在执行后把它删除即可
 * 思路:包裹一层
 * @param {*} event
 * @param {*} listener
 */
EventEmitter.prototype.once = function (event, listener) {
  const wrapper = (...args) => {
    listener(...args);
    //等它执行后,删除
    this.off(event, wrapper);
  };
  //如果在外部删除的话,传给off的是listener而不是wrapper,所以加个属性,在off的时候判断下
  listener.l = wrapper;
  this.on(event, wrapper);
};
module.exports = EventEmitter;

Buffer

  • node中为了支持二进制数据搞了一个数据类型Buffer( 也可以称为:缓冲区、内存),可以与字符串进行相互的转换
  • 文件的读写,读取的内容都是buffer类型,我们会设置格式将其转换,如utf-8
  • buffer在node中的展现形式是16进制
  • 中文编码utf8 (3个字节 = 一个汉字)
  • 一个字节由8个bit组成, 11111111 每位都是1的话,最大为255

进制知识补充

特殊记忆

  • 0b11 2进制,0b开头
  • 0o11 8进制,0o开头
  • 0x11 16进制,0x开头

进制计算

//二进制计算
111     1*2 + 1*2^1 + 1*2^2

console.log(0b111);//7
//8进制
111     1*8 + 1*8^1 + 1*8^2

console.log(0o111);//73
parseInt
  • parseInt('111',进制)
  • 返回数字
parseInt('111', 2);//将111以2进制解析,结果7
toString
  • 以()包裹,将一个值转为指定进制
  • 返回string
(7).toString(2);//将7转为2进制,结果'111'
encodeURIComponent
  • 转码
  • 中文的话,会转为16进制,一个汉字占3字节,以%分割
//E5 B4 94 分别为一个字节
encodeURIComponent('崔')
'%E5%B4%94'
encodeURIComponent('崔崔')
'%E5%B4%94%E5%B4%94'
encodeURIComponent('1')
//1,f也分别代表一个字节
'1'
encodeURIComponent('f')
'f'
base64
  • 编码
  • 好处:替换链接,不用发请求,快些
  • 缺点:会比原来的大3分之1,所以大文件就不合适了,比如有个3g的文件,转完就是4g
什么是base64
  • base64由64个字符组成,索引以0开始,到63结束
  • 按照索引从字符表中找到对应的内容
  • 一个字节默认为8个bit组成,二进制下8个1最大为255,那么base64就是将前2位变成0,让其最大不超过64
//默认最大: 11111111
//base最大:00111111

//例子:每次先取两个0,然后再拿6个数,可以看到比原来的多出了三分之一
//原始值  11101010 11010100 10101010
//转换后  00111010 00101101 00010010 00101010

方法

toString

  • 一个参数:格式,可省略,默认utf8
  • 将buffer转换为其他格式,如字符

alloc

  • 一个参数:number
  • 创建n个字节的buffer,固定的,不可扩容

from

  • 一个参数:array、string
  • array时:
    • 根据传递的数组,创建对应的buffer
    • 只识别number和字符串的number,(如果是string的字符,会先转number),最大为255
  • string时:
    • 返回转好的buffer
  • 特点:字符采用ascii码,一个字符一个字节,汉字采用utf8,一个汉字三个字节
  • 作用:创建一个固定的buffer
  • 识别不了的就为00
let buf1 = Buffer.from("崔崔崔");
console.log(buf1); //9个字节,每个汉字占3个字节<Buffer e5 b4 94 e5 b4 94 e5 b4 94>

let buf2 = Buffer.from([1, "1", "255", "256", "崔", 123]);
console.log(buf2); //<Buffer 01 01 ff 00 00 7b>  最大255,超出则识别不了,显示00,汉字识别不了,显示00

let buf3 = Buffer.from('1');
console.log(buf3); //<Buffer 31>

let buf4 = Buffer.from(1);
console.log(buf4); //<异常,The "value" argument must not be of type number.

let buf5 = Buffer.from();
console.log(buf5); //异常

copy

  • 实例.copy(拷贝到哪里去,从第几位开始放,从第几位开始区,取几位)
  • 后两个参数可以省略,不写的话默认为起始到结束
  • 不太常用,因为还需要自己计算长度,麻烦,通常使用concat
let buf1 = Buffer.from("小");
let buf2 = Buffer.from("崔");

let buf3 = Buffer.alloc(6);

buf1.copy(buf3, 0, 0, 3);//从buf1的0位开始,取3项,依次放到buf3的0位1位和2位
buf2.copy(buf3, 3, 0, 3);//从buf2的0位开始,取3项,依次放到buf3内3位4位和5位

//可以简写成这样
//buf1.copy(buf3, 0);
//buf2.copy(buf3, 3);
console.log(buf3, buf3.toString()); //<Buffer e5 b0 8f e5 b4 94> '小崔'

concat

  • 2个参数:(array,number?)
  • 按照先后顺序,拼接,然后返回一个新的buffer
  • 第二个参数是截取几个字节,如果不写的话,默认不截取,返回全部的
  • 内部是copy,但用起来更方便
//copy中案例可直接简写成一行
let buf3 = Buffer.concat([buf1,buf2]);//<Buffer e5 b0 8f e5 b4 94>
//
let buf4 = Buffer.concat([buf1,buf2], 2);//<Buffer e5 b0>

slice

  • 截取,等同于数组的slice
  • 不改变原buffer
let buf3 = Buffer.concat([buf1, buf2]);

let buf4 = buf3.slice();
let buf5 = buf3.slice(0, 2);
console.log(buf3, buf4, buf5)

属性

  • length 字节数

流 stream

  • 例1:有一个3G的文件,直接使用readFile读,会占用很大内存,这个时候可以使用流来进行少量多次的读取操作
  • 例2:状态码206代表分片传输,需要截取部分内容,整个读取文件然后截取显然不合适
  • 特点:少量多次,可控,可暂停和恢复,采用发布订阅的方式
  • 分类:读流、写流、双工流、转化流

可读流

  • 一个可读流一定包含data与end
  • 可以链式调用,使用on注册
  • data 将每次的buffer给到callback参数
  • end 读取完毕
  • error 异常
  • open 打开文件,属于文件独有的
  • close 关闭文件,属于文件独有的

createReadStream,ReadStream

  • 创建一个可读流,返回一个实例
  • 参数:(地址,options?)
  • createReadStream内部依赖ReadStream
  • 实例.pause() 暂停
  • 实例.resume() 继续
const fs = require("fs");
const path = require("path");
//效果一样,createReadStream内部依赖的就是ReadStream
//const rs = new fs.ReadStream(path.resolve(__dirname, "1.txt"), {
const rs = fs.createReadStream(path.resolve(__dirname, "1.txt"), {
  flags: "r", //默认为r,可不填
  highWaterMark: 3, //速率,每次拿3个字节,不写默认64k 64*1024
  start: 0, //起始位置,默认为0
  end: 5, //到第4个结束,0~5就是6个,每次拿3个要拿2次,  默认为文件最后一个
});
let arr = [];
rs.on("open", function (fd) {
  console.log("打开", fd);
});
rs.on("close", function () {
  console.log("关闭");
});
rs.on("data", function (chunk) {
  arr.push(chunk);
  console.log(chunk);
});

rs.on("error", function (err) {
  console.log("异常");
});
rs.on("end", function () {
  console.log("结束", Buffer.concat(arr).toString());
});
// rs.pause()  暂停
// rs.resume()  继续

/*
打开 21
<Buffer e5 95 8a>
<Buffer e5 ae 9e>
结束 啊实
关闭
*/


实现ReadStream

const EventEmitter = require("events");
const fs = require("fs");
class ReadStream extends EventEmitter {
  constructor(path, options = {}) {
    //初始化参数
    super();
    this.path = path;
    this.flags = options.flags || "r";
    this.highWaterMark = options.highWaterMark || 64 * 1024;
    this.start = options.start || 0;
    this.end = options.end;
    this.offset = this.start;
    this.flowing = false; //是否为继续读取,暂停开关,默认不读
    //打开文件
    this.open();
    //等待用户监听data,然后才开始发射,要不然没读了没有意义
    this.on("newListener", function (type) {
      if (type === "data" && !this.flowing) {
        //打开开关
        this.flowing = true;
        this.read();
      }
    });
  }
  //销毁
  destory(err) {
    if (err) {
      this.emit("error", err);
    }
    if (typeof this.fd === "number") {
      fs.close(this.fd, () => this.emit("close"));
    }
  }
  //暂停
  pause() {
    this.flowing = false;
  }
  //继续
  resume() {
    if (!this.flowing) {
      this.flowing = true;
      this.read();
    }
  }
  //打开文件
  open() {
    fs.open(this.path, this.flags, (err, fd) => {
      //如果失败,不走了
      if (err) this.destory(err);
      this.fd = fd;
      this.emit("open", fd);
    });
  }
  //读
  read() {
    if (typeof this.fd !== "number") {
      //文件没打开,就打开,然后打开后再执行read
      this.once("open", () => {
        this.read();
      });
      return;
    }
    //包含最后一个,所以+个1
    const howMuchToRead = this.end
      ? Math.min(this.end - this.offset + 1, this.highWaterMark)
      : this.highWaterMark;
    const buf = Buffer.alloc(howMuchToRead);
    fs.read(this.fd, buf, 0, buf.length, this.offset, (err, bytesRead) => {
      this.offset += bytesRead;
      if (bytesRead) {
        this.emit("data", buf.slice(0, bytesRead));
        //如果为true才读
        if (this.flowing) this.read();
      } else {
        //bytesRead为0,这时候就结束了
        //当howMuchToRead为0,容器已经没有空间了,bytesRead必然为0
        this.emit("end");
        this.destory();
      }
    });
  }
}

module.exports = ReadStream;
基于stream实现一个可读流
  • 声明一个ReadStream类,它继承了stream包的Readable类
  • 当每次调用read的时候,会先找Readable的read方法,它内部回去找ReadStream上的_read方法
  • _read执行的时候需要给实例push一个buffer类型数据,当push为null代表结束


const { Readable } = require("stream");

class ReadStream extends Readable {
  constructor() {
    super();
    this.i = 0;
  }
  _read() {
    //这里可以写自己的逻辑,但是按照它的规范来就行,this.push(buffer或null),null表示结束,buffer为继续
    if (this.i === 10) {
      this.push(null);
    } else {
      this.push(this.i++ + "");
    }
  }
}

let mrs = new ReadStream();
mrs.on("data", function (data) {
  console.log(data);
});
mrs.on("end", function () {
  console.log("结束");
});

/*
<Buffer 30>
<Buffer 31>
<Buffer 32>
<Buffer 33>
<Buffer 34>
<Buffer 35>
<Buffer 36>
<Buffer 37>
<Buffer 38>
<Buffer 39>
结束
*/

可写流

  • 一个可写流一定包含write与end方法
  • write是写,会将第一个参数异步写入,因为是异步,所以有回调,但回调可不传
    • write(内容,encoding?,function?)
    • 多个write按照先后顺序执行
    • write会返回布尔值
      • 判断依据:它会在存之前将这几次操作占用的内存与highWaterMark进行比对,比highWaterMark大或相等的话,返回false, 否则就是true,返回值起到一个警示作用,但不会阻碍写入
      • write的返回值是同步的,但实际执行是异步的
      • 它不是马上去写,而是先存到缓存区,然后批量执行,但第一次会先去写,然后调用clearBuffer来清除缓存区
      • 例子:假设highWaterMark为2,第一次存1,占用1字节,小于,那么返回true,第二次再存个2,占用1字节,上次也占了一字节,那就是2字节,大于等于了,这时候就返回false
  • end相当于write+end,也是异步操作,会在最后一次写入后,执行关闭
    • end(内容?,回调?)
    • 将内容写入后,执行回调,close事件会子啊end后执行
    • end后不可再执行write

createWriteStream,WriteStream

  • fs提供的可读流api
  • 它的option.highWaterMark与可读流的不同,在这里只起到警示作用,与write的返回值相关,默认16k
  • drain事件,会在为write的返回值为false后执行,执行顺序在write的回调前面,如果不满足预期,不会执行(不同node版本之间,执行时机也不同)
  • createWriteStream内部就是new了一个WriteStream
const fs = require("fs");
const path = require("path");
const ws = fs.createWriteStream(path.resolve(__dirname, "1.txt"), {
  flags: "w", //默认可不填
  highWaterMark: 1, //这个字段在这里表示的不是一次读多少,而是要不要继续读,默认16k
});
ws.on("open", function (fd) {
  console.log("打开文件", fd);
});
let f = ws.write("1", function (err, data) {
  console.log("1写完了");
});

console.log(f);
setTimeout(() => {
  ws.write("22", function (err, data) {
    console.log("2写完了");
  });
}, 1000);
ws.on("drain", function () {
  console.log("draindraindraindrain");
});

setTimeout(() => {
  ws.end("结束", function (err, data) {
    console.log("最后写个结束,然后关闭文件了");
  }); //结束
}, 2000);

ws.on("close", function () {
  console.log("关闭文件");
});

/*
false
打开文件 24
draindraindraindrain
1写完了
draindraindraindrain
2写完了
最后写个结束,然后关闭文件了
关闭文件

1.txt内容:122结束
*/

转化流

  • 我们读文件后,有时会想在写之前进行操作,比如压缩、加密等,可以统称为转化
  • 特点:输入和输出有关联

pipe

  • 集成读写流,可实现分段读写,异步执行
  • 适用于大文件读写
  • 可读可写
//使用方法
const fs = require("fs");
const path = require("path");
const rs = fs.createReadStream(path.resolve(__dirname, "1.txt"), {
  highWaterMark: 4,
});
const ws = fs.createWriteStream(path.resolve(__dirname, "2.txt"), {
  highWaterMark: 4,
});
// 不使用pipe
// rs.on("data", function (data) {
//   console.log(data);
//   let flag = ws.write(data);
//   if (!flag) rs.pause(); //暂停下
// });
// ws.on("drain", function () {
//   //满足预期后,执行这里
//   rs.resume(); //继续执行
// });
// //读完了,关闭ws
// rs.on("end", function () {
//   ws.end();
// });
rs.pipe(ws);
  • 在转换前加密下
//转化:加密、压缩等
const crypto = require("crypto");

//把hello进行MD5加密,然后转成base64
// const r1 = crypto.createHash("md5").update("hello").digest("base64");

// console.log(r1);

const { Transform } = require("stream");
class MyTransform extends Transform {
  _transform(chunk, encoding, clearBuffer) {
    //集成可读流与可写流
    let r = crypto.createHash("md5").update(chunk).digest("base64");
    this.push(r);
    clearBuffer();
  }
}
const myTransform = new MyTransform();

process.stdin.pipe(myTransform).pipe(process.stdout);

双工流

  • 读和写没直接关系,继承了读和写
const { Duplex } = require("stream");
class MyDuplex extends Duplex {
  _read() {
    console.log("_read");
  }
  _write() {
    console.log("_write");
  }
}

let myDuplex = new MyDuplex();

myDuplex.on("data", (chunk) => {
    console.log('读');
});//事件监听,默认走一下_read

myDuplex.write('ok');//调write走_write