Nodejs又一次学习

nodejs可以做什么?

  • 轻量级、高性能的 Web 服务

  • 前后端 JavaScript 同构开发

  • 便捷高效的前端工程化

nodejs架构

Natives modules

  • 当前层内容由 JS 实现

  • 提供应用程序可直接调用库,例如fs、path、http等

  • JS 语言无法直接操作底层硬件设置,所以还需要下层c/c++进行配合。

底层

  • V8:执行 JS 代码,提供桥梁接口(操作js执行底层操作)。

  • Libuv:事件循环、事件队列、异步IO。

  • 第三方模块:zlib 、http、c-ares等。

对于传统的高级语言,在实现IO操作的时候,都是采用并发处理,但是这样的话,如果同一时间事情过多,我们不可能无限的开启线程处理,所以就可能造成阻塞,这就体现了Nodejs异步非阻塞IO的优势了。通过一个线程来实现多个线程做的事情。所以对于IO密集型的高并发请求Nodejs有着天然的优势。但是对于cpu密集型的处理就有着天然的劣势。

Nodejs单线程配合事件驱动架构及libuv实现异步IO。

事件驱动架构

单线程如何实现高并发?

  • Nodejs主线程是单线程的,v8执行Nodejs代码,v8只有一个主线程执行js代码。但是libuv库存在一个线程池,来帮助我们异步执行IO操作。

  • 异步非阻塞IO配合事件回调通知。

Nodejs应用场景

nodejs作为中间层。

实时聊天应用程序。

操作数据库提供API服务。

总之Nodejs适用于IO密集型任务。

全局对象 / 变量

在nodejs中,global是全局变量的寄主。

全局中的this和global不一样,全局this指向{}

js 复制代码
// 在模块化调用中,this被赋值为{}
console.log(this == global); // false


(function () {
  console.log(this == global) // true
})()

Nodejs 常见全局变量

常用变量

  • _filename:返回正在执行脚本文件的绝对路径

  • _dirname:返回正在执行脚本所在目录

  • timer类函数:执行顺序与事件循环间的关系

  • process:提供与当前进程互动的接口

js 复制代码
// 1 资源: cpu 内存
console.log(process.memoryUsage())
// {
//   rss: 19881984, // 常驻内存
//   heapTotal: 4481024, // 申请的内存
//   heapUsed: 2710648, // 使用的内存
//   external: 893780, // 底层c、c++模块占用内存
//   arrayBuffers: 9898 // 独立空间大小,缓存区大小
// }
// console.log(process.cpuUsage()) // cpu占用时间片段

// 2 运行环境:运行目录、node环境、cpu架构、用户环境、系统平台
/* console.log(process.cwd())
console.log(process.version)
console.log(process.versions) // 相关库的版本
console.log(process.arch) // x64
console.log(process.env.NODE_ENV)
console.log(process.env.PATH) // 系统环境变量
console.log(process.env.USERPROFILE)  // 用户目录
console.log(process.platform) */

// 3 运行状态: 启动参数、PID、运行时间
/* console.log(process.argv)
console.log(process.argv0)  // execArgv
console.log(process.pid) */  // ppid 

setTimeout(() => {
  console.log(process.uptime())
}, 3000)

事件操作

js 复制代码
// 事件操作
process.on("exit", function() {
  console.log("exit...")
})

process.on("beforeExit", function() {
  console.log("before exit...")
})

// 主动退出
process.exit() // 手动退出并不会触发beforeExit

流操作

js 复制代码
// process.stdin.pipe(process.stdout)


process.stdin.setEncoding("utf-8")
process.stdin.on("readable", () => {
  const chunk = process.stdin.read() // 从输入中取出数据
  if(!chunk) {
    process.stdout.write("输出" + chunk)
  }
})
  • require:实现模块的加载

  • module、exports:处理模块的导出

Buffer

流也可以理解为一个数据结构,用于存储数据的,可以分段。对于大文件读取,可以避免超过内存过大占满内存,通过流操作配合管道可以将数据一段一段的传递。

buffer就是一片内存空间。可以实现Nodejs下二进制数据操作。不占据V8堆内存大小的内存空间,由c++底层分配内存,但是内存的回收还是由V8的GC去控制。一般配合Stream流进行使用,充当数据缓存区。

创建buffer对象

  • alloc:创建指定字节大小的 buffer

  • allocUnsafe:创建指定大小的 buffer(不安全)

  • from:接收数据,创建 buffer

js 复制代码
// const b1 = Buffer.alloc(10)
// // 创建的内存可能会有数据,该内存没有对象指向就可以被拿来创建内存,但是可能会有数据存在。
// const b2 = Buffer.allocUnsafe(10)

// console.log(b1)
// console.log(b2) 

// from 
/**
 * 字符串
 * 数组
 * buffer对象
 */
// const b1 = Buffer.from('1')
// console.log(b1) 

// const b1 = Buffer.from([0xe4, 0xb8, 0xad])
// const b1 = Buffer.from([0x60, 0b1001, 12]) // 60 09 0c 
// const b1 = Buffer.from(["张", 1], "utf") // 直接传入汉字不能识别,需要转成进制才可以传入
// console.log(b1)  // <Buffer 00 01>
// console.log(b1.toString()) 

