Published on

Node 中的一些基本模块

Authors
  • avatar
    Name
    Et cetera
    Twitter

OS 模块

import * as os from 'os'

// arch 返回一个字符串,表明 Node.js 二进制编译所用的 操作系统CPU架构
console.log(os.arch())
// cpus 返回一个对象数组,包含每个逻辑 CPU 内核的信息
console.log(os.cpus())
// homedir 返回当前用户的主目录的字符串路径
console.log(os.homedir())
// tmpdir 返回操作系统的默认临时文件夹
console.log(os.tmpdir())

Path 模块

import * as path from 'path'

// dirname 返回 path 的目录名 (文件夹)
path.dirname('/foo/bar/baz/asdf/quux')
console.log(path.dirname('/foo/bar/baz/asdf/quux'))

// basename 返回 path 的最后一部分 (文件名)
path.basename('/foo/bar/baz/asdf/quux.html')

const basePath = 'a/b'
// join 方法使用平台特定的分隔符把全部给定的 path 片段连接到一起,并规范化生成的路径
console.log(path.join(basePath, 'c', 'd'))

// resolve 方法把一个路径或路径片段的序列解析为一个绝对路径
console.log(path.resolve(__dirname, 'a', 'b', 'c', 'd'))

// __dirname 返回当前模块的目录名(类似于 __filename 的 path.dirname() 的值),或者理解为 pwd 命令的结果
console.log(__dirname)
// __filename 返回当前模块的文件名。这是当前模块的绝对路径(符号链接会被解析)
console.log(__filename)

url 模块

import * as URL from 'url'

// URL 类用于解析和格式化 URL
const url = new URL.URL('https://example.org/?a=1')
console.log(url)
// Output:
// URL {
//   href: 'https://example.org/?a=1',
//   origin: 'https://example.org',
//   protocol: 'https:',
//   username: '',
//   password: '',
//   host: 'example.org',
//   hostname: 'example.org',
//   port: '',
//   pathname: '/',
//   search: '?a=1',
//   searchParams: URLSearchParams { 'a' => '1' },
//   hash: ''
// }

// URLSearchParams 类定义了一些实用的方法来处理 URL 的查询字符串,get 方法返回键值对列表中与给定搜索参数关联的第一个值
console.log(url.searchParams.get('a'))
// set 方法设置键值对列表中与给定搜索参数关联的第一个值
url.searchParams.set('b', '2')
console.log(url.searchParams.get('b'))

// URL 类的静态方法 resolve(from, to) 方法会解析 from 和 to 参数为绝对路径,类似于执行 cd 命令
URL.resolve('/one/two/three', 'four') // '/one/two/four'

const urlObject = {
  protocol: 'https:',
  slashes: true,
  hostname: 'www.example.com',
  pathname: '/search',
  search: '?q=node.js',
}
URL.format(urlObject) // https://www.example.com/search?q=node.js

fs 模块

import * as path from 'path'
import * as fs from 'fs'

// 如果该目录下没有该文件,会自动创建,但是如果该目录不存在,会报错
const filename = path.resolve(__dirname, './test.ts')
// fs.readFile() 是异步函数用于读取文件,不指定编码格式时返回原始的 buffer
// fs.readFile(filename, 'utf-8', (err, data) => {
//   console.log(data)
// })

// sync 函数是同步的,会阻塞后面的代码,等待文件读取完成后才执行, 一般不使用
// 通常在程序启动时,运行有限的次数,不会对性能造成影响
// const content = fs.readFileSync(filename, 'utf-8')
// console.log(content)

// 推荐使用 promise 的方式
// node 现在在 promises 中单独提供了 promise 的 API,不需要引入第三方库
const test = async () => {
  // 这样直接调用会覆盖原来的内容
  // await fs.promises.writeFile(filename, 'aimyon')
  // 为了不覆盖原来的内容,需要加上 flag
  await fs.promises.writeFile(filename, 'aimyon', {
    flag: 'a', // 表示 append 追加写入
  })

  // 同样可以写入 buffer
  const buffer = Buffer.from('taka', 'utf-8')
  await fs.promises.writeFile(filename, buffer)
  console.log('文件写入成功')

  const content = await fs.promises.readFile(filename, 'utf-8')
  console.log(content)
}

