Node 常见模块

92 阅读12分钟

一、Node概览

本系列node环境为 16.15.1

1.1 简介

  • 官方对Node.js的定义:

    • Node.js是一个基于V8 JavaScript引擎的JavaScript运行时环境
  • 也就是说Node.js基于V8引擎来执行JavaScript的代码,但是不仅仅只有V8引擎:

    • V8可以嵌入到任何C ++应用程序中,无论是Chrome还是Node.js,事实上都是嵌入了V8引擎来执行JavaScript代码

    • 但是在Chrome浏览器中,还需要解析、渲染HTML、CSS等相关渲染引擎,另外还需要提供支持浏览器操作的API、浏览器自己的事件循环等

    • 另外,在Node.js中我们也需要进行一些额外的操作,比如文件系统读/写、网络IO、加密、压缩解压文件等操作;

1.2 Node的架构设计

  • 编写的JavaScript代码会经过V8引擎,再通过Node.js的Bindings,将任务放到Libuv的事件循环中

  • libuv(Unicorn Velociraptor—独角伶盗龙)是使用C语言编写的库

  • libuv 提供了事件循环、文件系统读写、网络IO、线程池等等内容

    image.png

二、fs模块的使用

2.1 简介

  • fs是File System的缩写,表示文件系统

  • 对于任何一个为服务器端服务的语言或者框架通常都会有自己的文件系统:

    • 因为服务器需要将各种数据、文件等放置到不同的地方

    • 比如用户数据可能大多数是放到数据库中的

    • 比如某些配置文件或者用户资源(图片、音视频)都是以文件的形式存在于操作系统上的

  • Node也有自己的文件系统操作模块,就是fs:

    • 借助于Node帮我们封装的文件系统,可以在任何的操作系统(window、Mac OS、Linux)上面直接去操作文件
    • 这也是Node可以开发服务器的一大原因,也是它可以成为前端自动化脚本等热门工具的原因
  • fs的API介绍

    • API文档

    • API大多数都提供三种操作方式:

      • 方式一:同步操作文件:代码会被阻塞,不会继续执行

      • 方式二:异步回调函数操作文件:代码不会被阻塞,需要传入回调函数,当获取到结果时,回调函数被执行

      • 方式三:异步Promise操作文件:代码不会被阻塞,通过 fs.promises 调用方法操作,会返回一个Promise,可以通过then、catch进行处理

2.1 fs读取文件

  • aaa.txt

    Hello World
    
  • 读取文件

    const fs = require('fs')
    
    // 1.同步读取
    const res1 = fs.readFileSync('./aaa.txt', {
      encoding: 'utf-8'
    })
    
    console.log(res1)
    console.log('后续的代码~')
    
    // 2.异步读取: 回调函数
    fs.readFile('./aaa.txt', {
      encoding: 'utf-8'
    }, (err, data) => {
      if(err) {
        console.log('读取文件错误:', err)
        return
      }
      console.log('读取文件内容:', data)
    })
    
    console.log('后续的代码~')
    
    // 3.异步读取: promise
    fs.promises.readFile('./aaa.txt', {
      encoding: 'utf-8'
    }).then(res => {
      console.log('获取到结果:', res)
    }).catch(err => {
      console.log('发生了错误:', err);
    })
    

2.2 fs文件描述符

  • 文件描述符(File descriptors)是什么呢?

    • 在常见的操作系统上,对于每个进程,内核都维护着一张当前打开着的文件和资源的表格

    • 每个打开的文件都分配了一个称为文件描述符的简单的数字标识符

    • 在系统层,所有文件系统操作都使用这些文件描述符来标识和跟踪每个特定的文件

    • Windows 系统使用了一个虽然不同但概念上类似的机制来跟踪资源

  • 为了简化用户的工作,Node.js 抽象出操作系统之间的特定差异,并为所有打开的文件分配一个数字型的文件描述符

  • 文件描述符

    • fs.open() 方法用于分配新的文件描述符。

    • 一旦被分配,则文件描述符可用于从文件读取数据、向文件写入数据、或请求关于文件的信息

    const fs = require('fs')
    
    // 打开一个文件
    fs.open('./bbb.txt', (err, fd) => {
      if (err) {
        console.log("打开文件错误:", err)
        return
      }
    
      // 1.获取文件描述符
      console.log(fd) // 3
    
      // 2.读取到文件的信息
      fs.fstat(fd, (err, stats) => {
        if (err) return
        console.log(stats)
    
        // 3.手动关闭文件
        fs.close(fd)
      })
    })
    