const b1 = Buffer.alloc(3)
const b2 = Buffer.from(b1) // 重新拷贝一份,修改b1不会影响b2

// console.log(b1) // <Buffer 00 00 00>
// console.log(b2)

b1[0] = 1
console.log(b1) // <Buffer 01 00 00>
console.log(b2) // <Buffer 00 00 00>

实例方法

  • fill:使用数据填充 buffer

  • write:向 buffer 中写入数据

  • toString:从 buffer 中提取数据

  • slice:截取 buffer

  • indexOf:在 buffer 中查找数据

  • copy:拷贝 buffer 中的数据

js 复制代码
let buf = Buffer.alloc(5)

// fill 将缓冲区填满
/**
 * (data, start, end)
 * 可以指定从哪个位置开始填充
 * 可以指定从哪个位置结束
 * 前闭后开
 */
// buf.fill("123", 1,3)
// console.log(buf)
// console.log(buf.toString()) 

// write 
/**
 * 有多少数据就写多少个,多余的剔除
 * (data, start, length)
 * 可以指定从哪个位置开始填充
 * 写入数据的字节数
 */

// buf.write('12345', 0, 4)
// console.log(buf)
// console.log(buf.toString())

// toString
/**
 * (data, start, end)
 * 可以指定从哪个位置开始读取
 * 可以指定从哪个位置结束
 * 前闭后开
 */
// buf = Buffer.from('123456')
// console.log(buf)
// console.log(buf.toString('utf-8', 1, 3)) // 23

// slice 
/**
 * (data, start, end)
 * 可以指定从哪个位置开始截取
 * 可以指定从哪个位置结束
 * 前闭后开
 */
// buf = Buffer.from('123456789')
// let b1 = buf.slice(-3)
// console.log(b1)
// console.log(b1.toString())  // 789

// indexOf
// 返回字节对应的下标, 如果传入的中文需要注意
// buf = Buffer.from('123045607890111213')
// console.log(buf)
// console.log(buf.indexOf('0', 3)) // 3

// copy 
/**
 * (目标, 从目标第几个位置写入, 从源第一个位置开始读取,从源第几个位置结束读取)
 * 
 * 前闭后开
 */
let b1 = Buffer.alloc(6)
let b2 = Buffer.from('123456')

b2.copy(b1, 3, 3, 5)
console.log(b1.toString()) // 45
console.log(b2.toString()) // 123456

静态方法

  • concat: 将多个 buffer 拼接成一个新的 buffer。参数是一个buffer数组。

  • isBuffer:判断当前数据是否为 buffer

js 复制代码
/**
 * 可以指定拼接的长度
 */
const b1 = Buffer.from([1,2,3])
const b2 = Buffer.from([3,4,5])
console.log(Buffer.concat([b1, b2], 2)) // <Buffer 01 02>


console.log(Buffer.isBuffer(b1))

核心模块

path

js 复制代码
const path = require('path')

// console.log(__filename)

// 1 获取路径中的基础名称 
/**
 * 01 返回的就是接收路径当中的最后一部分 
 * 02 第二个参数表示扩展名,如果说没有设置则返回完整的文件名称带后缀
 * 03 第二个参数做为后缀时,如果没有在当前路径中被匹配到,那么就会忽略
 * 04 处理目录路径的时候如果说,结尾处有路径分割符,则也会被忽略掉
 */
// console.log(path.basename(__filename))
// console.log(path.basename(__filename, '.js'))
// console.log(path.basename(__filename, '.css'))
// 会忽略后面的分隔符
// console.log(path.basename('/a/b/c')) // c
// console.log(path.basename('/a/b/c/')) // c

// 2 获取路径目录名 (路径)
/**
 * 01 返回路径中最后一个部分的上一层目录所在路径
 */

// console.log(path.dirname(__filename))
// console.log(path.dirname('/a/b/c')) // /a/b
// console.log(path.dirname('/a/b/c/')) // /a/b

// 3 获取路径的扩展名
/**
 * 01 返回 path路径中相应文件的后缀名(包括点)
 * 02 如果 path 路径当中存在多个点,它匹配的是最后一个点,到结尾的内容
 */

// console.log(path.extname(__filename))
// console.log(path.extname('/a/b')) // ""
// console.log(path.extname('/a/b/index.html.js.css'))
// console.log(path.extname('/a/b/index.html.js.')) // "."

// 4 解析路径
/**
 * 01 接收一个路径,返回一个对象,包含不同的信息
 * 02 root dir base ext name
 */
// const obj = path.parse('/a/b/c/index.html')
// // const obj = path.parse('/a/b/c/')
// // const obj = path.parse('./a/b/c/')
// console.log(obj) 
// {
//   root: '/',
//   dir: '/a/b/c', // path.dirname()
//   base: 'index.html', // path.basename()
//   ext: '.html', // path.extname()
//   name: 'index' // path.basename(path, ext)
// }

// 5 序列化路径
// const obj = path.parse('./a/b/c/')
// console.log(path.format(obj)) // ./a/b\c

