前言

在构建你的第一个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() ); // trueutil.types.isMap( new Map() );  // trueutil.types.isRegExp( /abc/ ); // trueutil.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 informationimport { 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 nodeimport path from 'path';import { readFile, writeFile } from 'fs/promises';import { getFileInfo } from './lib/fileinfo.js';// check fileslet  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 directoryif (outputInfo.isDir && outputInfo.canWrite && inputInfo.isFile) {  output = path.resolve(output, path.basename(input));}// check for errorsif (!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 fileconsole.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 contentcontent = 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 <!-- comments -->  .replace(/\s*([<>(){}}[\]])\s*/g, '$1') // remove space around brackets  .trim();let lengthNew = content.length;

产生的字符串用writeFile()输入到一个文件,并有一个状态信息展现保留状况:

let lengthNew = content.length;// write fileconsole.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 codeasync 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 codetry {  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 intervalimport 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 tickerimport Ticker from './lib/ticker.js';// trigger a new event every secondconst ticker = new Ticker(1000);

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

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

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

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

最初,输入以后监听器的数量:

// show number of listenersconsole.log(`listeners: ${ // show number of listenersconsole.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 Transformclass 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 <!-- comments -->        .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 streamconst  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

这是对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以发现的第一个<!--/*开始。如果两者都存在,则从part2中删除除该符号以外的其余内容。如果两者都没有找到,则在最初一个回车符处进行宰割。如果没有找到,将part1设为空字符串,part2设为整个块。如果part2变得十分大--兴许超过100,000个字符,因为没有回车符--将part2追加到part1,并将part2设为空字符串。这将确保被保留的局部不会有限地增长。
  4. 放大和输入part1
  5. 保留part2(它被增加到下一个块的开始)。

该过程对每个传入的数据块都会再次运行。

Worker Threads

官网文档是这么说的:Workers(线程)对于执行CPU密集型的JavaScript操作很有用。它们对I/O密集型的工作帮忙不大。Node.js内置的异步I/O操作比Workers的效率更高。

假如一个用户能够在你的Express应用程序中触发一个简单的、十秒钟的JavaScript计算。该计算将成为一个瓶颈,使所有用户的处理程序进行。你的应用程序不能解决任何申请或运行其余性能,除非它计算实现。

异步计算

解决来自文件或数据库数据的简单计算可能问题不大,因为每个阶段在期待数据达到时都是异步运行。数据处理产生在事件循环的不同迭代中。

然而,仅用JavaScript编写的长运行计算,比方图像处理或机器学习算法,将占用事件循环的以后迭代。

一种解决方案就是worker线程。这相似于浏览器的web worker以及在独立线程上启动JavaScript过程。主线程和worker线程能够替换信息来触发或者终止程序。

Workers和事件循环

Workers对CPU密集型JavaScript操作很有用,只管Node.js的主事件循环仍利用于异步I/O流动。

示例代码有一个worker我的项目,其在lib/dice.js中导出diceRun()函数。这是将任意数量的N面骰子投掷若干次,并记录总分的计数(应该是正态分布曲线的后果):

// dice throwingexport function diceRun(runs = 1, dice = 2, sides = 6) {  const stat = [];  while (runs > 0) {    let sum = 0;    for (let d = dice; d > 0; d--) {      sum += Math.floor( Math.random() * sides ) + 1;    }    stat[sum] = (stat[sum] || 0) + 1;    runs--;  }  return stat;}

index.js中的代码启动一个过程,每秒钟运行一次并输入一条信息:

// run process every secondconst timer = setInterval(() => {  console.log('  another process');}, 1000);

调用diceRun()函数,将两个骰子抛出10亿次:

import { diceRun } from './lib/dice.js';// throw 2 dice 1 billion timesconst  numberOfDice = 2,  runs = 999_999_999;const stat1 = diceRun(runs, numberOfDice);

这将暂停计时器,因为Node.js事件循环在计算实现之前不能持续下一次迭代。

而后,将上述代码在一个新的Worker中尝试雷同的计算。这会加载一个名为worker.js的脚本,并在配置对象上的workerData属性传递计算参数:

import { Worker } from 'worker_threads';const worker = new Worker('./worker.js', { workerData: { runs, numberOfDice } });

事件处理器被附加到运行worker.js脚本的worker对象上,以便它能接管传入的后果:

// result returnedworker.on('message', result => {  console.table(result);});

以及处理错误:

// worker errorworker.on('error', e => {  console.log(e);});

以及在解决实现后进行整顿:

// worker completeworker.on('exit', code => {  // tidy up});

worker.js脚本启动diceRun()计算,并在计算实现后向父脚本公布一条音讯--该音讯由下面的message处理器接管:

// worker threadimport { workerData, parentPort } from 'worker_threads';import { diceRun } from './lib/dice.js';// worker threadimport { workerData, parentPort } from 'worker_threads';import { diceRun } from './lib/dice.js';// start calculationconst stat = diceRun( workerData.runs, workerData.numberOfDice );// post message to parent scriptparentPort.postMessage( stat );

worker运行时,计时器并没有暂停,因为它是在另一个CPU线程上执行的。换句话说,Node.js的事件循环持续迭代,而没有长提早。

应用node index.js运行我的项目代码。

你应该留神到了,基于worker的计算运行速度稍快,因为线程齐全专用于该过程。如果你的应用程序中遇到性能瓶颈,请思考应用worker

Child Processes

有时须要调用那些不是用Node.js编写的或者有失败危险的应用程序。

实在案例

我写过一个Express应用程序,该程序生成了一个含糊的图像哈希值,用于辨认相似的图形。它以异步形式运行,并且运行良好,直到有人上传了一个蕴含循环援用的畸形GIF(动画帧A援用了帧B,而帧B援用了帧A)。

哈希值的计算永不完结。该用户放弃了并尝试再次上传。一次又一次。整个应用程序最终因内存谬误而解体。

该问题通过在子过程中运行散列算法最终被解决。Express应用程序保持稳定,因为它启动、监控并在计算工夫过长时终止了计算。

child process API容许你运行子过程,如有必要你能够监控并终止。这里有三个选项:

  • spawn:生成子过程。
  • fork:非凡类型的spawn,能够启动一个新的Node.js过程。
  • exec:生成shell并运行一条命令。运行后果被缓冲,当进行完结时返回一个回调函数。

不像worker线程,子过程独立于Node.js主脚本,并且无法访问雷同的内存。

Clusters

当你的Node.js应用程序在单核上运行时,你的64核服务器CPU是否没有失去充分利用?Cluster容许你fork任何数量的雷同过程来更无效地解决负载。

对于os.cpus()返回的每个CPU,初始的主过程可能会fork本人一次。当一个过程失败时,它也能够解决重启,并在fork的过程之间代理通信信息。

集群的工作成果惊人,但你的代码可能变得复杂。更简略和更弱小的抉择包含:

  • 过程管理器比方PM2,它提供了一个主动集群模式
  • 容器管理系统,如Docker或Kubernetes

都能够启动、监控和重启同一个Node.js应用程序的多个独立实例。即便有一个失败了,该应用程序也会放弃活动状态。

总结

本文提供了一个比拟有用的Node.js API的例子,但我激励你浏览文档,本人去发现它们。文档总体上是好的,并展现了简略的例子,但它在某些中央可能是简略的。

以上就是本文的所有内容,如果对你有所帮忙,欢送点赞珍藏转发~

  • 本文译自:https://www.sitepoint.com/use...
  • 作者:Craig Buckler