2.2 fs写入文件

  • fs.writeFile(file, data[, options], callback):在文件中写入内容

    const fs = require('fs')
    
    // 1.有一段内容(客户端传递过来http/express/koa)
    const content = "hello world, I am a txt"
    
    // 2.文件的写入操作
    fs.writeFile('./ccc.txt', content, {
      encoding: 'utf8',
      flag: 'a'
    }, (err) => {
      if (err) {
        console.log("文件写入错误:", err)
      } else {
        console.log("文件写入成功")
      }
    })
    
  • option参数:

    • flag:写入的方式
    • encoding:字符的编码
  • flag 选项

    • w 打开文件写入,默认值
    • w+打开文件进行读写(可读可写),如果不存在则创建文件
    • r打开文件读取,读取时的默认值
    • r+ 打开文件进行读写,如果不存在那么抛出异常
    • a打开要写入的文件,将流放在文件末尾。如果不存在则创建文件
    • a+打开文件以进行读写(可读可写),将流放在文件末尾。如果不存在则创建文件

2.3 fs文件夹操作

  • 创建文件夹

    const fs = require('fs')
    
    // 创建文件夹 directory
    fs.mkdir('./test', (err) => {
      console.log(err)
    })
    
  • 文件夹内容读取

    const fs = require("fs")
    
    // 读取文件夹
    // 1.读取文件夹, 获取到文件夹中文件的字符串
    // fs.readdir('./test', (err, files) => {
    //   console.log(files) // 以字符串的形式返回结果:['aaa', 'bbb', 'nba.txt']
    // })
    
    // 2.读取文件夹, 获取到文件夹中文件的信息
    // fs.readdir('./test', { withFileTypes: true }, (err, files) => {
    //   files.forEach(item => {
    //     if (item.isDirectory()) {
    //       console.log("item是一个文件夹:", item.name)
    
    //       fs.readdir(`./test/${item.name}`, { withFileTypes: true }, (err, files) => {
    //         console.log(files)
    //       })
    //     } else {
    //       console.log("item是一个文件:", item.name)
    //     }
    //   })
    // })
    
    // 3.递归的读取文件夹中所有的文件
    function readDirectory(path) {
      fs.readdir(path, { withFileTypes: true }, (err, files) => {
        files.forEach(item => {
          if (item.isDirectory()) {
            readDirectory(`${path}/${item.name}`)
          } else {
            console.log("获取到文件:", item.name)
          }
        })
      })
    }
    readDirectory('./test')
    
    // 获取到文件: nba.txt
    // 获取到文件: abc.txt
    // 获取到文件: cba.txt
    
  • 文件重命名

    const fs = require('fs')
    
    // 1.对文件夹进行重命名
    // fs.rename('./test', './dist', (err) => {
    //   console.log("重命名结果:", err) // null
    // })
    
    // 2.对文件重命名
    fs.rename('./ccc.txt', './ddd.txt', (err) => {
      console.log("重命名结果:", err)
    })
    

三、events模块