// 6 判断当前路径是否为绝对
// console.log(path.isAbsolute('foo'))
// console.log(path.isAbsolute('/foo'))
// console.log(path.isAbsolute('///foo')) // true
// console.log(path.isAbsolute(''))
// console.log(path.isAbsolute('.'))
// console.log(path.isAbsolute('../bar'))

// 7 拼接路径
/**
 * 返回拼接后的相对路径 / 绝对路径。取决于第一个参数
 * 
 * 会识别../
 */
// console.log(path.join('a/b', 'c', 'index.html'))
// console.log(path.join('/a/b', 'c', 'index.html'))
// console.log(path.join('/a/b', 'c', '../', 'index.html'))
// console.log(path.join('/a/b', 'c', './', 'index.html'))
// console.log(path.join('/a/b', 'c', '', 'index.html'))
// console.log(path.join(''))  // . 当前工作目录

// 8 规范化路径
// 可识别转义字符
// console.log(path.normalize(''))
// console.log(path.normalize('a/b/c/d'))
// console.log(path.normalize('a///b/c../d')) // a\b\c..\d
// console.log(path.normalize('a//\\/b/c\\/d'))
// console.log(path.normalize('a//\\b/c\\/d'))  // a\b\c\d
// console.log(path.normalize('a//\b/c\\/d'))  // a\c\d

// 9 绝对路径
// 返回绝对路径
// console.log(path.resolve()) // process.cwd()
/**
 * resolve([from], to)
 * 他会根据参数是否是绝对路径来进行返回,如果是就直接返回拼接好的,否则返回对应盘符的
 * 
 * 就是把to的部分拼接成一个绝对路径。
 * from就是尽可能多的相对路径参数,然后拼接时将忽略
 */
console.log(path.resolve('/a', 'b')) // C:\a\b
console.log(path.resolve('/a', '/b')) // C:\b
console.log(path.resolve('/a', '../b')) // C:\b
console.log(path.resolve('index.html'))

fs

三个基本概念

权限位:用户对于文件所具备的操作权限。

操作符:nodejs通过flag表示对文件的操作方式。

文件标识符:fd就是操作系统分配给被打开文件的标识,当文件被打开,操作系统就会分配一个数字标识。从3开始

fs api

  • readFile:从指定文件中读取数据

  • writeFile:向指定文件中写入数据

  • appendFile:追加的方式向指定文件中写入数据

  • copyFile:将某个文件中的数据拷贝至另一文件

  • watchFile:对指定文件进行监控

js 复制代码
const fs = require('fs')
const path = require('path')

// readFile
/**
 * 默认读取的是二进制数据,如果想要读取文本,我们需要在第二个参数指定utf-8
 */
// fs.readFile('04FS/a.txt', "utf-8",  (err, data) => {
//   if (!err) {
//     console.log(data)
//   }
// }) 

// writeFile 
/**
 * 路径不存在会自动创建
 */
// fs.writeFile('04FS/a.txt', 'ii', {
//   mode: 438,
//   // w+默认值会覆盖源文件数据,a, a+, r+不会覆盖原文件数据。
//   // r+表示从开始替换
//   flag: 'r+',
//   encoding: 'utf-8'
// }, (err) => {
//   if (!err) {
//     fs.readFile('04FS/a.txt', 'utf-8', (err, data) => {
//       console.log(data)
//     })
//   }
// }) 

// 追加内容
/**
 * 路径不存在会创建
 */

// fs.appendFile('04FS/a1.txt', 'hello node.js',{},  (err) => {
//   console.log('写入成功')
// }) 

// copyFile
/**
 * 直接覆盖目标文件(先清空再写入)
 * 
 * 源文件可以不存在
 */
// fs.copyFile('04FS/a222.txt', '04FS/a1.txt', () => {
//   console.log('拷贝成功')
// }) 

// watchFile
/**
 * {interval: 20} 每20ms监控一次
 */
fs.watchFile('04FS/a.txt', {interval: 20}, (curr, prev) => {
  // curr:修改之后的文件对象
  // prev:修改之前的文件对象
  if (curr.mtime !== prev.mtime) {
    console.log('文件被修改了')
    // 取消监听
    fs.unwatchFile('04FS/a.txt')
  }
})

文件打开与关闭

上面的文件读取和写入都是一次性的,但是对于大文件读取写入,显然是不合理的。所以Nodejs将文件的打开读取写入关闭单独设计。

js 复制代码
const fs = require('fs')
const path = require('path')

// open 
/**
 * 文件不存在将报错
 * 
 * 可以拿到文件标识符
 */
// fs.open(path.resolve(__dirname, 'a.txt'), 'r', (err, fd) => {
//   console.log(fd) // 3
// }) 

// close
fs.open(path.resolve(__dirname, 'a.txt'), 'r', (err, fd) => {
  console.log(fd)
  fs.close(fd, err => {
    console.log('关闭成功')
  })
})

文件分段读取和写入

js 复制代码
const fs = require('fs')

// read : 所谓的读操作就是将数据从磁盘文件中写入到 buffer 中
let buf = Buffer.alloc(10)

