深入node.js 3 模板引擎原理 事件 文件操作 可读流的实现原理

模板引擎的实现

模板引擎是基于new Function + with 实现的。
ejs使用
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
实现:
思路:借助fs的readFile先读取文件内容,然后使用正则表达式替换掉即可。
在这里插入图片描述
在这里插入图片描述
打印的结果是一样的。
复杂的情况呢?拼接字符串,拼成想要的代码
在这里插入图片描述
在这里插入图片描述
主要难点就是在字符串的拼接,第二部分,将全文分为三部分,然后拼接对应的,如

let str = ""; 
with(obj){
str+= `<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    `
arr.forEach(item=>{
str+=`
        <li>
            ${item}
        </li>
        `
})
str+=`
</body>

</html>`}
 return str

这就是拼出来的字符串,然后再new Function,包裹一层函数,将with的obj传入,返回str。
在这里插入图片描述

大概长这样。
效果就是:
在这里插入图片描述
所以本质就是将获取到的内容,使用正则表达式匹配,拼接字符串成我们想要的内容,用with包裹,改变内部作用域,再通过new Function将str包装成一个函数,传入对应的值给obj。然后运行之后str就能正常通过作用域获取值赋值。

buffer

在服务端,需要一个东西来标识内存,但不能是字符串,因为字符串无法标识图片。node中使用buffer来标识内存的数据。他把内存转换成了16进制来显示(16进制比较短)buffer每个字节的取值范围就是0-0xff(十进制的255).
node中buffer可以和字符串任意的转换(可能出现乱码)
编码规范:ASCII -> GB8030/GBK -> unicode -> UTF8
Buffer代表的是内存,内存是一段固定空间,产生的内存是固定大小,不能随意增加。
扩容:需要动态创建一个新的内存,把内容迁移过去。
在这里插入图片描述
在这里插入图片描述
创建一个长度为5的buffer,有点像数组,但是数组可以扩展,而buffer不可以扩展。
还有一种声明buffer。在这里插入图片描述

Buffer.form。
一般使用alloc来声明一个buffer,或者把字符串转换成Buffer使用。文件操作也是采用Buffer形式。

buffer使用

无论是二进制还是16进制,表现得东西是一样的。

base64编码:

base64可以放在任何路劲的链接里,可以减少请求次数。但是文件大小会变大。比如webpack中的asset/type,把一些小的文件转换成了Base64编码内嵌到了文件当中,虽然可以减少请求次数,但也增大了文件的大小。
在这里插入图片描述
base64的来源就是将每个字节都转化为小于64的值。没有加密功能,因为规则很简单。如
在这里插入图片描述

  • 第一步:将buffer中每个位置的值转为二进制。如上。
    一个字节有八位,八位的最大值是255,有可能超过64。而base64编码是要将每个字节转化为小于64的值。所以就取每个字节的6位。6位的最大值就是2*6 - 1 = 63。也就是:
    在这里插入图片描述
  • 第二步:将38的形式转成64的,保证每个字节小于64。将其转为十进制。
    在这里插入图片描述
    在这里插入图片描述
  • 第三步,通过特定的编码规则转换即完成。
    在这里插入图片描述
    将我们获取到的十进制传入,因为每个字节都是小于64的,所以不超过。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    完成。

buffer的常用方法

除了form和alloc还有

slice
// slice
const a = Buffer.from([1,2,3,4,5])
const d = a.slice(0,2)
d[1] = 4
console.log(d);
console.log(a); 	

在这里插入图片描述
与数组的用法相同,但是他并不是浅复制,而是直接关联在一起。改变d也会改变a。而数组的slice是浅复制。改变原始数据的值不会改变。

copy

将Buffer的数据拷贝到另一个数据上。

const a = Buffer.from([1, 2, 3, 4, 5]);
const d = Buffer.alloc(5);
a.copy(d, 1, 2, 3); //四个参数,拷贝到哪里?从d的第一个开始拷贝 a的a[2]->a[3]
console.log(d);

copy四个参数,分别是拷贝的目标d。从d的第几个长度开始。拷贝a的第2到第3位。
所以应该是 <Buffer 00 03 00 00 00 >
在这里插入图片描述

concat