3.1 events基本使用

  • Node中的核心API都是基于异步事件驱动的:

    • 在这个体系中,某些对象(发射器(Emitters))发出某一个事件

    • 可以监听这个事件(监听器 Listeners),并且传入的回调函数,这个回调函数会在监听到事件时调用

  • 发出事件和监听事件都是通过EventEmitter类来完成的,它们都属于events对象。

    • emitter.on(eventName, listener):监听事件,也可以使用addListener

    • emitter.off(eventName, listener):移除事件监听,也可以使用removeListener

    • emitter.emit(eventName[, ...args]):发出事件,可以携带一些参数

    // events模块中的事件总线
    const EventEmitter = require('events')
    
    // 创建EventEmitter的实例
    const emitter = new EventEmitter()
    
    // 监听事件
    function handleTestFn(name, age, height) {
      console.log('监听test的事件', name, age, height)
    }
    emitter.on('test', handleTestFn)
    
    // 发射事件
    setTimeout(() => {
      emitter.emit('test', "test", 18, 1.88)
    
      // 取消事件监听
      emitter.off('test', handleTestFn)
    
      setTimeout(() => {
        emitter.emit('test') // 这里不能发出事件,上面取消了
      }, 1000)
    }, 2000);
    

3.2 events其他方法

  • eventNames():返回当前 EventEmitter对象注册的事件字符串数组
  • getMaxListeners():返回当前 EventEmitter对象的最大监听器数量,可以通过setMaxListeners()来修改,默认是10
  • listenerCount():返回当前 EventEmitter对象某一个事件名称,监听器的个数
  • listeners():返回当前 EventEmitter对象某个事件监听器上所有的监听器数组
  • once(eventName, listenter):事件监听一次
  • prependListener():将监听事件添加到最前面
  • prependOnceListener():将监听事件添加到最前面,但是只监听一次
  • removeAllListener([eventName]):移除所有的监听器(不传递参数的情况下, 移除所有事件名称的所有事件监听; 传递参数的情况下, 只会移除传递的事件名称的事件监听)

四、Buffer类使用

4.1 二进制的知识

  • 计算机中所有的内容:文字、数字、图片、音频、视频最终都会使用二进制来表示

  • JavaScript可以直接去处理非常直观的数据:比如字符串,通常展示给用户的也是这些内容。

  • 对于服务器:

    • 服务器要处理的本地文件类型相对较多

    • 比如某一个保存文本的文件并不是使用 utf-8进行编码的,而是用 GBK,那么我们必须读取到他们的二进制数据,再通过GKB转换成对应的文字

    • 比如我们需要读取的是一张图片数据(二进制),再通过某些手段对图片数据进行二次的处理(裁剪、格式转换、旋转、添加滤镜),Node中有一个Sharp的库,就是读取图片或者传入图片的Buffer对其再进行处理

    • 比如在Node中通过TCP建立长连接,TCP传输的是字节流,我们需要将数据转成字节再进行传入,并且需要知道传输字节的大小(客户端需要根据大小来判断读取多少内容)

  • Buffer 和 二进制

    • Buffer中存储的是二进制数据,那么到底是如何存储呢?

      • 可以将Buffer看成是一个存储二进制的数组
      • 这个数组中的每一项,可以保存8位二进制0000 0000
    • 为什么是8位呢?

      • 在计算机中,很少的情况会直接操作一位二进制,因为一位二进制存储的数据是非常有限的
      • 所以通常会将8位合在一起作为一个单元,这个单元称之为一个字节(byte)
      • 也就是说 1byte = 8bit1kb=1024byte1M=1024kb
      • 比如很多编程语言中的int类型是4个字节,long类型时8个字节
      • 比如TCP传输的是字节流,在写入和读取时都需要说明字节的个数
      • 比如RGB的值分别都是255,所以本质上在计算机中都是用一个字节存储的