/**
 * fs.read(fd, buf, offset, length, position)
 * fd 定位当前被打开的文件 
 * buf 用于表示当前缓冲区
 * offset 表示当前从 buf 的哪个位置开始执行写入
 * length 表示当前次写入的长度
 * position 表示当前从文件的哪个位置开始读取
 * 
 * 注意:指定缓存区的数量必须大于等于读取的长度。否则报错
 */
//  fs.open('04FS/a.txt', 'r', (err, rfd) => {
//   fs.read(rfd, buf, 1, 4, 3, 
//     // data就是一个buffer数据 
//     // readBytes 读取的buffer长度
//     (err, readBytes, data) => {
//     console.log(readBytes)
//     console.log(data)
//     console.log(data.toString())
//   })
// }) 

// write 将缓冲区里的内容写入到磁盘文件中
/**
 * fs.write(fd, buf, offset, length, position)
 * fd 定位当前被打开的文件 
 * buf 用于表示当前缓冲区
 * offset 表示当前从 buf 的哪个位置开始执行读取
 * length 表示当前次写入的长度
 * position 表示当前从文件的哪个位置写入读取
 * 
 */
buf = Buffer.from('1234567890')
// 这里的关于写的操作描述符(w, w+, r+)都一样,我们可以在下面控制写入的位置
fs.open('04FS/b.txt', 'r+', (err, wfd) => {
  // written 每次写入的字节数
  // buffer 缓冲区
  fs.write(wfd, buf, 2, 4, 0, (err, written, buffer) => {
    console.log(written, '----')
    // 我们再次写入时,就需要指定写入的位置4
    fs.write(wfd, buf, 2, 4, 4, (err, written, buffer) => {
      console.log(written, '----')
      fs.close(wfd)
    })
  })
})

手写分段文件拷贝

js 复制代码
/**
 * 01 打开 a 文件,利用 read 将数据保存到 buffer 暂存起来
 * 02 打开 b 文件,利用 write 将 buffer 中数据写入到 b 文件中
 */
    function copyFile(src, dest) {
      let buf = Buffer.alloc(10)
      const BUFFER_SIZE = buf.length
      let readOffset = 0

      fs.open(path.resolve(__dirname, src), 'r', (err, rfd) => {
        fs.open(path.resolve(__dirname, dest), 'w', (err, wfd) => {
          function next () {
             //从buf的0位置开始放,读取10个字节,从文件的0、0+readBytes、...位置开始读取
            fs.read(rfd, buf, 0, BUFFER_SIZE, readOffset, (err, readBytes) => {
              if (!readBytes) {
                // 如果条件成立,说明内容已经读取完毕
                fs.close(rfd, ()=> {})
                fs.close(wfd, ()=> {})
                console.log('拷贝完成')
                return
              }
              // 从buf的0位置开始读, 写入readBytes字节,从文件readOffset(读多少写多少)位置开始写入
              fs.write(wfd, buf, 0, readBytes, readOffset, (err, written) => { 
                readOffset += readBytes
                next()
              })
            })
          }
          next()
        })
      })
    }

    copyFile("a.txt", "b.txt")

目录操作

  • access: 判断文件或目录是否具有操作权限

  • stat:获取目录及文件信息

  • mkdir:创建目录

  • rmdir:删除目录

  • readdir:读取目录中内容

  • unlink:删除指定文件

js 复制代码
const fs = require('fs')

// 一、access 
// 是否有操作权限 ,可以判断文件是存在
// fs.access('a.txt', (err) => {
//   if (err) {
//     console.log(err)
//   } else {
//     console.log('有操作权限')
//   }
// }) 

// 二、stat 
// 获取文件、目录信息
// fs.stat('a.txt', (err, statObj) => {
//   console.log(statObj)
//   console.log(statObj.size)
//   console.log(statObj.isFile())
//   console.log(statObj.isDirectory())
// }) 

// 三、mkdir 
// 创建目录
// 需要保证父级目录是存在的,如果不存在我们可以指定{recursive: true}递归创建
// fs.mkdir('a/b/c.txt', {recursive: true}, (err) => {
//   if (!err) {
//     console.log('创建成功')
//   }else{
//     console.log(err)
//   }
// }) 

// 四、rmdir
// 删除目录
// 默认只删除最后一级目录/文件
// 当目录不为空,则不能删除,如果想要强制删除,我们需要添加 {recursive: true}

// fs.rmdir('b', {recursive: true}, (err) => {
//   if (!err) {
//     console.log('删除成功')
//   } else {
//     console.log(err)
//   }
// })

// 五、readdir 
// 只会返回当前层级的目录和文件
// 路径不存在则返回undefined
// fs.readdir('a/b', (err, files) => {
//   console.log(files)
// }) 

// 六、unlink
// 删除指定文件, 给定的路径必须是文件路径,否者报错
fs.unlink('a', (err) => {
  if (!err) {
    console.log('删除成功')
  }else {
    console.log("err", err)
  }
}) 

手写文件递归创建

同步,异步,promise写法

js 复制代码
const fs = require('fs')
const path = require('path')