// 手动复制文件
const copy = async () => {
  const fromFilename = path.resolve(__dirname, './oor.txt')
  const buffer = await fs.promises.readFile(fromFilename)
  const toFilename = path.resolve(__dirname, './oor.copy.txt')
  await fs.promises.writeFile(toFilename, buffer)
  console.log('文件复制成功')
}

const getStat = async () => {
  const stat = await fs.promises.stat(filename)
  console.log(stat)
  // 其中还有 isDirectory()判断是否是目录/文件夹、isFile()是否是文件 方法
  // Stats {
  //   dev: 16777234,
  //   mode: 33188,
  //   nlink: 1,
  //   uid: 501,
  //   gid: 20,
  //   rdev: 0,
  //   blksize: 4096,
  //   ino: 74434423,
  //   size: 4,   // 占用字节
  //   blocks: 8,
  //   atimeMs: 1697959684291.214,
  //   mtimeMs: 1697959682037.0825,
  //   ctimeMs: 1697959682037.0825,
  //   birthtimeMs: 1697958645752.003,
  //   atime: 2023-10-22T07:28:04.291Z,  // 上次访问时间
  //   mtime: 2023-10-22T07:28:02.037Z,  // 上次文件内容修改时间
  //   ctime: 2023-10-22T07:28:02.037Z,  // 上次文件状态修改时间
  //   birthtime: 2023-10-22T07:10:45.752Z  // 文件创建时间
  // }
}

const dir = async () => {
  const res = await fs.promises.readdir(__dirname)
  // [ 'fs.ts', 'oor.copy.txt', 'oor.txt', 'test.ts' ]
  // 创建文件夹
  // await fs.promises.mkdir('test')
  // 删除文件夹
  await fs.promises.rmdir('test')
}

test()
copy()
dir()

console.log(filename)

文件 I/O

import { promises } from 'node:fs'
import path from 'node:path'

class File {
  public filename: string
  public name: string
  public ext: string
  public isFile: boolean
  public size: number
  public createTime: Date
  public updateTime: Date

  constructor(
    filename: string,
    name: string,
    ext: string,
    isFile: boolean,
    size: number,
    createTime: Date,
    updateTime: Date
  ) {
    this.filename = filename
    this.name = name
    this.ext = ext
    this.isFile = isFile
    this.size = size
    this.createTime = createTime
    this.updateTime = updateTime
  }

  async getContent(isBuffer = false) {
    if (this.isFile) {
      if (isBuffer) {
        return await promises.readFile(this.filename)
      } else {
        return await promises.readFile(this.filename, 'utf-8')
      }
    } else {
    }
  }

  async getChildren() {
    if (this.isFile) return []

    const children = await promises.readdir(this.filename)
    const childrenRes = children.map((name) => {
      const res = path.resolve(this.filename, name)

      return File.getFile(res)
    })

    return Promise.all(childrenRes)
  }

  static async getFile(filename: string) {
    const stat = await promises.stat(filename)
    const name = path.basename(filename)
    const ext = path.extname(filename)
    const isFile = stat.isFile()
    const size = stat.size
    const createTime = new Date(stat.birthtime)
    const updateTime = new Date(stat.mtime)

    return new File(filename, name, ext, isFile, size, createTime, updateTime)
  }
}

const readDir = async (dirname: string) => {
  const file = await File.getFile(dirname)

  return await file.getChildren()
}

const test = async () => {
  const filename = path.resolve(__dirname, './custom')
  const file = await readDir(filename)
  // 也支持循环调用
  // const child = await file[0].getChildren()
  console.log(file)
}

test()

文件流

node 中的 stream 模块提供了构建所有流 API 的基础。所有的流都是 EventEmitter 的实例。

什么是流?

  • 流是指数据的流动,数据从一个地方缓缓的流动到另一个地方,就像水流一样。

  • 流是一种抽象的数据结构,用于表示有序的字节数据序列。流可以是可读的(可读流)、可写的(可写流)、或者可读可写的(双工流)。所有的流都是 EventEmitter 的实例。

流是有方向的

  • 可读流(Readable):从文件中读取数据到内存中

  • 可写流(Writable):从内存中写入数据到文件中

  • 双工流(Duplex):可读可写

