• 有用的内置Node.js APIs


    前言

    在构建你的第一个Node.js应用程序时,了解node开箱即用的实用工具和API是很有帮助的,可以帮助解决常见的用例和开发需求。

    有用的Node.js APIs

    • Process:检索有关环境变量、参数、CPU使用情况和报告的信息。
    • OS:检索Node正在运行的操作系统和系统相关信息。比如CPU、操作系统版本、主目录等等。
    • Util:有用和常见方法的集合。用于帮助解码文本、类型检查和对比对象。
    • URL:轻松创建和解析URL。
    • File System API:与文件系统交互。用于创建、读取、更新以及删除文件、目录和权限。
    • Events:用于触发和订阅Node.js中的事件。其工作原理与客户端事件监听器类似。
    • Streams:用于在更小和更容易管理的块中处理大量数据,以避免内存问题。
    • Worker Threads:用来分离不同线程上的函数执行,以避免瓶颈。对于CPU密集型的JavaScript操作很有用。
    • Child Processes:允许你运行子进程,你可以监控并在必要时终止子进程。
    • Clusters:允许你跨核fork任何数量的相同进程,以更有效地处理负载。

    Process

    process对象提供有关你的Node.js应用程序以及控制方法的信息。可以使用该对象获取诸如环境变量、CPU和内存使用情况等信息。process是全局可用的:你可以在不import的情况下使用它。尽管Node.js文档推荐你显示地引用:

    import process from 'process';
    
    • process.argv:返回一个数组。该数组的前两个元素是Node.js的可执行路径和脚本名称。索引为2的数组项是传递的第一个参数。
    • process.env:返回包含环境名称与值的键值对对象。比如process.env.NODE_ENV
    • process.cwd():返回当前的工作目录。
    • process.platform:返回一个识别操作系统的字符串:'aix''darwin' (macOS),'freebsd''linux''openbsd''sunos',或者'win32' (Windows)。
    • process.uptime():返回Node.js进程已运行的秒数。
    • process.cpuUsage():返回当前进程的用户和系统CPU时间的使用情况--例如{ user: 12345, system: 9876 }。将该对象传给该方法,以获得一个相对的读数。
    • process.memoryUsage():返回一个以字节为单位描述内存使用情况的对象。
    • process.version:返回Node.js版本的字符串。比如18.0.0
    • process.report:生成诊断报告
    • process.exit(code):退出当前应用程序。使用退出码0来表示成功,或在必要时使用适当的错误代码

    OS

    [OS](https://nodejs.org/dist/latest/docs/api/os.html)API与process类似。但它也可以返回有关Node.js运行的操作系统的信息。它提供了诸如操作系统版本、CPU和启动时间等信息。

    • os.cpus():返回一个包含每个逻辑CPU核信息的对象数组。Clusters部分引用os.cpus()来fork进程。在一个16核CPU中,你会有16个Node.js应用程序的实例在运行以提高性能。
    • os.hostname():操作系统主机名。
    • os.version():标识操作系统内核版本的字符串。
    • os.homedir():用户主目录的完整路径。
    • os.tmpdir():操作系统默认临时文件目录的完整路径。
    • os.uptime():操作系统已运行的秒数。

    Util

    util模块提供了各种有用的JavaScript方法。其中最有用的是util.promisify(function),该方法接收错误优先类型的回调函数,并返回基于promise的函数。Util模块还可以帮助处理一些常见模式,诸如解码文本、类型检查和检查对象。

    • util.callbackify(function):接收一个返回promise的函数,并返回一个基于回调的函数。

    • util.isDeepStrictEqual(object1, object2):当两个对象严格相等(所有子属性必须匹配)时返回true

    • util.format(format, [args]):返回一个使用类printf格式的字符串。

    • util.inspect(object, options):返回一个对象的字符串表示,用于调试。与使用console.dir(object, { depth: null, color: true });类似。

    • util.stripVTControlCharacters(str):剥离字符串中的ANSI转义代码。

    • util.types:为常用的JavaScript和Node.js值提供类型检查。比如:

      import util from 'util';
      
      util.types.isDate( new Date() ); // true
      util.types.isMap( new Map() );  // true
      util.types.isRegExp( /abc/ ); // true
      util.types.isAsyncFunction( async () => {} ); // true
      

    URL

    URL是另一个全局对象,可以让你安全地创建、解析以及修改web URL。它对于从URL中快速提取协议、端口、参数和哈希值非常有用,而不需要借助于正则。比如:

    {
      href: 'https://example.org:8000/path/?abc=123#target',
      origin: 'https://example.org:8000',
      protocol: 'https:',
      username: '',
      password: '',
      host: 'example.org:8000',
      hostname: 'example.org',
      port: '8000',
      pathname: '/path/',
      search: '?abc=123',
      searchParams: URLSearchParams { 'abc' => '123' },
      hash: '#target'
    }
    

    你可以查看并更改任意属性。比如:

    myURL.port = 8001;
    console.log( myURL.href );
    // https://example.org:8001/path/?abc=123#target
    

    然后可以使用URLSearchParams API修改查询字符串值。比如:

    myURL.searchParams.delete('abc');
    myURL.searchParams.append('xyz', 987);
    console.log( myURL.search );
    // ?xyz=987
    

    还有一些方法可以将文件系统路径转换为URL,然后再转换回来。

    dns模块提供名称解析功能,因此你可以查询IP地址、名称服务器、TXT记录和其他域名信息。

    File System API

    fs API可以创建、读取、更新以及删除文件、目录以及权限。最近发布的Node.js运行时在fs/promises中提供了基于promise的函数,这使得管理异步文件操作更加容易。

    你将经常把fspath结合起来使用,以解决不同操作系统上的文件名问题。

    下面的例子模块使用stataccess方法返回一个有关文件系统对象的信息:

    // fetch file information
    import { constants as fsConstants } from 'fs';
    import { access, stat } from 'fs/promises';
    
    export async function getFileInfo(file) {
    
      const fileInfo = {};
    
      try {
        const info = await stat(file);
        fileInfo.isFile = info.isFile();
        fileInfo.isDir = info.isDirectory();
      }
      catch (e) {
        return { new: true };
      }
    
      try {
        await access(file, fsConstants.R_OK);
        fileInfo.canRead = true;
      }
      catch (e) {}
    
      try {
        await access(file, fsConstants.W_OK);
        fileInfo.canWrite = true;
      }
      catch (e) {}
    
      return fileInfo;
    
    }
    

    当传递一个文件名时,该函数返回一个包含该文件信息的对象。比如:

    {
      isFile: true,
      isDir: false,
      canRead: true,
      canWrite: true
    }
    

    filecompress.js主脚本使用path.resolve()将命令行上传递的输入和输出文件名解析为绝对文件路径,然后使用上面的getFileInfo()获取信息:

    #!/usr/bin/env node
    import path from 'path';
    import { readFile, writeFile } from 'fs/promises';
    import { getFileInfo } from './lib/fileinfo.js';
    
    // check files
    let
      input = path.resolve(process.argv[2] || ''),
      output = path.resolve(process.argv[3] || ''),
      [ inputInfo, outputInfo ] = await Promise.all([ getFileInfo(input), getFileInfo(output) ]),
      error = [];
    

    上述代码用于验证路径,必要时以错误信息终止:

    // use input file name when output is a directory
    if (outputInfo.isDir && outputInfo.canWrite && inputInfo.isFile) {
      output = path.resolve(output, path.basename(input));
    }
    
    // check for errors
    if (!inputInfo.isFile || !inputInfo.canRead) error.push(`cannot read input file ${ input }`);
    if (input === output) error.push('input and output files cannot be the same');
    
    if (error.length) {
    
      console.log('Usage: ./filecompress.js [input file] [output file|dir]');
      console.error('\n  ' + error.join('\n  '));
      process.exit(1);
    
    }
    

    然后用readFile()将整个文件读成一个名为content的字符串:

    // read file
    console.log(`processing ${ input }`);
    let content;
    
    try {
      content = await readFile(input, { encoding: 'utf8' });
    }
    catch (e) {
      console.log(e);
      process.exit(1);
    }
    
    let lengthOrig = content.length;
    console.log(`file size  ${ lengthOrig }`);
    

    然后JavaScript正则表达式会删除注释和空格:

    // compress content
    content = content
      .replace(/\n\s+/g, '\n')                // trim leading space from lines
      .replace(/\/\/.*?\n/g, '')              // remove inline // comments
      .replace(/\s+/g, ' ')                   // remove whitespace
      .replace(/\/\*.*?\*\//g, '')            // remove /* comments */
      .replace(//g, '')             // remove 
      .replace(/\s*([<>(){}}[\]])\s*/g, '$1') // remove space around brackets
      .trim();
    
    let lengthNew = content.length;
    

    产生的字符串用writeFile()输出到一个文件,并有一个状态信息展示保存情况:

    let lengthNew = content.length;
    
    // write file
    console.log(`outputting ${output}`);
    console.log(`file size  ${ lengthNew } - saved ${ Math.round((lengthOrig - lengthNew) / lengthOrig * 100) }%`);
    
    try {
      content = await writeFile(output, content);
    }
    catch (e) {
      console.log(e);
      process.exit(1);
    }
    

    使用示例HTML文件运行项目代码:

    node filecompress.js ./test/example.html ./test/output.html
    

    Events

    当发生一些事情时,你经常需要执行多个函数。比如说,一个用户注册你的app,因此代码必须添加新用户的详情到数据库中,开启一个新登录会话,并发送一个欢迎邮件。

    // example pseudo code
    async function userRegister(name, email, password) {
    
      try {
    
        await dbAddUser(name, email, password);
        await new UserSession(email);
        await emailRegister(name, email);
    
      }
      catch (e) {
        // handle error
      }
    
    }
    

    这一系列的函数调用与用户注册紧密相连。进一步的活动会引起进一步的函数调用。比如说:

    // updated pseudo code
    try {
    
      await dbAddUser(name, email, password);
      await new UserSession(email);
      await emailRegister(name, email);
    
      await crmRegister(name, email); // register on customer system
      await emailSales(name, email);  // alert sales team
    
    }
    

    你可以在这个单一的、不断增长的代码块中管理几十个调用。

    Events API提供了一种使用发布订阅模式构造代码的替代方式。userRegister()函数可以在用户的数据库记录被创建后触发一个事件--也许名为newuser

    任意数量的事件处理函数都可以订阅和响应newuser事件;这不需要改变userRegister()函数。每个处理器都是独立运行的,所以它们可以按任意顺序执行。

    客户端JavaScript中的事件

    事件和处理函数经常在客户端JavaScript中使用。比如说,当用户点击一个元素时运行函数:

    // client-side JS click handler
      document.getElementById('myelement').addEventListener('click', e => {
      
        // output information about the event
        console.dir(e);
      
      });
    

    在大多数情况下,你要为用户或浏览器事件附加处理器,尽管你可以提出你自己的自定义事件。Node.js的事件处理在概念上是相似的,但API是不同的。

    发出事件的对象必须是Node.js EventEmitter类的实例。这些对象有一个emit()方法来引发新的事件,还有一个on()方法来附加处理器。

    事件示例项目提供了一个类,该类可以在预定的时间间隔内触发一个tick事件。./lib/ticker.js模块导出一个default class,并extends EventEmitter

    // emits a 'tick' event every interval
    import EventEmitter from 'events';
    import { setInterval, clearInterval } from 'timers';
    
    export default class extends EventEmitter {
    

    constructor必须调用父构造函数。然后传递delay参数到start()方法:

    constructor(delay) {
      super();
      this.start(delay);
    }
    

    start()方法检查delay是否有效,如有必要会重置当前的计时器,并设置新的delay属性:

    start(delay) {
    
      if (!delay || delay == this.delay) return;
    
      if (this.interval) {
        clearInterval(this.interval);
      }
    
      this.delay = delay;
    

    然后它启动一个新的间隔计时器,运行事件名称为"tick"emit()方法。该事件的订阅者会收到一个包含延迟值和Node.js应用程序启动后秒数的对象:

    // start timer
        this.interval = setInterval(() => {
    
          // raise event
          this.emit('tick', {
            delay:  this.delay,
            time:   performance.now()
          });
    
        }, this.delay);
    
      }
    
    }
    

    event.js入口脚本导入了该模块,并设置了一秒钟的delay时段(1000毫秒)。

    // create a ticker
    import Ticker from './lib/ticker.js';
    
    // trigger a new event every second
    const ticker = new Ticker(1000);
    

    它附加了每次tick事件发生时触发的处理函数:

    // add handler
    ticker.on('tick', e => {
      console.log('handler 1 tick!', e);
    });
    
    // add handler
    ticker.on('tick', e => {
      console.log('handler 2 tick!', e);
    });
    

    第三个处理器仅使用once()方法对第一个tick事件进行触发:

    // add handler
    ticker.once('tick', e => {
      console.log('handler 3 tick!', e);
    });
    

    最后,输出当前监听器的数量:

    // show number of listenersconsole.log(`listeners: ${ // show number of listeners
    console.log(`listeners: ${ ticker.listenerCount('tick') }`);
    

    使用node event.js运行代码。

    输出显示处理器3触发了一次,而处理器1和2在每个tick上运行,直到应用程序被终止。

    Streams

    上面的文件系统示例代码在输出最小化的结果之前将整个文件读入内存。如果文件大于可用的RAM怎么办?Node.js应用程序将以"内存不足(out of memory)"错误失败。

    解决方案是流。这将在更小、更容易管理的块中处理传入的数据。流可以做到:

    • 可读:从文件、HTTP请求、TCP套接字、标准输入等读取。
    • 可写:写入到文件、HTTP响应、TCP套接字、标准输出等。
    • 双工:既可读又可写的流。
    • 转换:转换数据的双工流。

    每块数据都以Buffer对象的形式返回,它代表一个固定长度的字节序列。你可能需要将其转换为字符串或其他适当的类型进行处理。

    该示例代码有一个filestream项目,它使用一个转换流来解决filecompress项目中的文件大小问题。和以前一样,它在声明一个继承TransformCompress类之前,接受并验证了输入和输出的文件名:

    import { createReadStream, createWriteStream } from 'fs';
    import { Transform } from 'stream';
    
    // compression Transform
    class Compress extends Transform {
    
      constructor(opts) {
        super(opts);
        this.chunks = 0;
        this.lengthOrig = 0;
        this.lengthNew = 0;
      }
    
      _transform(chunk, encoding, callback) {
    
        const
          data = chunk.toString(),                  // buffer to string
          content = data
            .replace(/\n\s+/g, '\n')                // trim leading spaces
            .replace(/\/\/.*?\n/g, '')              // remove // comments
            .replace(/\s+/g, ' ')                   // remove whitespace
            .replace(/\/\*.*?\*\//g, '')            // remove /* comments */
            .replace(//g, '')             // remove 
            .replace(/\s*([<>(){}}[\]])\s*/g, '$1') // remove bracket spaces
            .trim();
    
        this.chunks++;
        this.lengthOrig += data.length;
        this.lengthNew += content.length;
    
        this.push( content );
        callback();
    
      }
    
    }
    

    当一个新的数据块准备好时,_transform方法被调用。它以Buffer对象的形式被接收,并被转换为字符串,被最小化,并使用push()方法输出。一旦数据块处理完成,一个callback()函数就会被调用。

    应用程序启动了文件读写流,并实例化了一个新的compress对象:

    // process streamconst  readStream = createReadStream(input),  wr// process stream
    const
      readStream = createReadStream(input),
      writeStream = createWriteStream(output),
      compress = new Compress();
    
    console.log(`processing ${ input }`)
    

    传入的文件读取流定义了.pipe()方法,这些方法通过一系列可能(或可能不)改变内容的函数将传入的数据输入。在输出到可写文件之前,数据通过compress转换进行管道输送。一旦流结束,最终on('finish')事件处理函数就会执行:

    readStream.pipe(compress).pipe(writeStream).on('finish', () => {
      console.log(`file size  ${ compress.lengthOrig }`);  console.log(`output     ${ output }`);  console.log(`chunks     readStream.pipe(compress).pipe(writeStream).on('finish', () => {
    
      console.log(`file size  ${ compress.lengthOrig }`);
      console.log(`output     ${ output }`);
      console.log(`chunks     ${ compress.chunks }`);
      console.log(`file size  ${ compress.lengthNew } - saved ${ Math.round((compress.lengthOrig - compress.lengthNew) / compress.lengthOrig * 100) }%`);
    
    });
    

    使用任意大小的HTML文件的例子运行项目代码:

    node filestream.js ./test/example.html ./test/output.html
    

    filestream.png

    这是对Node.js流的一个小例子。流处理是一个复杂的话题,你可能不经常使用它们。在某些情况下,像Express这样的模块在引擎盖下使用流,但对你的复杂性进行了抽象。

    你还应该注意到数据分块的挑战。一个块可以是任何大小,并以不便的方式分割传入的数据。考虑对这段代码最小化:

    <script type="module">
      // example script
      console.log('loaded');
    script>
    

    两个数据块可以依次到达:

    <script type="module">
    // example
    

    以及:

    <script>
      console.log('loaded');
    script>
    

    独立处理每个块的结果是以下无效的最小化脚本:

    <script type="module">script console.log('loaded');script>
    

    解决办法是预先解析每个块,并将其分割成可以处理的整个部分。在某些情况下,块(或块的一部分)将被添加到下一个块的开始。

    尽管会出现额外的复杂情况,但是最好将最小化应用于整行。因为/* */注释可以跨越不止一行。下面是每个传入块的可能算法:

    1. 将先前块中保存的任何数据追加到新块的开头。
    2. 从数据块中移除任意整个 以及 /* 到 */部分。
    3. 将剩余块分为两部分。其中part2以发现的第一个