/**
 * 01 将来调用时需要接收类似于 a/b/c ,这样的路径,它们之间是采用 / 去行连接
 * 02 利用 / 分割符将路径进行拆分,将每一项放入一个数组中进行管理  ['a', 'b', 'c']
 * 03 对上述的数组进行遍历,我们需要拿到每一项,然后与前一项进行拼接 /
 * 04 判断一个当前对拼接之后的路径是否具有可操作的权限,如果有则证明存在,否则的话就需要执行创建
 */

function makeDirSync (dirPath) {
  let items = dirPath.split(path.sep)
  for(let i = 1; i <= items.length; i++) {
    let dir = items.slice(0, i).join(path.sep)
    try {
      // 没有该文件,将报错,然后就会在catch中创建。
      fs.accessSync(dir)
    } catch (err) {
      fs.mkdirSync(dir)
    }
  }
}
js 复制代码
const fs = require('fs')
const path = require('path')
const {promisify} = require('util')

/* function mkDir (dirPath, cb) {
  let parts = dirPath.split('/')
  let index = 1

  function next () {
    if (index > parts.length) return cb && cb()

    let current = parts.slice(0, index++).join('/')

    fs.access(current, (err) => {
      if (err) {
        fs.mkdir(current, next)
      }else{
        next()
      }
    })
  }
  next()
}

mkDir('a/b/c', () => {
  console.log('创建成功')
}) */


// 将 access 与 mkdir 处理成 async... 风格
const access = promisify(fs.access)
const mkdir = promisify(fs.mkdir)

async function myMkdir (dirPath, cb) {
  let parts = dirPath.split('/')
  for(let index = 1; index <= parts.length; index++) {
    let current = parts.slice(0, index).join('/')
    try {
      await access(current)
    } catch (err) {
      await mkdir(current)
    }
  }
  cb && cb()
}

myMkdir('a/b/c', () => {
  console.log('创建成功')
})

手写删除文件/文件夹

js 复制代码
const { dir } = require('console')
const fs = require('fs')
const path = require('path')

/**
 * 需求:自定义一个函数,接收一个路径,然后执行删除
 * 01 判断当前传入的路径是否为一个文件,直接删除当前文件即可
 * 02 如果当前传入的是一个目录,我们需要继续读取目录中的内容,然后再执行删除操作
 * 03 将删除行为定义成一个函数,然后通过递归的方式进行复用
 * 04 将当前的名称拼接成在删除时可使用的路径
 */
function myRmdir (dirPath, cb) {
  // 判断当前 dirPath 的类型
  fs.stat(dirPath, (err, statObj) => {
    if (statObj.isDirectory()) { // b // c
      // 目录---> 继续读取
      fs.readdir(dirPath, (err, files) => {
        let dirs = files.map(item => { // c // d
          return path.join(dirPath, item)
        })
        let index = 0
        function next () {
          // 只有遍历到最后一层级再删除。如果该文件夹还有内容,就继续递归。直到dirs为空
          if (index == dirs.length) return fs.rmdir(dirPath, cb)

          let current = dirs[index++]

          myRmdir(current, next) 
        }

        next()

        // for(let file of dirs) {
        //   myRmdir(current, next)
        // }
      })
    } else {
      // 文件---> 直接删除
      fs.unlink(dirPath, cb)
    }
  })
}

myRmdir('b', () => {
  console.log('删除成功了')
})

文件写入流过程分析

模块化

module属性

  • id: 返回模块标识符, 一般是一个绝对路径

  • filename:返回文件模块的绝对路径

  • loaded: 返回布尔值, 表示模块是否完成加载

  • parent: 返回对象存放调用当前模块的模块

  • children: 返回数组, 存放当前模块调用的其它模块

  • exports: 返回当前模块需要暴露的内容

  • paths: 返回数组, 存放不同目录下的node modules 位置

require 属性

  • resolve:返回模块文件绝对路径
js 复制代码
console.log(require.resolve("./m")) // 返回指定路径的绝对路径
  • extensions:依据不同后缀名执行解析操作

  • main:返回主模块对象

require.main表示入口文件,就是顶级文件。未被其他文件导入的文件。

如果该文件被引入,那么require.main就指向入口文件。

js 复制代码
// require.main指向入口文件
console.log(module.parent.parent.parent... === require.main) // true

commonjs模块化加载源码解析

nodejs中的内置模块在nodejs运行的时候就已经被作为二进制加载到内存中,require时直接在内存中取出。

加载流程分析

  • 路径分析:确定目标模块位置 (内置,第三方(module.paths),本地)

  • 文件定位:确定目标中的具体文件。(js/json/node -> package.json中的main -> index.js/json/node)

  • 编译执行:对模块内容进行编译,返回可用的exports对象。(js -> 函数调用,传递参数。json -> JSON.parse())

源码解析

  • relResolveCacheIdntifier 根据路径查找该路径是否被缓存。
  • 无,解析并补全路径为完整的路径
  • 根据路径查找模块是否被缓存。
  • 是否是内置模块
  • 创建一个module对象,并缓存该模块。
  • module.load() 初始化一些模块属性。
  • 通过后缀取出对应的函数。
  • 函数内通过readFileSync同步加载文件 内容。
  • module._compile() 内部将文件内容包裹在一个全局函数中。即每个模块的作用域函数。
  • 通过获取require、exports、module、__dirname、__filename传入并调用全局函数,返回结果。其中this被绑定为空对象。