用于拼接buffer
在这里插入图片描述
在这里插入图片描述
Buffer.concat(arr, index)
第二个参数是拼接出来的Buffer的长度,如果大于原本的长度,用00填写。

Buffer.myConcat = function (
  bufferList,
  length = bufferList.reduce((a, b) => a + b.length, 0)
) {
  let bigBuffer = Buffer.alloc(length);
  let offset = 0;
  bufferList.forEach((item) => {
    //使用copy每次拷贝一份然后offset向下走。
    item.copy(bigBuffer, offset);
    offset += item.length;
  });
  return bigBuffer
};

借助copy,逐个拷贝一份即可。
在这里插入图片描述
在这里插入图片描述

文件操作

fs模块有两种基本api,同步,异步。
io操作,input,output,输入输出。
在这里插入图片描述在这里插入图片描述
读取时默认是buffer类型。
写入的时候,默认会将内容以utf8格式写入,如果文件不存在则创建。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
读取的data是Buffer类型,写入的是utf8格式。
这种读写适合小文件

读取文件某段内容的办法

在这里插入图片描述
fs.open用于打开一个文件。fs.read用来读取内容并且写入到buffer中。
在这里插入图片描述
fs.write用于将内容写入某个文件之中。如上,打开了b.txt,然后用fs.wirte。五个参数,分别是fd,buffer,从buffer的第0个位置,到buffer的第0个位置,从b.txt的第0位开始写,回调函数。

在这里插入图片描述
在这里插入图片描述
写入成功。
这种写法也不太美观,每次都需要fs.open然后fs.read或者fs.wirte,容易造成回调地狱。

流 Stream的出现

源码的实现步骤:
fs的CreateReadStrem是new了一个ReadStream,他是基于Stream的Readable类的,然后自己实现了_read方法。供Stream.prototype.read调用。

  • 1 内部会先open文件,然后直接直接继续读取操作,默认是调用了pause暂停。
  • 2 监听用户是否绑定了data事件,resume,是的话开始读取事件
  • 3 调用fs.read 将数据读取出来。
  • 4 调用this.push去emit data事件,然后判断是否可以读取更多再去读取。
    第一种: fs.readFile(需要将文件读取到磁盘中,占用内容)=>fs.wirteFile
    第二种: fs.open => fs.read => fs.write 回调地狱。
    实现读取三个字节写入三个字节。采用fs.open fs.read fs.write的方法。
    实现copy方法。
    在这里插入图片描述

看实现:在这里插入图片描述
首先创建一个三字节的Buffer。
然后使用fs.open打开要读取和要写入的文件。
在这里插入图片描述
因为我们是每三个每三个读取,所以需要采用递归方式,一直读取文件。在这里插入图片描述
直到读取完毕,调用回调函数。fs.read和fs.write的参数都是类似的,即fd,buffer,buffer的start,buffer的end,读取文件/写入文件的start、回调函数(err,真正读取到的个数/真正写入的个数)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
现在基本实现了读一部分,写一部分,但是读写的逻辑写在一起了,需要把他拆开。

流 Stream模块

可读流

不是一下子把文件都读取完毕,而是可以控制读取的个数和读取的速率。
流的概念跟fs没有关系,fs基于stream模块底层扩展了一个文件读写方法。
在这里插入图片描述
所以fs.open,fs.read等需要的参数,createReadStream也需要、
在这里插入图片描述
返回一个对象,获取需要监听data事件。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
close事件在end事件之后触发。
由此可以看出:流的内部基于 fs.open fs.close fs.read fs.write以及事件机制。
在这里插入图片描述
暂停是不再触发data事件
在这里插入图片描述
rs.resume()是恢复。
在这里插入图片描述
在这里插入图片描述

实现readStream

从vscode调试源码得知在这里插入图片描述
实现思路:

  • createReadStream内部new了一个ReadStream的实例,ReadStream是来自于Stream模块。
  • 做一系列参数默认后, 调用this.open方法,这个方法会去调用fs.open去打开文件,打开之后触发事件,从回调的形式发布订阅模式,然后监听事件,当发现有用户注册了data事件之后,调用fs.read,j监听open事件,在open之后再去读取文件等等。
  • 这样我们的读写逻辑就分离开了,从回调的形式变成了发布订阅模式,有利于解耦。
    第一步:
    在这里插入图片描述
    第一步:参数初始化,并调用fs.open
    在这里插入图片描述
    open打开之后会触发open事件,注意,这里是异步的
    第二步: 监听用户注册的data事件,当用户注册了data事件才去调用fs.read。调用this.read的时候open还没完成。
    在这里插入图片描述

