• 技术文章 >web前端 >js教程

    深入浅析Node.js中常见的内置模块

    青灯夜游青灯夜游2022-05-16 21:00:54转载481
    本篇文章带大家了解一下Nodejs中常见的内置模块(路径、文件系统、Events),希望对大家有所帮助!

    内置模块path

    路径的演练

    如果我们盲目的将两个路径进行拼接,比如path1 + '/' + path2,可能在当前所在的操作系统中是可以跑起来的,但如果我们换了一个操作系统执行代码,可能就识别不到该路径了,因为不同的操作系统要求的路径分割符是不一样的

    const path = require('path')
    // 我们path1和path2两部分路径故意写错误的分隔符出来
    const path1 = 'User\ASUS\DCC'
    const path2 = 'MyWork\5月4日'
    // path对象中的resolve方法可以实现两个路径之间根据当前操作系统选取正确的路径分隔符进行拼接
    const fileName = path.resolve(path1, path2)
    // 可以看到我们原字符串中原本是用\拼接的,通过path.resolve方法之后都换成了使用\进行拼接
    console.log(fileName); // C:\Users\ASUS\Desktop\前端学习\算法\User\ASUS\DCC\MyWork\5月4日

    path模块的其他方法

    1. 获取路径的信息

    const path = require('path')
    const file = '/foo/bar/baz/asdf/quux.html'
    const path1 = path.dirname(file)
    const path2 = path.basename(file)
    const path3 = path.extname(file)
    console.log(path1); // /foo/bar/baz/asdf
    console.log(path2); // quux.html
    console.log(path3); // .html

    2. join路径拼接

    path.join() 方法使用平台特定的分隔符把全部给定的 path 片段连接到一起,并规范化生成的路径。简单来说就是根据当前的操作系统选取合适的路径分隔符将多个路径拼接在一起,当然其也会纠正原先路径中不正确的分隔符

    const path = require('path')
    const path1 = '/USER/ASUS'
    const path2 = 'My/DCC'
    const fileName = path.join(path1, path2)
    console.log(fileName); // \USER\ASUS\My\DCC

    3. resolve方法拼接(用的最多)

    path.resolve() 方法会把一个路径或路径片段的序列解析为一个绝对路径

    const path = require('path')
    const path1 = 'asd/sad'
    const path2 = 'My/DCC'
    const fileName1 = path.join(path1, path2)
    const fileName2 = path.resolve(path1, path2)
    console.log(fileName1); // asd\sad\My\DCC
    console.log(fileName2); // C:\Users\ASUS\Desktop\前端学习\算法\asd\sad\My\DCC

    4. resolve和join方法的区别

    path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif');
    // 如果当前工作目录为 /home/myself/node,
    // 则返回 '/home/myself/node/wwwroot/static_files/gif/image.gif'
    const path = require('path')
    const path1 = 'asd/sad'
    const path2 = '/My/DCC'
    const fileName1 = path.join(path1, path2)
    const fileName2 = path.resolve(path1, path2)
    console.log(fileName1); // asd\sad\My\DCC
    console.log(fileName2); // \My\DCC
    const path = require('path')
    const path1 = '/USER/ASUS'
    const path2 = 'My/DCC/'
    path.join(path1, path2) // \USER\ASUS\My\DCC\
    path.resolve(path1, path2) // \USER\ASUS\My\DCC

    path.resolve方法在webpack中也有大量使用

    比如我们在react项目使用craco来配置路径别名的时候

    1.png

    使用ES Module也是可以引入node中的核心模块的

    因为上面也有说过,ES Module导入CommonJS的模块是被允许的,所以自然也可以导入嵌入在node中的核心模块

    // test.mjs
    import path from 'path'
    console.log(path); // 是正常的path对象

    内置模块fs

    fs的API介绍

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

    const fs = require('fs')
    const filename = './test.html'
    const file = fs.statSync(filename)
    console.log(file);

    statSync方法可以同步读取我们的文件信息, 返回一个 fs.Stats 实例

    2.png

    fs.stat使用方法:fs.stat(path, callback),回调有两个参数 (err, stats) ,其是异步读取文件的,在发生错误或者读取完成之后都会去执行我们的回调函数

    const fs = require('fs')
    const filename = './test.html'
    // 回调函数的两个参数,一个是错误信息,另一个是文件信息
    const file = fs.stat(filename, (err, info) => {
      console.log(info); // Stats类
    })
    console.log(file); // undefined,因为是异步的,所以并不会阻塞代码执行,打印的时候还没有获取到文件信息
    const fs = require('fs')
    const filename = './test.html'
    // fs.promises是fs模块中的一个对象,它里面的很多方法都是基于promise的
    const file = fs.promises.stat(filename).then(res => {
      console.log(res); // Status类
    }).catch(err => {
      console.log(err);
    })
    console.log(file); // Promise { <pending> }

    文件描述符

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

    为了简化用户的工作,Node.js抽象出操作系统之间的特定差异,并为所有打开的文件分配一个数字型的文件描述符。也就是说node中的api很多把文字描述符的东西屏蔽掉了,相当于内部帮你做了这些操作

    const fs = require('fs')
    const filename = './test.html'
    fs.open(filename, (err, fd) => {
      console.log(fd); // 4
      
      // 通过描述符去读取对应的文件信息
       fs.fstat(fd, (err, info) => {
        console.log(info); // Stats对象
      })
    })

    文件的读写

    如果我们下网对文件的内容进行操作,这个时候可以使用文件的读写

    1. 文件写入

    const fs = require('fs')
    fs.writeFile('./a.txt', '你好啊!', err => {
      console.log(err);
    })

    我们原本是没有这个文件的,但由于options参数中的flag属性默认是w,所以会帮我们自动创建一个文件并将对应的值写入

    3.png

    在上面的代码中,你会发现有一个大括号没有填写任何的内容,这个就是写入时填写的options参数

    const fs = require('fs')
    fs.writeFile('./a.txt', '你好啊!', { flag: 'a+' }, err => {
      console.log(err);
    })

    我们将flag改为a+之后,做的就是文件的追加操作了,发现我们要写入的文字出现在了目标文件的末尾

    4.png

    2. 文件读取

    在文件读取时,如果不填写encoding,则返回的结果是Buffer,类似是一串二进制编码

    因为在fs.readFile方法中,encoding属性的默认值为null,也就是说他是没有默认值的,需要我们手动指定才行,其flag的默认值是'r'

    const fs = require('fs')
    // 没有指定encoding,他就不知道以哪种字符编码格式去读取文件
    fs.readFile('./a.txt', (err, data) => {
      console.log(data); // <Buffer e4 bd a0 e5 a5 bd e5 95 8a ef bc 81 e4 bd a0 e5 a5 bd e5 95 8a ef bc 81 e4 bd a0 e5 a5 bd e5 95 8a ef bc 81>
    })
    
    fs.readFile('./a.txt', {encoding: 'utf-8'}, (err, data) => {
      console.log(data); // 你好啊!你好啊!你好啊!
    })

    文件夹操作

    1. 新建一个文件夹 — fs.mkdir(path[, mode], callback)

    const fs = require('fs')
    const path = require('path')
    // 绝对路径
    const targetDir = path.resolve(__dirname, 'dcc')
    try {
      fs.mkdirSync(targetDir)
    } catch (err) {
      console.log(err);
    }
    
    // 相对路径
    const dirname = './dcc'
    // fs模块有个existsSync方法可以判断当前参数所对应的路径存不存在
    if (!fs.existsSync(dirname)) {
      fs.mkdir(dirname, err => {
        console.log(err);
      })
    }

    发现对应的文件夹已经创建好了,如果我们再执行一遍这个程序发现会报错,说明了相同的文件夹是不可以重复创建的,同时也说明了当我们使用fs.mkdir方法创建文件时,可以传入绝对路径,也能传入相对路径

    5.png

    2. 获取文件夹的所有文件 — fs.readdir(path[, options], callback)

    const fs = require('fs')
    fs.readdir('./dcc', (err, files) => {
      // 其读取到的是一个文件数组,包括里面的目录也能读取到
      console.log(files); // [ 'a.html', 'b.txt', 'c.md', dir ]
    })

    思考:如果我们现在想把该文件夹里面的所有文件读取出来,比如说文件夹中其它文件夹的文件,应该要怎么实现呢?

    其实我们可以通过传入参数的形式让readdir方法读取目录下面文件的时候,把它对应的文件类型也传递出来,也就是将options所对应的withFileTypes 属性更改为true

    6.png

    那么每一个文件信息都对应一个Dirent对象,且每个对象中都有一个isDirectory方法(在原型上)用来判断当前文件是不是文件夹,如下图所示:

    7.png

    既然文件夹里面还有可能会套文件夹,所以想要读取出所有文件的路径就必须要用递归的方法来实现了

    const fs = require('fs')
    const path = require('path')
    const getFileName = (dirname) => {
      // 根据目录路径读取该路径下的所有文件名称,
      // withFileTypes属性改为true是为了在读取文件名称的时候顺便将其文件类型暴露出来
      // 文件类型并不是Dirent对象下的属性,而是要我们通过isDirectory方法去获得
      fs.readdir(dirname, { withFileTypes: true }, (err, files) => {
        files.forEach(file => {
          // 每个Dirent对象上都有一个isDirectory方法用于判断该文件是否为文件夹
          if (file.isDirectory()) {
            // 如果当前文件仍然是个文件夹,则需要通过resolve方法找到它的路径,然后递归调用函数
            const filePath = path.resolve(dirname, file.name)
            getFileName(filePath)
            // 不是文件夹就直接将名称打印出来即可
          } else {
            console.log(file.name);
          }
        })
      })
    }
    getFileName('./dcc')

    从打印结果可以得知,确实已经递归实现了打印一个目录下面的所有文件名称

    8.png

    3. 文件夹重命名

    重命名可能操作可能需要以管理员身份运行编辑器才被允许

    const fs = require('fs')
    fs.rename('./dcc', './kobe', err => {
      console.log(err); 
    })

    文件夹的复制案例

    场景:一个文件夹中有很多个文件夹,每一个文件夹中又有很多的文件,现要求将这些文件按照原先所在的文件目录格式选取出指定后缀名的文件拷贝到另一个文件夹中

    const fs = require('fs')
    const path = require('path')
    // 获取起始文件路径和目标文件路径
    const startPath = process.argv[2]
    const endPath = process.argv[3]
    const ext = process.argv[4] || '.js'
    // 得到起始路径下的文件
    const allDirs = fs.readdirSync(startPath)
    // 遍历其实文件夹并取出他的子文件夹
    for (const name of allDirs) {
      // 原来文件夹的路径
      const originDir = path.resolve(startPath, name)
      // 获得到拷贝过去后的文件夹路径
      const targetDirname = path.resolve(endPath, name)
      // 通过判断这个路径存不存在,来决定要不要新的创建文件夹,如果存在则说明该文件已经被创建过了,直接跳过去创建下一个文件夹即可
      if (fs.existsSync(targetDirname)) continue;
      // 在目标路径下创建一个新的文件夹
      fs.mkdirSync(targetDirname)
      // 读取与之对应的文件夹内的文件
      const currDirFiles = fs.readdirSync(originDir)
      // 遍历该文件夹,得到所有文件
      for (const name of currDirFiles) {
        // 判断当前文件的后缀名是不是要拷贝过去的文件
        if (path.extname(name) === ext) {
          // 拼接得到原先文件的路径和拷贝过去的文件路径
          const originCopyName = path.resolve(originDir, name)
          const targetCopyName = path.resolve(targetDirname, name)
          // 利用copyFileSync方法进行文件拷贝,其接收两个参数,一个是要被拷贝的源文件名称,另一个是拷贝操作的目标文件名
          fs.copyFileSync(originCopyName, targetCopyName)
          console.log('拷贝成功!');
        }
      }
    }

    当我们执行 node test.js ./dir1 ./dir2 .txt命令之后,发现以txt为后缀名的文件都被拷贝过去了,说明我们的程序没有问题

    9.png

    Events模块

    events基础方法

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

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

    我们从events模块中导入的内容和其它模块有所不同,因为其是一个类。我们可以根据这个类创建出一个“发射器”

    const EmitterEmitter = require('events')
    console.log(EmitterEmitter);
    const emitter = new EmitterEmitter()

    通过发射器,我们可以监听、取消、发射相应的事件

    // addEventListener 是 on 的简写
    emitter.on('click', (...args) => {
      console.log('我被点击了1', args); // 2s之后输出:我被点击了1 [ 'kobe', 'james' ]
    }) 
    
    // 如果绑定的都是相同的事件,那么触发的时候按照监听的顺序来执行
    emitter.on('click', (...args) => {
      console.log('我被点击了2', args); // // 2s之后输出:我被点击了2 [ 'kobe', 'james' ]
    }) 
    
    setTimeout(() => {
      emitter.emit('click', 'kobe', 'james') // 发射事件,可以携带参数
    }, 2000)

    如果我们把setTimeout里面的函数改写一下,然后再将第二个注册事件的回调函数抽离出去,打印结果会发生什么变化呢?

    // addEventListener 是 on的缩写
    emitter.on('click', (...args) => {
      console.log('我被点击了1', args);
    })
    
    const clickFn = (...args) => {
      console.log('我被点击了2', args);
    }
    
    emitter.on('click', clickFn)
    
    setTimeout(() => {
      emitter.emit('click', 'kobe', 'james')
      emitter.off('click', clickFn)
      emitter.emit('click', 'kobe', 'james')
    }, 2000)

    第一次发射事件的时候,两个注册的事件都会被触发,但是当我们使用emitter.off取消了第二个注册事件的后,下次发射相同事件时,第二个事件就不会再被触发了

    10.png

    events获取信息

    const EmitterEmitter = require('events')
    const emitter = new EmitterEmitter()
    emitter.on('click', (...args) => {
      console.log('我被点击了1', args);
    })
    const clickFn = (...args) => {
      console.log('我被点击了2', args);
    }
    emitter.on('click', clickFn)
    emitter.on('tab', clickFn)
    
    console.log(emitter.eventNames()); // [ 'click', 'tab' ]
    console.log(emitter.listenerCount('click')); // 2
    console.log(emitter.listeners('click')); // [ [Function (anonymous)], [Function: clickFn] ]

    events中不常用方法

    const EmitterEmitter = require('events')
    const emitter = new EmitterEmitter()
    emitter.once('click', (...args) => {
      console.log('我被点击了1', args);
    })
    setTimeout(() => {
      // 这一次事件的发射会执行emitter.once绑定的函数
      emitter.emit('click', 'kobe', 'james')
      // 第二次事件的发射不会执行emitter.once绑定的函数
      emitter.emit('click', 'kobe', 'james')
    }, 500)
    const EmitterEmitter = require('events')
    const emitter = new EmitterEmitter()
    emitter.on('click', (...args) => {
      console.log('我被点击了1', args);
    })
    // prependListener方法绑定的函数执行会比用on绑定的回调函数早
    emitter.prependListener('click', (...args) => {
      console.log('我被点击了2', args);
    })
    setTimeout(() => {
      emitter.emit('click', 'kobe', 'james')
    }, 500)

    更多node相关知识,请访问:nodejs 教程

    以上就是深入浅析Node.js中常见的内置模块的详细内容,更多请关注php中文网其它相关文章!

    声明:本文转载于:掘金社区,如有侵犯,请联系admin@php.cn删除
    专题推荐:nodejs​ Node.js node
    上一篇:JavaScript对象的构造函数和new操作符(实例详解) 下一篇:浅析Angular变更检测机制,聊聊如何进行性能优化?
    VIP课程(WEB全栈开发)

    相关文章推荐

    • 【腾讯云】年中优惠,「专享618元」优惠券!• node导出模块有哪两种方式• 什么是单点登录?怎么使用Nodejs实现SSO• 安装node时会自动安装npm吗• node爬取数据实例:聊聊怎么抓取小说章节• Vercel是什么?怎么部署Node服务?• 深入聊聊node.js中的EventEmitter
    1/1

    PHP中文网