Node.js<四>——常见的内置模块解析

语言: CN / TW / HK

内置模块path

路径的演练

  • path模块用于对路径和文件进行处理,提供了很多好用的方法
  • 并且我们知道在Mac OSLinuxwindows上的路径分隔符是不一样的

    • 在Mac OS、Linux的Unix操作系统上使用/来作为文件路径的分隔符
    • windows上会使用`或者`来作为文件路径的分隔符,当然目前也支持/
  • 那么如果我们在windows上使用\来作为分隔符开发了一个应用程序,要部署到Linux上面应该怎么办呢?

    • 显示路径会出现一些问题
    • 所以为了屏蔽它们之间的差异,在开发中对于路径的操作我们可以使用path模块

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

js 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. 获取路径的信息

  • path.dirname() 方法返回一个 path 的目录名
  • path.basename() 方法返回一个 path 的最后一部分,一般来说是文件名
  • path.extname() 方法返回 path 的扩展名

js 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 片段连接到一起,并规范化生成的路径。简单来说就是根据当前的操作系统选取合适的路径分隔符将多个路径拼接在一起,当然其也会纠正原先路径中不正确的分隔符

js 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() 方法会把一个路径或路径片段的序列解析为一个绝对路径

js 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 片段后还未生成一个绝对路径,则当前工作目录会被用上,且其能识别.././/

js path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif'); // 如果当前工作目录为 /home/myself/node, // 则返回 '/home/myself/node/wwwroot/static_files/gif/image.gif'

  • 给定的路径的序列是从右往左被处理的,后面每个 path 被依次解析,直到构造完成一个绝对路径

js 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

  • 生成的路径是规范化后的,且末尾的斜杠会被删除,除非路径被解析为根目录

js 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来配置路径别名的时候

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

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

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

内置模块fs

  • fs是File System的缩写,表示文件系统
  • 对于任何一个为服务器端服务的语言或者框架通常都会有自己的文件系统

    • 因为服务器需要将各种数据、文件等放置到不同的地方
    • 比如用户数据可能大多数是放到数据库中的
    • 比如某些配置文件或者用户资源(图片、音视频)都是以文件的形式存在于操作系统上的
  • Node也有自己的文件系统操作模块,就是fs

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

fs的API介绍

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

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

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

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

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

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

js const fs = require('fs') const filename = './test.html' // 回调函数的两个参数,一个是错误信息,另一个是文件信息 const file = fs.stat(filename, (err, info) => { console.log(info); // Stats类 }) console.log(file); // undefined,因为是异步的,所以并不会阻塞代码执行,打印的时候还没有获取到文件信息

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

    • 很多的api都提供了promise方式,但并不是所有的,所以大家在使用某些东西的时候可以先去查阅文档

js 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)是什么呢?

  • POSIX 系统上,对于每个进程,内核都维护着一张当前打开着的文件和资源的表格
  • 每个打开的文件都分配了一个称为文件描述符的简单的数字标识符
  • 在系统层,所有文件系统操作都是用这些文字描述符来标识和跟踪每个特定的文件
  • Windows系统使用了一个虽然不同但概念上类似的机制来跟踪资源

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

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

```js 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对象 }) }) ```

文件的读写

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

  • fs.readFile(path, options, callback):读取文件的内容
  • fs.wraiteFile(file, data, options, callback):在文件中写入内容

1. 文件写入

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

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

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

  • flag:写入的方式,默认是 'w'

    • 'r' - 以读取模式打开文件。如果文件不存在则发生异常
    • 'r+' - 以读写模式打开文件。如果文件不存在则发生异常
    • 'rs+' - 以同步读写模式打开文件。命令操作系统绕过本地文件系统缓存
    • 'w' - 以写入模式打开文件。文件会被创建(如果文件不存在)或截断(如果文件存在)
    • 'wx' - 类似 'w',但如果 path 存在,则失败
    • 'w+' - 以读写模式打开文件。文件会被创建(如果文件不存在)或截断(如果文件存在)。
    • 'wx+' - 类似 'w+',但如果 path 存在,则失败
    • 'a' - 以追加模式打开文件。如果文件不存在,则会被创建
    • 'ax' - 类似于 'a',但如果 path 存在,则失败
    • 'a+' - 以读取和追加模式打开文件。如果文件不存在,则会被创建
    • 'ax+' - 类似于 'a+',但如果 path 存在,则失败

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

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

  • encoding:字符的编码,默认是'utf8'

2. 文件读取

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

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

```js const fs = require('fs') // 没有指定encoding,他就不知道以哪种字符编码格式去读取文件 fs.readFile('./a.txt', (err, data) => { console.log(data); // })

fs.readFile('./a.txt', {encoding: 'utf-8'}, (err, data) => { console.log(data); // 你好啊!你好啊!你好啊! }) ```

文件夹操作

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

```js 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方法创建文件时,可以传入绝对路径,也能传入相对路径

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

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

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

js 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')

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

3. 文件夹重命名

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

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

文件夹的复制案例

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

js 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为后缀名的文件都被拷贝过去了,说明我们的程序没有问题

Events模块

events基础方法

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

  • 在这个体系中,你某些对象(发射器(Emitters))发出某一个事件
  • 我们可以监听这个事件(监听器(Listeners)),并且传入的回调函数会在事件被触发时调用

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

  • emitter.on(eventName, listener):监听事件,也可以使用addListener
  • emitter.off(eventName, listener):移除监听事件,也可以使用removeListener
  • emitter.emit(eventName[, ...args]):发出事件,可以携带一些参数

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

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

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

  • 可以同时监听多个相同的事件,绑定的函数都会被执行
  • 发射事件的时候可以携带多个参数,在监听的回调函数中,我们可以用...剩余运算符来将他们集中到一个变量中
  • 如果绑定的都是相同的事件,那么触发的时候按照监听的顺序来执行,下面代码就是先执行“我被点击了1”,然后再执行“我被点击了2”

```js // 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里面的函数改写一下,然后再将第二个注册事件的回调函数抽离出去,打印结果会发生什么变化呢?

```js // 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取消了第二个注册事件的后,下次发射相同事件时,第二个事件就不会再被触发了

events获取信息

  1. emitter.eventNames(eventName)返回一个列出触发器已注册监听器的事件的数组
  2. emitter.listenerCount(eventName)返回正在监听名为 eventName的事件的监听器的数量
  3. emitter.listeners(eventName) 返回名为 eventName的事件的监听器数组的副本

``` 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中不常用方法

  1. emitter.once绑定的事件只监听一次。添加一个单次 listener 函数到名为 eventName 的事件。 下次触发 eventName 事件时,监听器会被移除,然后再调用

js 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)

  1. emitter.prependListener将监听事件添加到最前面,但是添加 listener 函数到名为 eventName 的事件的监听器数组的开头。 不会检查 listener 是否已被添加。多次调用并传入相同的 eventNamelistener 会导致 listener 被添加与调用多次

js 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)

  1. mitter.prependOnceListener:将监听事件添加到最前面,但是只监听一次
  2. emitter.removeAllListeners([eventName]) 移除全部或指定 eventName 的监听器;注意,在代码中移除其他地方添加的监听器是一个不好的做法,尤其是当 EventEmitter 实例是其他组件或模块(如 socket 或文件流)创建的。