在这里插入图片描述
所以第一次read的时候需要判断,然后只监听一次open事件,重复打开read事件。
在这里插入图片描述
这个end和start配合,表示读取文件从哪到哪的位置,但是end是包后的,比如上面的end为4,实际上读取到的是5。
在这里插入图片描述
创建buffer存放读取的内容,再判断应该读取多少内容,以哪个小为准。
在这里插入图片描述
然后打开fs.read。将读取到的buffer发布出去。再次调用this.read去继续读取。在这里插入图片描述start=1,end=4,读取到2345的内容,正确。
在这里插入图片描述
不给end,每次3个3个的读取。
在这里插入图片描述
接着实现暂停,需要一个开关。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这样就基本完成了。
在这里插入图片描述
在这里插入图片描述

总结:

一开始实现的的copy方法,也是利用fs.open, fs.read, fs.write等,通过回调的形式完成的,这样虽能完成,但是内聚度较高,容易形成回调地狱。
而基于fs模块,和events模块,实现的可读流,可以有效的解耦刚才的代码,通过发布订阅模式,在一开始订阅事件,在每个时间点对应发布事件,然后代码执行,各司其职。
open和close是文件流独有的,
可读流具备:on (‘data’ | ‘end’ | ‘error’), resume, pause这些方法。

相关代码:
// copy
const fs = require("fs");
const path = require("path");

// let buf = Buffer.alloc(3);
// //open打开一个文件,第一个参数是路劲,第二个参数是打开文件用途,第三个是回调函数。
// fs.open(path.resolve(__dirname, "a.txt"), "r", function (err, fd) {
//   // fd 是file descriptor文件描述
//   console.log("fd", fd);
//   //读取a.txt的内容,并且将内容写入buf中的第0个位置到3第三个位置,从a.txt的第六个位置开始
//   fs.read(fd, buf, 0, 3, 3, function (err, data) {
//     fs.open(path.resolve(__dirname, "./b.txt"), "w", function (err, fd2) {
//       fs.write(fd2, buf, 0, 3, 0, function (err, data2) {
//         console.log("buf", buf);
//         console.log("data2", data2);
//       });
//     });
//   });
// });

function copy(source, target, cb) {
  const BUFFER_SIZE = 3;
  const buffer = Buffer.alloc(BUFFER_SIZE);
  //每次读入文件的位置
  let r_offset = 0;
  //每次写入新文件的位置
  let w_offset = 0;

  //读取一部分数据,写入一部分数据

  //第三个参数可以是权限  权限有三个组合 rwx 可读可写可执行 r的权限是4,w的权限是2,x的权限是1  421 = 777 可写可读可执行
  // 0o666表示最大的权限,默认不用写。

  //读取的文件必须要存在。写入文件不存在会创建,如果文件有内容会清空。
  fs.open(source, "r", function (err, fd1) {
    //打开写的文件
    fs.open(target, "w", function (err, fd2) {
      //每次读取三个写入三个,回调的方式实现的功能,需要用递归
      // 同步代码则可以采用while循环

      function next() {
        fs.read(
          fd1,
          buffer,
          0,
          BUFFER_SIZE,
          r_offset,
          function (err, bytesRed) {
            // bytesRed真正读取到的个数
            if (err) {
              cb("读取失败");
              return;
            }
            if (bytesRed) {
              //将读取到的内容写入target目标
              fs.write(
                fd2,
                buffer,
                0,
                bytesRed,
                w_offset,
                function (err, written) {
                  if (err) retrun;
                  // written 真正写入的个数
                  r_offset += bytesRed; //每次写入之后,下一次读取的内容就该往前进
                  w_offset += written;
                  next();
                }
              );
            } else {
              //读取内容为空,结束
              fs.close(fd1, () => {});
              fs.close(fd2, () => {});
              cb();
            }
          }
        );
      }
      next();
    });
  });
}

