前言
在构建你的第一个 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
的函数,这使得治理异步文件操作更加容易。
你将常常把 fs
和path
联合起来应用,以解决不同操作系统上的文件名问题。
上面的例子模块应用 stat
和access
办法返回一个无关文件系统对象的信息:
// 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 <!-- comments -->
.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
我的项目中的文件大小问题。和以前一样,它在申明一个继承 Transform
的Compress
类之前,承受并验证了输出和输入的文件名:
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 <!-- 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 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
这是对 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>
解决办法是事后解析每个块,并将其宰割成能够解决的整个局部。在某些状况下,块(或块的一部分)将被增加到下一个块的开始。
只管会呈现额定的简单状况,然而最好将最小化利用于整行。因为 <!-- -->
和/* */
正文能够逾越不止一行。上面是每个传入块的可能算法:
- 将先前块中保留的任何数据追加到新块的结尾。
- 从数据块中移除任意整个
<!--
到-->
以及/*
到*/
局部。 - 将残余块分为两局部。其中
part2
以发现的第一个<!--
或/*
开始。如果两者都存在,则从part2
中删除除该符号以外的其余内容。如果两者都没有找到,则在最初一个回车符处进行宰割。如果没有找到,将part1
设为空字符串,part2
设为整个块。如果part2
变得十分大 – 兴许超过 100,000 个字符,因为没有回车符 – 将part2
追加到part1
,并将part2
设为空字符串。这将确保被保留的局部不会有限地增长。 - 放大和输入
part1
。 - 保留
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 throwing
export 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 second
const timer = setInterval(() => {console.log('another process');
}, 1000);
调用 diceRun()
函数,将两个骰子抛出 10 亿次:
import {diceRun} from './lib/dice.js';
// throw 2 dice 1 billion times
const
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 returned
worker.on('message', result => {console.table(result);
});
以及处理错误:
// worker error
worker.on('error', e => {console.log(e);
});
以及在解决实现后进行整顿:
// worker complete
worker.on('exit', code => {// tidy up});
worker.js
脚本启动 diceRun()
计算,并在计算实现后向父脚本公布一条音讯 – 该音讯由下面的 message
处理器接管:
// worker threadimport {workerData, parentPort} from 'worker_threads';import {diceRun} from './lib/dice.js';
// worker thread
import {workerData, parentPort} from 'worker_threads';
import {diceRun} from './lib/dice.js';
// start calculation
const stat = diceRun(workerData.runs, workerData.numberOfDice);
// post message to parent script
parentPort.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