4.2 Buffer和字符串之间转换

  • Buffer相当于是一个字节的数组,数组中的每一项对应一个字节的大小
    // 1.创建Buffer(不推荐)
    // const buf = new Buffer('hello')
    // console.log(buf)
    
    // 2.创建Buffer
    // const buf2 = Buffer.from('world')
    // console.log(buf2)
    
    // 3.创建Buffer(字符串中包含中文) 一个中文字符 三个字节
    // const buf3 = Buffer.from('你好啊hhhhh')
    // console.log(buf3)
    // console.log(buf3.toString())
    
    // 4.手动指定的Buffer创建过程的编码
    // 编码操作
    const buf4 = Buffer.from('哈哈哈', 'utf16le')
    console.log(buf4)
    // 解码操作
    console.log(buf4.toString('utf16le'))
    
    // 注意:编码和解码不同,会出现乱码
    

4.3 Buffer的其他创建方式

  • Buffer.alloc(10)

    // 1.创建一个Buffer对象
    // 8个字节大小的buffer内存空间
    const buf = Buffer.alloc(8)
    // console.log(buf)
    
    // 2.手动对每个字节进行访问
    // console.log(buf[0]) // 0
    // console.log(buf[1]) // 0
    
    // 3.手动对每个字节进行操作
    buf[0] = 100
    buf[1] = 0x66
    console.log(buf)
    console.log(buf.toString()) // df
    
    // m 的 ASCII码
    buf[2] = 'm'.charCodeAt()
    console.log(buf)
    
  • 从文件中读取buffer

    const fs = require('fs')
    
    // 1.从文件中读取buffer
    // fs.readFile('./aaa.txt', { encoding: 'utf-8' }, (err, data) => {
    //   console.log(data)
    // })
    
    // fs.readFile('./aaa.txt', (err, data) => {
    //   console.log(data.toString()) // Hello World
    // })
    
    // fs.readFile('./aaa.txt', (err, data) => {
    //   data[0] = 0x6d
    //   console.log(data.toString()) // mello World
    // })
    
    
    // 2.读取一个图片的二进制(node中有一个库sharp)
    fs.readFile('./kobe02.png', (err, data) => {
      console.log(data) // <Buffer ff d8 ff ... 36217 more bytes>
    })
    

4.4 Buffer源码创建过程

  • 事实上我们创建Buffer时,并不会频繁的向操作系统申请内存,它会默认先申请一个8 * 1024个字节大小的内存,也就是8kb

五、Stream的使用

5.1 Stream的概念理解

  • 什么是Stream(小溪、小河,在编程中通常翻译为流)呢?

    • 第一反应应该是流水,源源不断的流动
    • 程序中的流也是类似的含义,可以想象从一个文件中读取数据时,文件的二进制(字节)数据会源源不断的被读取到程序中
    • 而这个一连串的字节,就是我们程序中的流
  • 对流的理解:

    • 是连续字节的一种表现形式和抽象概念
    • 流应该是可读的,也是可写的
  • 在上面的内容,可以直接通过 readFile 或者 writeFile 方式读写文件,为什么还需要流呢?

    • 直接读写文件的方式,虽然简单,但是无法控制一些细节的操作
    • 比如从什么位置开始读、读到什么位置、一次性读取多少个字节
    • 读到某个位置后,暂停读取,某个时刻恢复继续读取等等
    • 或者这个文件非常大,比如一个视频文件,一次性全部读取并不合适
  • 文件读写的Stream

    • 事实上Node中很多对象是基于流实现的:

      • http模块的Request和Response对象
    • 官方文档:所有的流都是EventEmitter的实例