copy("./a.txt", "b.txt", (err, data) => {
  console.log("copy success");
});

createStream的实现

const EventMitter = require("events");
const fs = require("fs");
class ReadStream extends EventMitter {
  constructor(path, options = {}) {
    super();
    this.path = path;
    //操作
    this.flags = options.flags || "r";
    this.encoding = options.encoding || null;
    this.autoClose = options.autoClose || true;
    this.start = options.start || 0;
    this.end = options.end || Infinity; //读取的个数,包后的,如果是1 ,就可能读取到0,1,2
    this.highWaterMark = options.highWaterMark || 64 * 1024;
    this.emitClose = options.emitClose || false;
    this.offset = this.start; // 每次读取文件的位移数

    this.flowing = true; //暂停继续开关

    this.open(); // 文件操作,注意这个方法是异步的。

    // events可以监听newListener,可以获取注册的所有事件
    this.on("newListener", function (type) {
      if (type === "data") {
        //用户订阅了data事件,才去读取。
        this.read(); //这时候文件还没open,fd为undefined。
      }
    });
  }

  pause() {
    //暂停
    this.flowing = false;
  }

  resume() {
    //继续
    if (!this.flowing) {
      this.flowing = true;
      this.read();
    }
  }

  destory(err) {
    if (err) {
      this.emit("error", err);
    }
    if (this.autoClose) {
      fs.close(this.fd, () => {
        this.emit("close");
      });
    }
  }

  read() {
    //希望在open之后打开
    if (typeof this.fd !== "number") {
      this.once("open", (fd) => {
        //之前实现的copy这段逻辑是写在了fs.open里面,换成发布订阅模式之后就可以分离出来。
        this.read(); //第二次read的时候,fd有值了
      });
    } else {
      //判断每次读取多少。因为this.end是包后的,比如start = 0, end = 1, 那么读取的就是 0 , 1, 2所以需要+1
      const howMutchToRead = Math.min(
        this.end - this.offset + 1,
        this.highWaterMark
      );
      const buffer = Buffer.alloc(howMutchToRead);
      fs.read(
        this.fd,
        buffer,
        0,
        howMutchToRead,
        this.offset,
        (err, byteRead) => {
          if (err) {
            this.destory(err);
          } else {
            if (byteRead) {
              //读取到文件,发布data事件,发送真正读取到的内容
              this.offset += byteRead;
              this.emit("data", buffer.slice(0, byteRead));
              this.flowing && this.read();
            } else {
              this.emit("end");
              if (this.autoClose) {
                this.destory();
              }
            }
          }
        }
      );
    }
  }

  open() {
    fs.open(this.path, this.flags, (err, fd) => {
      if (err) {
        //报错:
        this.destory(err);
      }
      //从回调的形式变成了发布订阅模式
      this.fd = fd;
      this.emit("open", fd);
    });
  }
}

const rs = new ReadStream("./a.txt", {
  flags: "r", //创建可读流。
  encoding: null, //默认Buffer
  autoClose: true, //自动关闭,相当于读取完毕调用fs.close
  emitClose: true, //触发close事件
  start: 0, //从文件哪里开始读取
  highWaterMark: 3, //每次读取的数据个数,默认是64*1025字节。
  //end: 4, // 比如这个就会读取 1到5的内容
});

rs.on("open", () => {
  console.log("文件打开了");
});

// rs.on("data", (data) => {
//   console.log("监听Data事件", data);
// });

// //底层还是 fs.open fs.read fs.close
// const rs = fs.createReadStream("./a.txt", {
//   flags: "r", //创建可读流。
//   encoding: null, //默认Buffer
//   autoClose: true, //自动关闭,相当于读取完毕调用fs.close
//   emitClose: true, //触发close事件
//   start: 0, //从文件哪里开始读取
//   highWaterMark: 3, //每次读取的数据个数,默认是64*1025字节。
// }); //返回一个对象

//没监听前,是非流动模式,监听后,是流动模式。

//监听data事件,并且不停触发
rs.on("data", function (chunk) {
  console.log(chunk);
  //暂停
  rs.pause();
});

rs.on("end", function () {
  console.log("读取完毕");
});

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

setInterval(() => {
  console.log("一秒后");
  rs.resume();
}, 1000);

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

coderlin_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值