自己实现commonjs模块化

  • 通过fs.readFileSync读取文件,eval执行字符串。

缺点:没有自己的作用域。

js 复制代码
    const fs = require("fs")

    const content = fs.readFileSync("./demo.js", "utf-8") // var  a = 10

    eval(content)

    console.log("a", a) // 10
  • 通过Function执行字符串。

缺点:定义麻烦。

js 复制代码
const fs = require("fs")

const content = fs.readFileSync("./demo.js", "utf-8") // var  a = 10

const fn = new Function("", `${content} return a`);

console.log(fn()) // 10
  • 通过nodejs内置的核心模块VM

可以创建一个独立运行的沙箱环境。

js 复制代码
const fs = require("fs")
const vm = require("vm")

const b = 100
a = 100 // 可以被使用
const content = fs.readFileSync("./demo.js", "utf-8") // var  a = 10


// vm.runInThisContext(content) // 内部是一个沙箱,不能使用外部变量。但是外部可以使用沙箱内的内容
// console.log(a) // 10

vm.runInThisContext("a = a + 10") // 但是沙箱内可以使用外部为使用声明定义的变量
console.log(a) // 110

实现

js 复制代码
const { dir } = require('console')
const fs = require('fs')
const path = require('path')
const vm = require('vm')

function Module (id) {
  // 存储文件路径
  this.id = id
  // 存储导出内容
  this.exports = {}
}

// 分析传入的路径
Module._resolveFilename = function (filename) {
  // 利用 Path 将 filename 转为绝对路径
  let absPath = path.resolve(__dirname, filename)
  
  // 判断当前路径对应的内容是否存在()
  if (fs.existsSync(absPath)) { // 这里查到文件夹也是可以通过的
    // 如果条件成立则说明 absPath 对应的内容是存在的
    // 如果是文件夹取index.js,否则抛出错误
    if(fs.statSync(absPath).isDirectory()) {
      if(fs.existsSync(path.join(absPath, "index.js"))) {
        return path.join(absPath, "index.js")
      }else {
        throw new Error(`${filename} is not exists`)
      }
    }else {
      return absPath
    }
  } else {
    // 文件定位
    let suffix = Object.keys(Module._extensions)

    for(var i=0; i<suffix.length; i++) {
      let newPath = absPath + suffix[i]
      if (fs.existsSync(newPath)) {
        return newPath
      }
    }
  }
  throw new Error(`${filename} is not exists`)
}

// 定义查询的扩展名和函数映射
Module._extensions = {
  '.js'(module) {
    // 读取
    let content = fs.readFileSync(module.id, 'utf-8')

    // 包装
    content = Module.wrapper[0] + content + Module.wrapper[1] 
    
    // VM 
    let compileFn = vm.runInThisContext(content)

    // 准备参数的值
    let exports = module.exports
    let dirname = path.dirname(module.id)
    let filename = module.id

    // 调用
    compileFn.call(exports, exports, myRequire, module, filename, dirname)
  },
  '.json'(module) {
    let content = JSON.parse(fs.readFileSync(module.id, 'utf-8'))

    module.exports = content
  }
}

Module.wrapper = [
  "(function (exports, require, module, __filename, __dirname) {",
  "})"
]

// 缓存对象 (绝对路径:module对象)
Module._cache = {}

// 加载模块,取出扩展名执行对应函数
Module.prototype.load = function () {
  let extname = path.extname(this.id)
  
  Module._extensions[extname](this)
}

function myRequire (filename) {
  // 1 绝对路径
  let mPath = Module._resolveFilename(filename)
  
  // 2 缓存优先
  let cacheModule = Module._cache[mPath]
  if (cacheModule) return cacheModule.exports

  // 3 创建空对象加载目标模块
  let module = new Module(mPath)

  // 4 缓存已加载过的模块
  Module._cache[mPath] = module

  // 5 执行加载(编译执行)
  module.load()

  // 6 返回数据
  return module.exports
}

events

  • node.js 是基于事件驱动的异步操作架构,内置events模块

  • events 模块提供了 EventEmitter类

  • node.js 中很多内置核心模块继承EventEmitter

这部分比较简单,就不过于介绍了。

事件循环

stream

nodejs诞生之初是为了提高IO性能,所以文件操作系统和网络模块实现了流接口。

nodejs的流就是处理流式数据的抽象接口。

不通过流来读取文件的缺陷

  • 同步读取资源文件,用户需要等待数据读取完成

  • 资源文件最终一次性加载至内存,开销较大

流操作的用途

  • 数据分段传输。
  • 通过管道对分段数据进行加工。

流处理数据的优势

  • 时间效率:流的分段处理可以同时操作多个数据 chunk

  • 空间效率:同一时间流无须占据大内存空间

  • 使用方便:流配合管理,扩展程序变得简单