5.2 可读流的使用Readable

  • createReadStream 的几个参数:

    • start:文件读取开始的位置
    • end:文件读取结束的位置
    • highWaterMark:一次性读取字节的长度,默认是64kb
  • 基本使用

    const fs = require('fs')
    
    // 1.一次性读取
    // 缺点一: 没有办法精准控制从哪里读取, 读取什么位置.
    // 缺点二: 读取到某一个位置的, 暂停读取, 恢复读取.
    // 缺点三: 文件非常大的时候, 多次读取.
    // fs.readFile('./aaa.txt', (err, data) => {
    //   console.log(data)
    // })
    
    // 2.通过流读取文件
    // 2.1. 创建一个可读流
    // start: 从什么位置开始读取
    // end: 读取到什么位置后结束(包括end位置字节)
    const readStream = fs.createReadStream('./aaa.txt', {
     start: 8,
     end: 22,
     highWaterMark: 3
    })
    
    readStream.on('data', (data) => {
      console.log(data.toString())
    
      // 暂停读取
      readStream.pause()
    
      setTimeout(() => {
        readStream.resume()
      }, 2000)
    })
    
  • 其他事件

    const fs = require('fs')
    
    // 1.通过流读取文件
    const readStream = fs.createReadStream('./aaa.txt', {
     start: 8,
     end: 22,
     highWaterMark: 3
    })
    
    // 2.监听读取到的数据
    readStream.on('data', (data) => {
      console.log(data.toString())
    })
    
    // 3.补充其他的事件监听
    readStream.on('open', (fd) => {
      console.log('通过流将文件打开~', fd) // 通过流将文件打开~ 3
    })
    
    readStream.on('end', () => {
      console.log('已经读取到end位置')
    })
    
    readStream.on('close', () => {
      console.log('文件读取结束, 并且被关闭')
    })
    

5.3 可写流的使用Writable

  • 不能监听到 close 事件:

    • 因为写入流在打开后是不会自动关闭的
    • 必须手动关闭,来告诉Node已经写入结束了
    • 并且会发出一个 finish 事件的
  • 基本使用

    const fs = require('fs')
    
    // 1.一次性写入内容
    // fs.writeFile('./bbb.txt', 'hello world', {
    //   encoding: 'utf-8',
    //   flag: 'a+'
    // }, (err) => {
    //   console.log('写入文件结果:', err)
    // })
    
    // 2.创建一个写入流
    const writeStream = fs.createWriteStream('./ccc.txt', {
      flags: 'a'
    })
    
    writeStream.on('open', (fd) => {
      console.log('文件被打开', fd)
    })
    
    writeStream.write('test txt')
    writeStream.write('aaaa')
    writeStream.write('bbbb', (err) => {
      console.log("写入完成:", err) // undefined
    })
    
    writeStream.on('finish', () => {
      console.log('写入完成了')
    })
    
    writeStream.on('close', () => {
      console.log('文件被关闭~')
    })
    
    // 3.写入完成时, 需要手动去掉用close方法
    // writeStream.close()
    
    // 4.end方法: 
    // 操作一: 将最后的内容写入到文件中, 并且关闭文件
    // 操作二: 关闭文件
    writeStream.end('哈哈哈哈')
    
  • start 的属性

    const fs = require('fs')
    
    const writeStream = fs.createWriteStream('./ddd.txt', {
      // mac上面是没有问题
      // flags: 'a+',
      // window上面是需要使用r+
      flags: 'r+',
      start: 5
    })
    
    writeStream.write('my name is lilei')
    writeStream.close()
    

5.4 pipe可读可写流连接一起

  • 文件的拷贝流

    const fs = require('fs')
    
    // 1.方式一: 一次性读取和写入文件
    // fs.readFile('./foo.txt', (err, data) => {
    //   console.log(data)
    //   fs.writeFile('./foo_copy01.txt', data, (err) => {
    //     console.log('写入文件完成', err) // null
    //   })
    // })
    
    // 2.方式二: 创建可读流和可写流
    // const readStream = fs.createReadStream('./foo.txt')
    // const writeStream = fs.createWriteStream('./foo_copy02.txt')
    
    // readStream.on('data', (data) => {
    //   writeStream.write(data)
    // })
    
    // readStream.on('end', () => [
    //   writeStream.close()
    // ])
    
    // 3.在可读流和可写流之间建立一个管道
    const readStream = fs.createReadStream('./foo.txt')
    const writeStream = fs.createWriteStream('./foo_copy03.txt')
    
    readStream.pipe(writeStream)