为什么需要流?

  • 其他介质和内存的数据规模不一致

  • 其他介质和内存的数据处理能力不一致

可读流

fs.createReadStream(path[, options]): 创建一个文件可读流,用于读取文件。(返回:Readable 的子类 ReadStream)

文件流的读取部分(有节省内存)

import fs from 'node:fs'
import path from 'node:path'

const filename = path.resolve(__dirname, './oor.txt')
const rs = fs.createReadStream(filename, {
  encoding: 'utf-8',
  highWaterMark: 1, // 每次读取数量,如果 encoding 有值,该数量表示一个字符数;如果 encoding 为 null,则表示字节数
  autoClose: true, // 读完后自动关闭
})

rs.on('open', () => {
  console.log('file open')
})

rs.on('error', () => {
  console.log('file error')
})

rs.on('close', () => {
  console.log('file close')
})

// 只有注册了 data 事件才会开始读
rs.on('data', (chunk) => {
  console.log('file data', chunk)
  rs.pause() // 会触发 pause 事件
})

rs.on('pause', () => {
  console.log('file pause')
  setTimeout(() => {
    rs.resume() // 会触发 resume 事件
  }, 1000)
})

rs.on('resume', () => {
  console.log('file resume')
})

rs.on('end', () => {
  console.log('file read end')
})

可写流

import fs from 'node:fs'
import path from 'node:path'

const filename = path.resolve(__dirname, './oor.txt')

const ws = fs.createWriteStream(filename, {
  encoding: 'utf-8',
  highWaterMark: 2, // 默认 16 kb
})

// writeStream 中也有事件
// open close error,同样通过 on 来监听

// 返回值是一个 boolean,
// true 表示写入通道没有被填满,接下来的数据可以直接写入
// false 表示通道已经被填满,不能直接写入
// const flag = ws.write('a')
// console.log(flag)

// 背压问题,因为写入队列是内存中的数据,内存是很有限的
// for (let i = 0; i < 1024 * 1024; i++) {
//   ws.write(Buffer.from('a'))
// }

// let i = 0
//
// // 一直写,知道到上限为止
// function write() {
//   let flag = true
//   while (i < 1024 * 1024 && flag) {
//     // 写入 a 得到下一次还能不能直接写入
//     flag = ws.write('a')
//     i++
//   }
// }
//
// write()

// 当写入队列清空时,会触发 drain 事件
// ws.on('drain', () => {
//   // 相较之前对内存压力更低
//   write()
//   console.log('可以继续写')
// })

读取一个文件,并复制内容到另外一个文件

import path from 'node:path'
import fs from 'node:fs'

// 方式一
async function copy() {
  const from = path.resolve(__dirname, './oor.txt')
  const to = path.resolve(__dirname, './oor2.copy.txt')

  console.time('>>>1')
  const content = await fs.promises.readFile(from)

  await fs.promises.writeFile(to, content)
  console.timeEnd('>>>1')
  console.log('completed')
}

copy()

// 方式二
async function copy2() {
  const from = path.resolve(__dirname, './oor.txt')
  const to = path.resolve(__dirname, './oor3.copy.txt')

  console.time('>>>2')
  const rs = fs.createReadStream(from)
  const ws = fs.createWriteStream(to)

  rs.on('data', (chunk) => {
    const flag = ws.write(chunk)

    if (!flag) {
      // 表示会造成背压
      rs.pause()
    }
  })

  ws.on('drain', () => {
    // 监控暂停,然后继续读写
    rs.resume()
  })

  rs.on('close', () => {
    //   写完
    ws.end()
    console.timeEnd('>>>2')
    console.log('completed')
  })
}

copy2()

// 方式三: 使用管道,同样可以解决背压问题
async function copy3() {
  const from = path.resolve(__dirname, './oor.txt')
  const to = path.resolve(__dirname, './oor4.copy.txt')

  console.time('>>>3')
  const rs = fs.createReadStream(from)
  const ws = fs.createWriteStream(to)

  // 将可读流连接到可写流
  rs.pipe(ws)
  rs.on('close', () => {
    console.timeEnd('>>>3')
    console.log('completed')
  })
}