Node.js 中流的分类

  • Readable:可读流,能够实现数据的读取

  • Writeable:可写流,能够实现数据的写操作

  • Duplex:双工流,既可读又可写。读和写的数据是分离的。

  • Tranform:转换流,可读可写,还能实现数据转换。将读写操作进行连通。

Nodejs中流的特点

  • Stream模块实现了上述四个具体的抽象

  • 所有的流都继承自EventEmitter,都可以通过监听内置事件来处理数据。

自定义可读流

js 复制代码
const {Readable} = require('stream')

// 模拟底层数据
let source = ['zh', 'llm', 'hh']

// 自定义类继承 Readable
class MyReadable extends Readable{
  constructor(source) {
    super()
    this.source = source
  }
  // 提供数据消费
  _read() {
    // 将数据放在缓存区
    let data = this.source.shift() || null 
    this.push(data)
  }
}

// 实例化
let myReadable = new MyReadable(source)

/**
 * 
 * 两种读取方式
 * - readable:将数据读取到缓存区,然后在进行消费(暂停模式)
 * - data:数据远远不断地输出,可以直接消费 (流动模式)
 */
myReadable.on('readable', () => {
  let data = null 
  // read方法可以设置指定的读取长度
  while((data = myReadable.read(2)) != null) {
    console.log(data.toString())
  }
}) 

// myReadable.on('data', (chunk) => {
//   console.log(chunk.toString())
// })

自定义可写流

js 复制代码
const { Writable } = require('stream')

class MyWriteable extends Writable {
  constructor() {
    super()
  }
  _write(chunk, en, done) {
    // 将数据输出到控制台
    process.stdout.write(chunk.toString() + '\n')
    // 执行传入的回调
    process.nextTick(done)
  }
}

let myWriteable = new MyWriteable()

myWriteable.write('我是zhang-glitch', 'utf-8', () => {
  console.log('end')
})

自定义Duplex双工流

读和写的数据是分离的。

js 复制代码
let {Duplex} = require('stream')

class MyDuplex extends Duplex{
  constructor(source) {
    super()
    this.source = source
  }
  _read() {
    let data = this.source.shift() || null 
    this.push(data)
  }
  _write(chunk, en, next) {
    process.stdout.write(chunk + "\n")
    process.nextTick(next)
  }
}

let source = ['a', 'b', 'c']
let myDuplex = new MyDuplex(source)

// 读和写是相互独立的。
myDuplex.on('data', (chunk) => {
  console.log(chunk.toString())
}) 

myDuplex.write('我是zhang-glitch', () => {
  console.log("end")
})

自定义一个Transform双工流

写入和读取的数据是相通的。

js 复制代码
const {Transform} = require("stream")

class MyTransform extends Transform {
  constructor(options) {
    super()
  }

  _transform(chunk, encoding, callback) {
    // 处理数据,交给可读流读取
    this.push(chunk.toString()+"111")
    callback(null)
  }
}

const myTransform = new MyTransform()

// 作为可写流写入数据
myTransform.write("张")
myTransform.write("三")

// 作为可读流读取数据
myTransform.on("readable", () => {
  console.log(myTransform.read().toString())
})

文件流

文件可读流

两种读取方式

  • data。读取时不经过缓冲区,直接读取。我们可以调用pause, resume来实现为readable模式。
  • readable。读取缓存区的存在的数据。如果该数据长度为0,就会重新触发_read进行数据读取到缓存区。读取的长度就是highWaterMark定义的长度。

由于我们是按照字节为单位分段读取,中间过程是无用的,所以我们一般都会将数据缓存在buffer中,当读取完毕后,统一处理。

js 复制代码
const fs = require('fs')

let rs = fs.createReadStream('test.txt', {
  // 操作符
  flags: 'r',
  // buffer的形式读取
  encoding: null, 
  // 标识符
  fd: null,
  // 权限位
  mode: 438,
  // 自动关闭文件
  autoClose: true, 
  // 表示读取范围,头尾都包 (开始读取)
  start: 0,
  // 结束读取 
  // end: 3,
  // 每次读取几个字节数据。即buffer缓冲区长度 默认是64kb
  highWaterMark: 3
})

// data事件中的 rs._readableState.length 始终是0,所以我们读取时并不在缓冲区中读取
// rs.on('data', (chunk) => {
//   console.log(chunk.toString())
//   // 通过pause, resume可以实现暂停模式读取文件
//   // 暂停读取
//   // rs.pause()
//   // setTimeout(() => {
//   //   // 开始读取
//   //   rs.resume()
//   // }, 1000)
// }) 

// 会自动触发该事件,知道数据读取完成
// rs.on('readable', () => {
//   // let data = rs.read()
//   // console.log(data?.toString())
//   let data
//   while((data = rs.read(1)) !== null) {
//     console.log(data.toString())
//     // 读取缓存区的存在的数据。如果该数据长度为0,就会重新触发_read进行数据读取到缓存区。读取的长度就是highWaterMark定义的长度
//     console.log('----------', rs._readableState.length)
//   }
// }) 



// createReadStream调用后就会触发
// rs.on('open', (fd) => {
//   console.log(fd, '文件打开了')
// })

// 数据消费完毕出发
// rs.on('close', () => {
//   console.log('文件关闭了')
// })


// 数据读取是分段的,我们需要拿到读取的所有buffer,然后在处理
let bufferArr = []
rs.on('data', (chunk) => {
  bufferArr.push(chunk)
})

rs.on('end', () => {
  // 如果是buffer,我们需要通过concat拼接
  console.log(Buffer.concat(bufferArr).toString())
  // 注意from传入的参数是一个数组,而不是buffer数组。
  // console.log(bufferArr, Buffer.from(bufferArr).toString())
  console.log('当数据被清空之后')
})

rs.on('error', (err) => {
  console.log('出错了')
})

文件可写流

写入数据只能写入buffer和字符串。

js 复制代码
const fs = require('fs')

const ws = fs.createWriteStream('test.txt', {
  flags: 'w', 
  mode: 438,
  fd: null,
  encoding: "utf-8",
  // 从文件哪个位置开始写入
  start: 0,
  // 默认是16kb
  highWaterMark: 3
})

// 写入表示一次文件的打开和关闭。而不是调用write为一次。

// 字符串 或者  buffer -> fs rs
let buf = Buffer.from('abc')
ws.write(buf, () => {
  console.log('ok1')
}) 

// 多次写入
ws.write('中国', () => {
  console.log('ok2')
}) 

// createWriteStream 调用时触发
ws.on('open', (fd) => {
  console.log('open', fd)
}) 


// close 是在数据写入操作全部完成之后再执行。
// 只有手动调用end方法才会触发
ws.on('close', () => {
  console.log('文件关闭了')
}) 

// end 执行之后就意味着数据写入操作完成
ws.end('结束')


// error 并不能捕获语法错误。
ws.on('error', (err) => {
  console.log('出错了')
})

write的执行流程

js 复制代码
const fs = require("fs")


const ws = fs.createWriteStream("test.txt", {
  highWaterMark: 3
})


ws.write("1234")
// ws.write("2")
// ws.write("3")

ws.on("drain", () => {
  // 当写入的字节数大于等于highWaterMark时触发
  console.log("drain触发")
})

通过drain事件来限制写入速度

js 复制代码
/**
 * 需求:"中国牛逼" 写入指定的文件
 * 01 一次性写入
 * 02 分批写入
 */
let fs = require('fs')

let ws = fs.createWriteStream('test.txt', {
  highWaterMark: 3
})

// 对于大量数据,他会造成内存溢出。
// 如果生产者生产的数据大于消费者消费的数据,那么多余的数据将放在缓冲区中
// ws.write('中国牛逼')

let source = "中国牛逼".split('')
let num = 0
let flag = true

function executeWrite () {
  flag = true
  while(num !== 4 && flag) {
    flag = ws.write(source[num])
    num++
  }
}

executeWrite()

ws.on('drain', () => {
  console.log('drain 执行了')
  // 控制继续写入
  executeWrite()
})

背压机制

就是实现nodejs平滑写入内容。

nodejs的stream实现了背压机制。如果没有背压机制读写数据可能会出现问题。当生产者的生产的数据量大于消费者消费的数据量,就会将多余的数据放在缓冲区中,但是缓冲区也有大小限制,可能会造成内存泄漏,GC频繁调用,其他进程变慢等。

js 复制代码
let fs = require('fs')

let rs = fs.createReadStream('test.txt', {
  highWaterMark: 4
})

let ws = fs.createWriteStream('test1.txt', {
  highWaterMark: 1
})

let flag = true

rs.on('data', (chunk) => {
  flag = ws.write(chunk, () => {
    console.log('写完了')
  })
  if (!flag) {
    rs.pause()
  }
})

// 当drain触发时,表示数据可以继续写入
ws.on('drain', () => {
  rs.resume()
}) 

// pipe做的事情就是上面那些代码做的事情
// rs.pipe(ws)
相关推荐
温轻舟6 分钟前
前端开发 -- 自动回复机器人【附完整源码】
前端·javascript·css·机器人·html·交互·温轻舟
赵大仁7 分钟前
深入解析 Vue 3 的核心原理
前端·javascript·vue.js·react.js·ecmascript
csstmg19 分钟前
记录一次前端绘画海报的过程及遇到的几个问题
前端
bidepanm20 分钟前
Vue.use()和Vue.component()
前端·javascript·vue.js
Javatutouhouduan32 分钟前
如何系统全面地自学Java语言?
java·后端·程序员·编程·架构师·自学·java八股文
顾平安36 分钟前
手写 PromiseA+ 实现,轻松通过 872 条用例
前端
胡西风_foxww40 分钟前
【ES6复习笔记】对象方法扩展(17)
前端·笔记·es6·对象·方法·扩展·对象方法扩展
后端转全栈_小伵41 分钟前
MySQL外键类型与应用场景总结:优缺点一目了然
数据库·后端·sql·mysql·学习方法
bin91531 小时前
npm报错
前端·npm·node.js
一指流沙q1 小时前
Chrome被360导航篡改了怎么改回来?
前端·chrome