96SEO 2026-02-20 05:19 12
用于处理二进制数据的缓冲区类型。

这些初始部分之后是突出和演示一些最重要的
浏览器中console.log()、console.warn()
通常在开发者控制台中的输出旁边显示小图标以指示日志消息的种类。
Node
风格程序你会知道这些程序通常主要从命令行参数获取输入其次从环境变量获取输入。
console.log(process.argv);然后你可以执行该程序并看到如下输出
[/usr/local/bin/node,/private/tmp/argv.js,--arg1,--arg2,filename
可执行文件消耗不会出现在process.argv中。
在上面的示例中--trace-uncaught命令行参数实际上并没有做任何有用的事情它只是用来演示它不会出现在输出中。
任何出现在
文件名之后的参数如--arg1和filename将出现在process.argv中。
通过process.env对象使这些变量可用。
该对象的属性名称是环境变量名称属性值始终为字符串是这些变量的值。
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin,PWD:
--help来查找-p和-e命令行参数的作用。
然而作为提示注意你可以将上面的行重写为node
程序在执行文件中的最后一行代码后完成执行时退出。
然而通常情况下一个
程序将在执行初始文件后继续运行。
正如我们将在接下来的章节中讨论的那样Node
程序直到运行完初始文件并且所有回调都被调用且没有更多待处理事件时才会退出。
一个基于
的服务器程序监听传入的网络连接理论上会永远运行因为它总是会等待更多事件。
一个程序可以通过调用process.exit()来强制退出。
用户通常可以通过在运行程序的终端窗口中键入
程序。
程序可以通过注册一个信号处理程序函数process.on(SIGINT,
如果程序中的代码抛出异常而没有catch子句捕获它程序将打印堆栈跟踪并退出。
由于
的异步特性发生在回调或事件处理程序中的异常必须在本地处理或根本不处理这意味着处理程序中异步部分发生的异常可能是一个困难的问题。
如果你不希望这些异常导致程序完全崩溃注册一个全局处理程序函数将被调用而不是崩溃
process.setUncaughtExceptionCaptureCallback(e
被拒绝并且没有.catch()调用来处理它会出现类似的情况。
截至
13这不是导致程序退出的致命错误但会在控制台打印详细的错误消息。
在未来的某个
拒绝预计将成为致命错误。
如果你不希望未处理的拒绝打印错误消息或终止程序注册一个全局处理程序函数
的模块系统使用require()函数将值导入模块使用exports对象或module.exports属性从模块导出值。
这些是
模块”的支持。
这两种模块系统并不完全兼容因此这有点棘手。
Node
需要在加载模块之前知道该模块是否将使用require()和module.exports还是将使用import和export。
当
模块时它会自动定义require()函数以及标识符exports和module并且不会启用import和export关键字。
另一方面当
模块时它必须启用import和export声明并且不能定义额外的标识符如require、module和exports。
正在加载的模块的类型最简单的方法是将这些信息编码在文件扩展名中。
如果您将
模块加载期望它使用import和export并且不会提供require()函数。
如果您将代码保存在以.cjs*结尾的文件中那么
模块提供require()函数并且如果您使用import或export声明则会抛出
会在与文件相同的中查找。
一旦找到最近的package.json文件Node
对象中的顶级type属性。
如果type属性的值是“module”那么
程序当找不到这样的文件时或者找到文件但它没有type属性时Node
模块而不想使用.mjs文件扩展名时才需要使用这个package.json*技巧。
会在项目的根目录中的名为package.json的文件中跟踪这些依赖项以及关于您的程序的其他信息。
由
以获取更多深入信息。
我在这里提到它是因为除非您编写的程序不使用任何外部库否则您几乎肯定会使用
框架https://expressjs.com来简化任务。
要开始您可以为项目创建一个中输入npm
会询问您项目名称、版本号等信息然后根据您的回答创建一个初始的package.json文件。
库以及其所有依赖项并将所有包安装在本地*node_modules/*目录中
是一种通用编程语言因此完全可以编写乘法大矩阵或执行复杂统计分析等
的设计使得轻松实现高度并发的服务器成为可能可以同时处理许多请求。
不使用线程来实现并发。
多线程编程通常很难正确完成也很难调试。
此外线程是一个相对较重的抽象如果你想编写一个能够处理数百个并发请求的服务器使用数百个线程可能需要大量的内存。
因此Node
编程模型这实际上是一种巨大的简化使得创建网络服务器成为一种常规技能而不是一种神秘技能。
默认设置为异步和非阻塞来保持高并发水平同时保持单线程编程模型。
Node
非常认真地采取了非阻塞的方法甚至可能会让你感到惊讶。
你可能期望从网络读取和写入的函数是异步的但
更进一步为从本地文件系统读取和写入文件定义了非阻塞异步函数。
这是有道理的当你考虑到Node
是在旋转硬盘仍然是标准的时代设计的而在进行文件操作之前确实有毫秒级的阻塞“寻道时间”等待磁盘旋转以开始文件操作。
在现代数据中心所谓的“本地”文件系统实际上可能在网络的某个地方上面还有网络延迟。
但即使异步读取文件对你来说是正常的Node
仍然更进一步例如用于启动网络连接或查找文件修改时间的默认函数也是非阻塞的。
中有一些同步但非阻塞的函数它们运行完成并返回而无需阻塞。
但大多数有趣的函数执行某种输入或输出这些是异步函数因此它们可以避免甚至最微小的阻塞。
Node
是基于回调的。
如果你还没有阅读或已经忘记第十三章现在是回到那一章的好时机。
通常你传递给异步
错误优先回调通常用两个参数调用。
错误优先回调的第一个参数通常在没有错误发生的情况下为
null第二个参数是由你调用的原始异步函数产生的数据或响应。
将错误参数放在第一位的原因是为了让你无法忽略它你应该始终检查这个参数中是否有非空值。
如果它是一个错误对象甚至是一个整数错误代码或字符串错误消息那么出现了问题。
在这种情况下你回调函数的第二个参数可能是
以下代码演示了如何使用非阻塞的readFile()函数读取配置文件将其解析为
fileconsole.error(err);callback(null);return;}let
contentsconsole.error(e);}callback(data);});
promises但由于它在错误优先回调方面相当一致使用util.promisify()包装器可以轻松创建基于
的变体。
这是我们如何重写readConfigFile()函数以返回一个
的函数用于处理文件系统。
我们将在本章后面讨论它们但请注意在前面的代码中我们可以用fs.promises.readFile()替换pfs.readFile()。
确实定义了许多阻塞的同步变体函数特别是在文件系统模块中。
这些函数通常以Sync结尾的名称清晰标记。
当服务器首次启动并读取其配置文件时它尚未处理网络请求实际上几乎不可能发生并发。
因此在这种情况下没有必要避免阻塞我们可以安全地使用像fs.readFileSync()这样的阻塞函数。
我们可以从这段代码中删除async和await并编写我们的readConfigFile()函数的纯同步版本。
这个函数不是调用回调或返回
的内置非阻塞函数使用操作系统版本的回调和事件处理程序。
当您调用这些函数之一时Node
会采取行动启动操作然后向操作系统注册某种事件处理程序以便在操作完成时通知它。
您传递给
程序启动时它运行您告诉它运行的任何代码。
这段代码可能调用至少一个非阻塞函数导致注册回调或事件处理程序与操作系统。
到达程序末尾时它会阻塞直到发生事件此时操作系统再次启动它。
Node
函数导致注册更多的操作系统事件处理程序。
一旦您的回调函数运行完毕Node
密集型应用程序来说这种基于事件的并发方式是高效且有效的。
只要使用非阻塞
字符串互操作缓冲区中的字节可以从字符字符串初始化或转换为字符字符串。
字符编码将某个字符集中的每个字符映射到一个整数。
给定一个文本字符串和一个字符编码我们可以将字符串中的字符
为一系列字节。
给定一个正确编码的字节序列和一个字符编码我们可以将这些字节
类有执行编码和解码的方法你可以通过这些方法来识别因为它们期望一个
computer.subarray(0,3).map(xx1).toString()
类。
另一方面如果你只是处理从文件或网络读取或写入的文本那么你可能只会遇到
中传递一个字符串或者期望返回一个字符串你需要指定要使用的文本编码的名称。
如果你这样做了那么你可能根本不需要使用
API这种异步性采用的形式是两个参数的错误优先回调当请求的操作完成时调用。
但是一些更复杂的
设计围绕对象而不是函数时或者当需要多次调用回调函数时或者当可能需要多种类型的回调函数时通常会出现这种情况。
例如考虑
类这种类型的对象是一个服务器套接字用于接受来自客户端的传入连接。
当它首次开始监听连接时会发出“listening”事件每当客户端连接时会发出“connection”事件当关闭并不再监听时会发出“close”事件。
可以发出多种类型的事件事件类型通过名称标识。
要注册事件处理程序请调用
方法传递事件类型的名称以及当事件发生时应该调用的函数。
EventEmitters
});如果您更喜欢使用更明确的方法名称来注册事件侦听器也可以使用
上当前注册的所有处理程序函数来处理该类型的事件。
它们按照从第一个注册到最后注册的顺序依次调用。
如果有多个处理程序函数它们将在单个线程上依次调用请记住Node
中没有并行处理。
重要的是事件处理函数是同步调用的而不是异步调用的。
这意味着
方法不会将事件处理程序排队以在以后的某个时间调用。
emit()
会依次调用所有已注册的处理程序并且在最后一个事件处理程序返回之前不会返回。
这样的阻塞函数的事件处理程序直到同步文件读取完成将不会发生进一步的事件处理。
如果您的程序是一个需要响应的网络服务器之类的程序那么重要的是保持事件处理程序函数非阻塞和快速。
如果需要在事件发生时进行大量计算通常最好使用处理程序使用
setImmediate()它会在处理完所有挂起的回调和事件后立即调度一个函数。
类还定义了一个emit()方法导致注册的事件处理程序函数被调用。
如果您正在定义自己的基于事件的
进行编程时通常不常用。
emit()必须以事件类型的名称作为第一个参数调用。
传递给emit()的任何其他参数都成为注册的事件处理程序函数的参数。
处理程序函数还使用设置为
对象本身的this值调用这通常很方便。
请记住箭头函数总是使用定义它们的上下文的this值并且不能使用任何其他this值调用。
尽管如此箭头函数通常是编写事件处理程序的最方便方式。
事件处理程序函数返回的任何值都会被忽略。
但是如果事件处理程序函数抛出异常则它会从emit()调用中传播出去并阻止执行任何在抛出异常之后注册的处理程序函数。
使用错误优先回调重要的是您始终检查第一个回调参数以查看是否发生错误。
对于基于事件的
时您应该养成注册“error”事件处理程序的习惯。
“error”事件在
类中得到特殊处理。
如果调用emit()来发出“error”事件并且没有为该事件类型注册处理程序则将抛出异常。
由于这是异步发生的因此您无法在catch块中处理异常因此这种错误通常会导致程序退出。
在实现处理数据的算法时几乎总是最容易将所有数据读入内存进行处理然后将数据写出。
例如您可以编写一个
{fs.writeFile(destinationFilename,
}这个copyFile()函数使用异步函数和回调函数因此不会阻塞并适用于像服务器这样的并发程序。
但请注意它必须分配足够的内存来一次性容纳整个文件的内容。
在某些情况下这可能没问题但如果要复制的文件非常大或者您的程序高度并发且可能同时复制许多文件时它就会开始出现问题。
这个copyFile()实现的另一个缺点是它在完成读取旧文件之前无法开始写入新文件。
解决这些问题的方法是使用流算法其中数据“流”进入您的程序被处理然后流出您的程序。
思路是您的算法以小块处理数据完整数据集不会一次性保存在内存中。
当流式解决方案可行时它们更节省内存并且也可能更快。
Node
API。
我们将在“流动模式”中看到copyFile()函数的流式版本。
可读流是数据的来源。
例如由fs.createReadStream()返回的流是可以读取指定文件内容的流。
process.stdin是另一个可读流返回标准输入的数据。
可写流是数据的接收端或目的地。
例如fs.createWriteStream()
的返回值是一个可写流它允许以块的形式向其写入数据并将所有数据输出到指定的文件。
双工流将可读流和可写流合并为一个对象。
例如net.connect()
返回的对象都是双工流。
如果向套接字写入数据则数据将通过网络发送到套接字连接的计算机。
如果从套接字读取数据则可以访问另一台计算机写入的数据。
转换流也是可读和可写的但与双工流有一个重要的区别写入转换流的数据变得可读通常以某种转换形式从同一流中读取。
例如zlib.createGzip()
算法写入其中的数据。
类似地crypto.createCipheriv()
对象。
如果向可写缓冲区写入字符串它将自动使用缓冲区的默认编码或您指定的任何编码进行编码。
Node
还支持“对象模式”其中流读取和写入比缓冲区和字符串更复杂的对象。
Node
可读流必须从某处读取数据可写流必须将数据写入某处因此每个流都有两个端点一个输入和一个输出或者一个源和一个目的地。
基于流的
的棘手之处在于流的两端几乎总是以不同的速度流动。
也许从流中读取数据的代码想要比实际写入流中的数据更快地读取和处理数据。
或者反过来也许数据被写入流中的速度比从流的另一端读取和提取数据的速度更快。
流实现几乎总是包含一个内部缓冲区用于保存已写入但尚未读取的数据。
缓冲有助于确保在请求时有可读取的数据并且在写入数据时有空间可用于保存数据。
但是这两件事情都无法保证基于流的编程的本质是读取者有时必须等待数据被写入因为流缓冲区为空写入者有时必须等待数据被读取因为流缓冲区已满。
通常具有阻塞调用读取数据的调用在数据到达流之前不会返回写入数据的调用会阻塞直到流的内部缓冲区有足够的空间来容纳新数据。
然而在基于事件的并发模型中阻塞调用是没有意义的Node
通过事件协调流的可读性缓冲区不为空和可写性缓冲区不满的需求使得
有时您需要从流中读取数据然后将相同的数据写入另一个流。
例如想象一下您正在编写一个简单的
服务器用于提供静态文件目录。
在这种情况下您需要从文件输入流中读取数据并将其写入网络套接字。
但是您可以简单地将两个套接字连接在一起作为“管道”让
为您处理复杂性而不是编写自己的处理读取和写入的代码。
只需将可写流传递给可读流的pipe()方法
{fs.createReadStream(filename).pipe(socket);
}以下实用函数将一个流导向另一个流并在完成或发生错误时调用回调函数
{readable.close();writable.close();callback(err);}//
handleError).pipe(writable).on(error,
}转换流在管道中特别有用并创建涉及两个以上流的管道。
以下是一个压缩文件的示例函数
fs.createReadStream(filename);let
error.pipe(gzipper).pipe(destination).on(error,
}使用pipe()方法将数据从一个可读流复制到一个可写流很容易但在实践中通常需要以某种方式处理数据因为它在程序中流动。
一种方法是实现自己的转换流来进行处理这种方法允许您避免手动读取和写入流。
例如下面是一个类似
grep实用程序的函数它从输入流中读取文本行但只写入与指定正则表达式匹配的行
(this.pattern.test(this.incompleteLine))
GrepStream,.pipe(process.stdout)
及更高版本中可读流是异步迭代器这意味着在async函数中您可以使用for/await循环从流中读取字符串或缓冲区块使用的代码结构类似于同步代码。
有关异步迭代器和for/await循环的更多信息请参见§13.4。
使用异步迭代器几乎和使用pipe()方法一样简单当您需要以某种方式处理每个读取的块时可能更容易。
以下是我们如何使用async函数和for/await循环重写前一节中的grep程序的方法
Bufferssource.setEncoding(encoding);//
{destination.write(incompleteLine
exceptions.console.error(err);process.exit();});16.5.3
前面代码示例中的异步grep()函数演示了如何将可读流用作异步迭代器但它还演示了您可以通过将其传递给write()方法来简单地向可写流写入数据。
write()方法将缓冲区或字符串作为第一个参数。
对象流期望其他类型的对象但超出了本章的范围。
如果传递缓冲区则将直接写入该缓冲区的字节。
如果传递字符串则在写入之前将其编码为字节的缓冲区。
当您将字符串作为write()的唯一参数传递时可写流具有默认编码。
默认编码通常为“utf8”但您可以通过在可写流上调用setDefaultEncoding()来显式设置它。
或者当您将字符串作为write()的第一个参数传递时可以将编码名称作为第二个参数传递。
write()可选地将回调函数作为其第三个参数。
当数据实际写入并不再位于可写流的内部缓冲区中时将调用此函数。
如果发生错误也可能调用此回调但不能保证。
您应在可写流上注册“error”事件处理程序以检测错误。
write()方法具有非常重要的返回值。
当您在流上调用write()时它将始终接受并缓冲您传递的数据块。
如果内部缓冲区尚未满则返回true。
或者如果缓冲区现在已满或过满则返回false。
此返回值是建议性的您可以忽略它——如果您继续调用write()可写流将根据需要扩大其内部缓冲区。
但请记住首先使用流式
从write()方法返回false的返回值是一种背压形式流向你发送的消息表示你写入数据的速度比处理速度快。
对这种背压的正确响应是停止调用write()直到流发出“drain”事件表示缓冲区中再次有空间。
例如下面是一个向流写入数据的函数并在可以继续向流写入更多数据时调用回调函数
}有时可以连续调用write()多次有时必须在写入之间等待事件这导致算法变得笨拙。
这就是使用pipe()方法如此吸引人的原因之一当你使用pipe()时Node
如果你在程序中使用await和async并将可读流视为异步迭代器那么实现上面的write()实用程序的基于
的版本以正确处理背压是很简单的。
在我们刚刚看过的异步grep()函数中我们没有处理背压。
下面示例中的异步copy()函数演示了如何正确处理背压。
请注意此函数只是将源流中的块复制到目标流中并调用copy(source,
destination)就像调用source.pipe(destination)一样
process.stdout);在我们结束对流写入的讨论之前再次注意不响应背压可能导致程序使用的内存超出预期当可写流的内部缓冲区溢出并不断增大时。
如果你正在编写一个网络服务器这可能是一个远程可利用的安全问题。
假设你编写了一个通过网络传输文件的
服务器但你没有使用pipe()也没有花时间处理write()方法的背压。
攻击者可以编写一个
客户端发起对大文件如图像的请求但实际上从未读取请求的主体。
由于客户端没有从网络中读取数据而服务器也没有响应背压服务器上的缓冲区将会溢出。
如果攻击者有足够的并发连接这可能会演变成一个拒绝服务攻击使你的服务器变慢甚至崩溃。
API。
如果你的程序不能使用管道或异步迭代你将需要选择这两种基于事件的
在流动模式中当可读数据到达时它会立即以“data”事件的形式发出。
要在此模式下从流中读取数据只需为“data”事件注册事件处理程序流将在数据块缓冲区或字符串可用时将其推送给你。
请注意在流动模式下不需要调用read()方法你只需要处理“data”事件。
请注意新创建的流不会立即处于流动模式。
注册“data”事件处理程序会将流切换到流动模式。
方便的是这意味着流在注册第一个“data”事件处理程序之前不会发出“data”事件。
如果你正在使用流模式从可读流中读取数据处理数据然后将其写入可写流那么你可能需要处理可写流的背压。
如果write()方法返回false表示写入缓冲区已满你可以在可读流上调用pause()来暂时停止data事件。
然后当你从可写流中收到“drain”事件时你可以在可读流上调用resume()来重新开始data事件的流动。
流在流动模式下在达到流的末尾时会发出一个“end”事件。
这个事件表示不会再发出更多的“data”事件。
并且像所有流一样如果发生错误将会发出一个“error”事件。
在流部分的开头我们展示了一个非流式的copyFile()函数并承诺会有一个更好的版本。
以下代码展示了如何实现一个使用流动模式
并处理背压的流式copyFile()函数。
这本来更容易通过pipe()调用来实现但在这里作为协调从一个流到另一个流的数据流的多个事件处理程序的有用演示。
fs.createReadStream(sourceFilename);let
fs.createWriteStream(destinationFilename);input.on(data,
可读流的另一种模式是“暂停模式”。
这是流开始的模式。
如果你从未注册“data”事件处理程序也从未调用pipe()方法那么可读流将保持在暂停模式。
在暂停模式下流不会以“data”事件的形式向你推送数据。
相反你需要通过显式调用其read()方法来从流中拉取数据。
这不是一个阻塞调用如果流上没有可读数据它将返回null。
由于没有同步
也是基于事件的。
在暂停模式下当流上有数据可读时可读流会发出“readable”事件。
作为响应你的代码应该调用read()方法来读取数据。
你必须在循环中这样做重复调用read()直到它返回null。
这样完全排空流的缓冲区是必要的以便在将来触发新的“readable”事件。
如果在仍然有可读数据时停止调用read()你将不会收到另一个“readable”事件你的程序可能会挂起。
暂停模式下的流会像流动模式下的流一样发出“end”和“error”事件。
如果你正在编写一个从可读流读取数据并将其写入可写流的程序那么暂停模式可能不是一个好选择。
为了正确处理背压你只想在输入流可读且输出流没有积压时才读取。
在暂停模式下这意味着读取和写入直到read()返回null或write()返回false然后在readable或drain事件上重新开始读取或写入。
这是不够优雅的你可能会发现在这种情况下流动模式或管道更容易。
哈希。
它使用一个处于暂停模式的可读流以块的形式读取文件的内容然后将每个块传递给计算哈希的对象。
请注意在
errorconsole.error(err.toString());
on.“os”模块与process不同需要使用require()显式加载提供了关于
运行的计算机和操作系统的类似低级细节的访问。
你可能永远不需要使用这些功能中的任何一个但值得知道
的“fs”模块是一个用于处理文件和名称的实用函数。
“fs”模块包含一些高级函数用于轻松读取、写入和复制文件。
但是该模块中的大多数函数都是低级
的某些部分很简洁和不直观。
例如删除文件的函数称为unlink()。
API主要是因为通常每个基本操作都有多个变体。
正如本章开头所讨论的大多数函数如fs.readFile()都是非阻塞的、基于回调的和异步的。
通常情况下每个函数都有一个同步阻塞的变体比如fs.readFileSync()。
在
的异步变体比如fs.promises.readFile()。
大多数“fs”函数的第一个参数是一个字符串指定要操作的文件的路径文件名加可选的目录名。
但是其中一些函数也支持一个以整数“文件描述符”作为第一个参数而不是路径的变体。
这些变体的名称以字母“f”开头。
例如fs.truncate()截断由路径指定的文件而fs.ftruncate()截断由文件描述符指定的文件。
还有一个基于
的fs.promises.truncate()它期望一个路径还有另一个基于
中的文件描述符。
最后在“fs”模块中有一些函数的变体的名称以字母“l”开头。
这些“l”变体类似于基本函数但不会遵循文件系统中的符号链接而是直接操作符号链接本身。
要使用“fs”模块处理文件首先需要能够命名要处理的文件。
文件通常由路径指定这意味着文件本身的名称以及文件所在的的所有。
处理路径可能有点棘手因为不同的操作系统使用不同的字符来分隔路径段需要特殊处理。
Node
/b/t.js请注意path.normalize()只是一个字符串操作函数没有访问实际文件系统。
fs.realpath()和fs.realpathSync()函数执行文件系统感知的规范化它们解析符号链接并解释相对于当前工作目录的相对路径名。
风格的路径可以使用path.posix而不是path。
反之如果想在
路径可以使用path.win32。
path.posix和path.win32定义了与path本身相同的属性和函数。
我们将在接下来的章节中介绍一些“fs”函数它们期望一个文件描述符而不是文件名。
文件描述符是作为操作系统级别引用“打开”文件的整数。
通过调用fs.open()或fs.openSync()函数你可以为给定的名称获取一个描述符。
进程一次只能打开有限数量的文件因此当你使用完文件描述符时调用fs.close()是很重要的。
如果你想要使用最底层的fs.read()和fs.write()函数允许你在文件中跳转不同时间读取和写入文件的位你需要打开文件。
在“fs”模块中有其他使用文件描述符的函数但它们都有基于名称的版本只有当你打算打开文件进行读取或写入时才真正有意义使用基于描述符的函数。
中fs.open()的等价物是fs.promises.open()它返回一个解析为
的最底层的read()和write()方法否则真的没有理由创建一个。
如果你确实创建了一个
FileHandle记得在使用完毕后调用它的close()方法。
如果你的文件很小或者内存使用和性能不是最高优先级那么通常最容易的方法是一次性读取整个文件的内容。
你可以同步地、通过回调或
来做到这一点。
默认情况下你会得到文件的字节作为缓冲区但如果指定了编码你将得到一个解码后的字符串。
utf8).***n(processFileText).catch(handleReadError);//
}如果你能够按顺序处理文件的内容并且不需要同时将文件的整个内容保存在内存中那么通过流来读取文件可能是最有效的方法。
我们已经广泛讨论了流这里是你如何使用流和pipe()方法将文件的内容写入标准输出的示例
encoding).pipe(process.stdout);
}最后如果你需要对从文件中读取的字节以及何时读取它们进行低级别的控制你可以打开一个文件以获取文件描述符然后使用fs.read()、fs.readSync()或fs.promises.read()从文件的指定源位置读取指定数量的字节到指定的缓冲区的指定目标位置
});如果你需要从文件中读取多个数据块基于回调的read()API
Buffer.alloc(length);fs.readSync(fd,
中写入文件与读取文件非常相似但有一些额外的细节需要了解。
其中一个细节是创建一个新文件的方式就是简单地向一个尚不存在的文件名写入。
中有三种基本的写入文件的方式。
如果文件的整个内容是一个字符串或缓冲区你可以使用fs.writeFile()基于回调、fs.writeFileSync()同步或fs.promises.writeFile()基于
fs.writeFileSync(path.resolve(__dirname,
settings.json),JSON.stringify(settings));如果要写入文件的数据是字符串并且想要使用除了“utf8”之外的编码请将编码作为可选的第三个参数传递。
相关的函数fs.appendFile()、fs.appendFileSync()和fs.promises.appendFile()类似但当指定的文件已经存在时它们会将数据追加到末尾而不是覆盖现有文件内容。
如果要写入文件的数据不是一个块或者不是同时在内存中的所有数据那么使用
流是一个不错的方法假设您计划从头到尾写入数据而不跳过文件中的位置
fs.createWriteStream(numbers.txt);
output.end();最后如果您想要将数据写入文件的多个块并且希望能够控制写入每个块的确切位置那么可以使用fs.open()、fs.openSync()或fs.promises.open()打开文件然后使用结果文件描述符与fs.write()或fs.writeSync()函数。
这些函数有不同形式的字符串和缓冲区。
字符串变体接受文件描述符、字符串和要写入该字符串的文件位置可选的第四个参数为编码。
缓冲区变体接受文件描述符、缓冲区、偏移量和长度指定缓冲区内的数据块并指定要写入该块的字节的文件位置。
如果您有要写入的
对象数组可以使用单个fs.writev()或fs.writevSync()。
使用fs.promises.open()和它生成的
你可以使用fs.truncate()、fs.truncateSync()或fs.promises.truncate()来截断文件的末尾。
这些函数以路径作为第一个参数长度作为第二个参数并修改文件使其具有指定的长度。
如果省略长度则使用零并且文件变为空。
尽管这些函数的名称是这样的但它们也可以用于扩展文件如果指定的长度比当前文件大小长文件将扩展为零字节到新大小。
如果您已经打开要修改的文件可以使用带有文件描述符或
这里描述的各种文件写入函数在数据“写入”后返回或调用其回调或解析其
已将数据交给操作系统。
但这并不一定意味着数据实际上已经写入到持久存储中至少您的一些数据可能仍然在操作系统中的某个地方或设备驱动程序中缓冲等待写入磁盘。
如果调用fs.writeSync()同步将一些数据写入文件并且在函数返回后立即发生停电您可能仍会丢失数据。
如果要强制将数据写入磁盘以确保它已经安全保存使用fs.fsync()或fs.fsyncSync()。
这些函数仅适用于文件描述符没有基于路径的版本。
的流类的前面讨论包括两个copyFile()函数的示例。
这些不是您实际使用的实用程序因为“fs”模块定义了自己的fs.copyFile()方法当然还有fs.copyFileSync()和fs.promises.copyFile()。
这些函数将原始文件的名称和副本的名称作为它们的前两个参数。
这些可以指定为字符串或
或缓冲区对象。
可选的第三个参数是一个整数其位指定控制copy操作细节的标志。
对于基于回调的fs.copyFile()最后一个参数是在复制完成时不带参数调用的回调函数或者如果出现错误则带有错误参数调用。
以下是一些示例
Date().toISOString()}fs.constants.COPYFILE_EXCL
fs.constants.COPYFILE_FICLONE).***n(()
err);});fs.rename()函数以及通常的同步和基于
的变体移动和/或重命名文件。
调用它时传入当前文件的路径和所需的新文件路径。
没有标志参数但基于回调的版本将回调作为第三个参数
backups/ch15.bak);请注意没有标志可以防止重命名覆盖现有文件。
同时请记住文件只能在文件系统内重命名。
函数fs.link()和fs.symlink()及其变体具有与fs.rename()相同的签名并且类似于fs.copyFile()只是它们分别创建硬链接和符号链接而不是创建副本。
最后fs.unlink()、fs.unlinkSync()和fs.promises.unlink()是
继承而来其中删除文件基本上是创建其硬链接的相反操作。
调用此函数并传递一个回调如果使用基于回调的版本来删除要删除的文件的字符串、缓冲区或
fs.unlinkSync(backups/ch15.bak);16.7.5
fs.stat()、fs.statSync()和fs.promises.stat()函数允许您获取指定文件或目录的元数据。
例如
对象包含其他更隐晦的属性和方法但此代码演示了您最有可能使用的属性。
fs.lstat()及其变体的工作方式与fs.stat()完全相同只是如果指定的文件是符号链接则
对象则可以使用fs.fstat()或其变体获取已打开文件的元数据信息而无需再次指定文件名。
除了使用fs.stat()及其所有变体查询元数据外还有用于更改元数据的函数。
fs.chmod()、fs.lchmod()和fs.fchmod()以及同步和基于
的版本设置文件或目录的“模式”或权限。
模式值是整数其中每个位具有特定含义并且在八进制表示法中最容易理解。
例如要使文件对其所有者只读且对其他人不可访问请使用0o400
accidentally!fs.chown()、fs.lchown()和fs.fchown()以及同步和基于
最后您可以使用fs.utimes()和fs.futimes()及其变体设置文件或目录的访问时间和修改时间。
});fs.mkdtemp()及其变体接受您提供的路径前缀将一些随机字符附加到其后这对安全性很重要创建一个以该名称命名的路径返回或传递给回调给您。
fs.mkdtempSync(path.join(os.tmpdir(),
}“fs”模块为列出并向您提供一个字符串数组或指定每个项目的名称和类型文件或目录的
对象数组。
这些函数返回的文件名只是文件的本地名称而不是整个路径。
以下是示例
name)));}).catch(console.error);如果你预计需要列出可能有数千条条目的的
fs.promises.opendir(dirpath);for
fs.promises.stat(path.join(dirpath,
stats.size;console.log(String(size).padStart(10),
相对较低级本章节无法覆盖所有功能。
但接下来的示例演示了如何编写基本的客户端和服务器。
模块。
第二个参数是一个回调函数当服务器的响应开始到达时将调用该回调并传入一个
状态和头部信息是可用的但正文可能还没有准备好。
IncomingMessage
对象是一个可读流你可以使用本章前面演示的技术从中读取响应正文。
application/json,Content-Length:
Buffer.byteLength(bodyText)}};if
https.request(requestOptions);//
request.request.write(bodyText);request.end();//
discarded.response.resume();return;}//
header.response.setEncoding(utf8);//
事件注册一个事件处理程序使用该处理程序来读取客户端的请求特别是
服务器从本地文件系统提供静态文件并实现了一个调试端点通过回显客户端的请求来响应。
url.parse(request.url).pathname;//
headerresponse.setHeader(Content-Type,
requestresponse.write(${request.method}
HTTP/${request.httpVersion}\r\n);//
type;switch(path.extname(filename))
fs.createReadStream(filename);stream.once(readable,
ends.response.setHeader(Content-Type,
type);response.writeHead(200);stream.pipe(response);});stream.on(error,
message.response.setHeader(Content-Type,
charsetUTF-8);response.writeHead(404);response.end(err.message);});}});
服务器。
但请注意生产服务器通常不直接构建在这些模块之上。
相反大多数复杂的服务器是使用外部库实现的——比如
如果您习惯使用流那么网络相对简单因为网络套接字只是一种双工流。
要创建一个服务器调用net.createServer()然后调用生成的对象的listen()方法告诉服务器在哪个端口上监听连接。
对象将生成“connection”事件并传递给事件侦听器的值将是一个
对象是一个双工流您可以使用它从客户端读取数据并向客户端写入数据。
编写客户端甚至更容易将端口号和主机名传递给net.createConnection()以创建一个套接字用于与在该主机上运行并在该端口上监听的任何服务器通信。
randomElement(Object.keys(jokes));let
readline.createInterface({input:
}这样的简单基于文本的服务器通常不需要一个定制的客户端。
如果您的系统上安装了nc“netcat”实用程序您可以使用它来与这个服务器通信方法如下
require(net).createConnection(6789,
域套接字”进行进程间通信这些套接字通过文件系统路径而不是端口号进行标识。
的客户端和服务器以及“tls”模块它类似于“https”对“http”的关系。
tls.Server和tls.TLSSocket类允许创建使用
中“child_process”模块定义了许多函数用于作为子进程运行其他程序。
运行另一个程序的最简单方法是使用child_process.execSync()。
默认情况下此返回值是一个缓冲区但您可以在可选的第二个参数中指定编码以获得一个字符串。
所以例如如果您正在编写一个脚本性能不是一个问题您可能会使用child_process.execSync()来列出一个目录而不是使用fs.readdirSync()函数
意味着您传递给它的字符串可以包含多个以分号分隔的命令并且可以利用
功能如文件名通配符、管道和输出重定向。
这也意味着您必须小心永远不要将来自用户输入或类似不受信任来源的命令传递给
shell它无法解析命令行您必须将可执行文件作为第一个参数传递并将命令行参数数组作为第二个参数传递
函数是同步的它们会阻塞并在子进程退出之前不返回。
使用这些函数很像在终端窗口中输入
命令它们允许您逐个运行一系列命令。
但是如果您正在编写一个需要完成多个任务且这些任务彼此不依赖的程序那么您可能希望并行运行它们并同时运行多个命令。
您可以使用异步函数
与它们的同步变体类似只是它们立即返回一个代表正在运行的子进程的
对象并且它们将错误优先的回调作为最后一个参数。
当子进程退出时将调用回调并实际上会使用三个参数调用它。
第一个是错误如果有的话如果进程正常终止则为
null。
第二个参数是发送到子进程标准输出流的收集输出。
第三个参数是发送到子进程标准错误流的任何输出。
对象允许您终止子进程并向其写入数据然后可以从其标准输入读取。
当我们讨论
util.promisify(child_process.exec);function
Promise.all(promises).***n(outputs
函数——同步和异步——都设计用于与快速运行且不产生大量输出的子进程一起使用。
即使是异步的
函数允许您在子进程仍在运行时流式访问子进程的输出。
它还允许您向子进程写入数据子进程将把该数据视为其标准输入流上的输入这意味着可以动态与子进程交互根据其生成的输出发送输入。
一样调用它提供要运行的可执行文件以及一个单独的命令行参数数组传递给它。
spawn()
对象是一个事件发射器。
你可以监听“exit”事件以在子进程退出时收到通知。
ChildProcess
对象还有三个流属性。
stdout和stderr是可读流当子进程写入其
流变得可读。
请注意这里名称的倒置。
在子进程中stdout是一个可写输出流但在父进程中ChildProcess
对象的stdin属性是一个可写流你写入到这个流的任何内容都会在子进程的标准输入上可用。
代码模块的函数。
fork()期望与spawn()相同的参数但第一个参数应指定
使用fork()创建的子进程可以通过其标准输入和标准输出流与父进程通信就像在spawn()的前一节中描述的那样。
但是fork()还为父子进程之间提供了另一个更简单的通信渠道。
上的“message”事件来接收子进程发送的消息。
在子进程中运行的代码可以使用process.send()向父进程发送消息并且可以监听process上的“message”事件来接收父进程发送的消息。
这里例如是一些使用fork()创建子进程的代码然后向该子进程发送消息并等待响应的代码
child_process.fork(${__dirname}/child.js);//
{console.log(message.hypotenuse);
parent.process.send({hypotenuse:
});启动子进程是一个昂贵的操作子进程必须进行数量级更多的计算才能使用fork()和这种方式的进程间通信才有意义。
如果你正在编写一个需要对传入事件非常敏感并且还需要执行耗时计算的程序那么你可能会考虑使用一个单独的子进程来执行计算以便它们不会阻塞事件循环并降低父进程的响应性。
尽管在这种情况下线程—参见§16.11—可能比子进程更好的选择。
send()的第一个参数将使用JSON.stringify()进行序列化并在子进程中使用JSON.parse()进行反序列化因此你应该只包含
格式支持的值。
然而send()有一个特殊的第二个参数允许你传输
的机器上运行该服务器那么你可以使用fork()创建多个子进程来处理请求。
在父进程中你可能会监听
对象上的“connection”事件然后从该“connection”事件中获取
对象并使用特殊的第二个参数send()到一个子进程中处理。
请注意这是一个不太常见的情况的不太可能的解决方案。
与编写分叉子进程的服务器相比保持服务器单线程并在生产环境中部署多个实例来处理负载可能更简单。
API§15.13非常相似。
多线程编程以难度大而著称。
这几乎完全是因为需要仔细同步线程对共享内存的访问。
但
的工作线程通过消息传递进行通信而不是使用共享内存。
主线程可以通过调用表示该线程的
对象的postMessage()方法向工作线程发送消息。
工作线程可以通过监听“message”事件来接收来自其父级的消息。
工作线程可以通过自己的postMessage()方法向主线程发送消息父级可以通过自己的“message”事件处理程序接收消息。
示例代码将清楚地说明这是如何工作的。
核心处理更多的计算那么线程可以让您在多个核心之间分配工作这在今天的计算机上已经很普遍。
如果您在
中进行科学计算、机器学习或图形处理那么您可能希望使用线程来为问题提供更多的计算能力。
的全部性能您可能仍然希望使用线程来保持主线程的响应性。
考虑一个处理大型但相对不频繁请求的服务器。
假设它每秒只收到一个请求但需要大约半秒钟的阻塞
的空闲时间。
但当两个请求在几毫秒内同时到达时服务器甚至无法开始响应第二个请求直到第一个响应的计算完成。
相反如果服务器使用工作线程执行计算服务器可以立即开始响应两个请求并为服务器的客户提供更好的体验。
假设服务器有多个
核心它还可以并行计算两个响应的主体但即使只有一个核心使用工作线程仍然可以提高响应性。
一般来说工作线程允许我们将阻塞的同步操作转换为非阻塞的异步操作。
如果您正在编写一个依赖不可避免同步的传统代码的程序您可以使用工作线程来避免在需要调用该传统代码时阻塞。
工作线程并不像子进程那样沉重但也不轻量级。
通常情况下除非有大量工作要做否则创建工作线程是没有意义的。
一般来说如果您的程序既不受
模块被称为“worker_threads”。
在本节中我们将使用标识符threads来引用它
require(worker_threads);该模块定义了一个
类来表示一个工作线程您可以使用threads.Worker()构造函数创建一个新线程。
以下代码演示了如何使用此构造函数创建一个工作线程并展示了如何从主线程向工作线程传递消息以及从工作线程向主线程传递消息。
它还演示了一个技巧允许您将主线程代码和工作线程代码放在同一个文件中。
²
workerreticulator.postMessage(splines);//
complete.threads.parentPort.once(message,
thread.threads.parentPort.postMessage(splines);});
标识符创建一个加载和运行与主线程相同文件的工作线程。
不过一般来说你会传递一个文件路径。
请注意如果指定相对路径则相对于
process.cwd()而不是相对于当前运行的模块。
如果你想要一个相对于当前模块的路径可以使用类似
构造函数还可以接受一个对象作为其第二个参数该对象的属性为工作线程提供可选配置。
我们稍后会介绍其中一些选项但现在请注意如果将
require(worker_threads);threads.parentPort.postMessage(threads.isMainThread);
的对象上创建一个副本而不是直接与工作线程共享。
这样可以防止工作线程和主线程共享内存。
你可能会期望这种复制是通过
threads.parentPort.postMessage()
注册来自父线程的消息的事件处理程序。
在主线程中threads.parentPort
属性向工作线程传递一个初始消息该消息将在工作线程启动后立即可用这样工作线程就不必等待“message”事件才能开始工作。
属性来指定一组自定义的环境变量。
作为一个特殊可能危险的情况父线程可以将
threads.SHARE_ENV这将导致两个线程共享一组环境变量以便一个线程中的更改在另一个线程中可见。
默认情况下工作线程中的process.stdout和process.stderr流会简单地传输到父线程中对应的流。
这意味着例如console.log()和console.error()在工作线程中的输出方式与主线程中完全相同。
你可以通过在Worker()构造函数的第二个参数中传递stdout:true或stderr:true来覆盖此默认行为。
如果这样做那么工作线程写入这些流的任何输出都可以在父线程的worker.stdout和worker.stderr流中读取到。
这里存在一个潜在的令人困惑的流方向倒置我们在本章前面的子进程中也看到了相同的情况工作线程的输出流是父线程的输入流工作线程的输入流是父线程的输出流。
如果工作线程调用process.exit()只有该线程退出整个进程不会退出。
工作线程不允许更改它们所属进程的共享状态。
当从工作线程调用process.chdir()和process.setuid()等函数时会抛出异常。
操作系统信号如SIGINT和SIGTERM只会传递给主线程它们无法在工作线程中接收或处理。
创建新的工作线程时会同时创建一个通信通道允许工作线程和父线程之间传递消息。
正如我们所见工作线程使用threads.parentPort与父线程发送和接收消息父线程使用
创建自定义通信通道。
如果你已经阅读了该部分接下来的内容会让你感到很熟悉。
假设一个工作线程需要处理主线程中两个不同模块发送的两种不同消息。
这两个不同模块可以共享默认通道并使用worker.postMessage()发送消息但如果每个模块都有自己的私有通道向工作线程发送消息会更清晰。
或者考虑主线程创建两个独立工作线程的情况。
自定义通信通道可以让这两个工作线程直接相互通信而不必通过父线程发送所有消息。
使用MessageChannel()构造函数创建一个新的消息通道。
一个
对象。
在其中一个端口上调用postMessage()将导致另一个端口生成“message”事件并携带
channel.port1.postMessage(hello);
printed你也可以在任一端口上调用close()来断开两个端口之间的连接并表示不会再交换更多消息。
当任一端口上调用close()时将向两个端口传递“close”事件。
对象然后使用这些对象在主线程内传输消息。
为了在工作线程中使用自定义通信通道我们必须将两个端口中的一个从创建它的线程传输到将要使用它的线程。
下一节将解释如何做到这一点。
对象但只能使用一种特殊技术作为特例。
postMessage()
transferList是一个要在线程之间传输而不是复制的对象数组。
MessagePort并且可以直接将现有对象交给另一个线程。
然而关于在线程之间传输值的关键是一旦值被传输它就不能再在调用
handleMessagesFromWorker);MessagePort
对象并不是唯一可以传输的对象。
如果你使用一个类型化数组作为消息调用
postMessage()或者消息中包含一个或多个任意深度嵌套的类型化数组那么这个类型化数组或这些类型化数组将会被结构化克隆算法简单地复制。
但是类型化数组可能很大例如如果你正在使用一个工作线程对数百万像素进行图像处理。
因此为了效率起见postMessage()
还给了我们传输类型化数组而不是复制它们的选项。
线程默认共享内存。
JavaScript
中的工作线程通常避免共享内存但当我们允许这种受控传输时可以非常高效地完成。
这种安全性的保证在于当一个类型化数组被传输到另一个线程时它在传输它的线程中将变得无法使用。
在图像处理场景中主线程可以将图像的像素传输给工作线程然后工作线程在完成后可以将处理后的像素传回主线程。
内存不需要被复制但永远不会被两个线程同时访问。
一样一旦传输了一个类型化数组它就变得无法使用。
如果尝试使用已经传输的
或类型化数组不会抛出异常当与它们交互时这些对象只是停止执行任何操作。
除了在线程之间传输类型化数组实际上还可以在线程之间共享类型化数组。
只需创建一个所需大小的
SharedArrayBuffer然后使用该缓冲区创建一个类型化数组。
当通过
支持的类型化数组时底层内存将在线程之间共享。
在这种情况下不应该将共享缓冲区包含在
从未考虑过线程安全并且多线程编程非常难以正确实现。
这也是为什么
运算符也不是线程安全的因为它需要读取一个值递增它然后写回。
如果两个线程同时递增一个值它通常只会被递增一次如下面的代码所示
million.console.log(sharedArray[0]);});});
knowthreads.parentPort.postMessage(done);
SharedArrayBuffer即当两个线程在共享内存的完全不同部分上操作时。
你可以通过创建两个作为非重叠区域视图的类型化数组来强制执行这一点然后让你的两个线程使用这两个单独的类型化数组。
例如可以这样执行并行归并排序一个线程对数组的下半部分进行排序另一个线程对数组的上半部分进行排序。
或者某些类型的图像处理算法也适合这种方法多个线程在图像的不同区域上工作。
如果你确实需要允许多个线程访问共享数组的同一区域你可以通过使用
也被添加以定义共享数组元素上的原子操作。
例如Atomics.add()函数读取共享数组的指定元素将指定值添加到其中并将总和写回数组。
它以原子方式执行此操作就好像它是一个单独的操作并确保在操作进行时没有其他线程可以读取或写入该值。
Atomics.add()允许我们重新编写我们刚刚查看的并获得正确结果的并行增量代码即对共享数组元素进行
20,000,000.console.log(Atomics.load(sharedArray,
increment}threads.parentPort.postMessage(done);
20,000,000。
但它比它替换的不正确代码慢大约九倍。
在一个线程中执行所有
万次增量会更简单、更快。
还要注意原子操作可能能够确保图像处理算法的线程安全其中每个数组元素都是完全独立于所有其他值的值。
但在大多数实际程序中多个数组元素通常彼此相关并且需要某种高级别的线程同步。
低级别的Atomics.wait()和Atomics.notify()函数可以帮助解决这个问题但本书不涉及它们的使用讨论。
的“worker_threads”模块用于使用消息传递而不是共享内存进行真正的多线程编程。
定义了一个fs.copyFile()函数实际上你会在实践中使用它。
将工作代码定义在一个单独的文件中通常更清晰、更简单。
但当我第一次遇到
的fork()系统调用时两个线程运行同一文件的不同部分的技巧让我大吃一惊。
我认为值得演示这种技术仅仅因为它的奇怪优雅。
恭喜您达到本书的最后一章。
如果您已经阅读了前面的所有内容现在您对
语言的两个广泛使用的扩展。
无论您是否选择为自己的项目使用这些工具和扩展您几乎肯定会在其他项目中看到它们的使用因此至少了解它们是很重要的。
本章不会以任何全面的方式记录这些工具和扩展。
目标只是以足够深度解释它们以便您了解它们为何有用以及何时可能需要使用它们。
本章涵盖的所有内容在
编程世界中被广泛使用如果您决定采用工具或扩展您会在网上找到大量文档和教程。
在编程中术语lint指的是在技术上正确但不雅观、可能存在错误或以某种方式不够优化的代码。
linter是一种用于检测代码中
是ESLint。
如果您运行它然后花时间实际修复它指出的问题它将使您的代码更清洁更不容易出现错误。
考虑以下代码
code/ch17/linty.jscode/ch17/linty.js1:1
可能看起来很挑剔。
我们是使用双引号还是单引号真的很重要吗另一方面正确的缩进对于可读性很重要使用和let而不是和var可以保护您免受微妙错误的影响。
未使用的变量是代码中的累赘——没有理由保留它们。
的原因之一是强制执行一致的编码风格以便当程序员团队共同工作在共享的代码库上时他们使用兼容的代码约定。
这包括代码缩进规则但也可以包括诸如首选引号类型以及for关键字和其后的开括号之间是否应该有空格等内容。
对这段代码进行了缩进修复添加了缺失的分号围绕二进制运算符添加了空格并在
Prettier它将简单地在原地重新格式化指定的文件而不是打印重新格式化的版本。
如果你使用
就会变得非常强大。
我觉得写松散的代码然后看到它被自动修复很解放。
是可配置的但只有少数选项。
你可以选择最大行长度、缩进量、是否应该使用分号、字符串是单引号还是双引号以及其他一些内容。
一般来说Prettier
Prettier。
然而在这本书中的代码中我没有使用它因为在我的许多代码中我依赖仔细的手动格式化来垂直对齐我的注释而
这样的动态语言支持测试框架大大减少了编写测试所需的工作量几乎让测试编写变得有趣JavaScript
有很多测试工具和库许多都是以模块化的方式编写的因此可以选择一个库作为测试运行器另一个库用于断言第三个库用于模拟。
然而在本节中我们将描述
getJSON(https://globaltemps.example.com/api/city/${city.toLowerCase()});//
URL并且是否正确地转换了温度标度。
我们可以使用类似下面的基于
是一个异步函数所以测试也是异步的——测试异步函数可能有些棘手但
require(./getTemperature.js);//
getJSON.mockResolvedValue(0);//
https://globaltemps.example.com/api/city/vancouver;let
await(getTemperature(Vancouver));//
that.expect(getJSON).toHaveBeenCalledWith(expectedURL);});//
212FgetJSON.mockResolvedValue(100);
ch17/getTemperature.test.jsgetTemperature()✓
correctlyexpect(received).toBe(expected)
getJSON.mockResolvedValue(100);
(ch17/getTemperature.test.js:31:43)Test
ch17/getTemperature.test.jsgetTemperature()✓
(1ms)------------------|--------|---------|---------|---------|------------------|
------------------|--------|---------|---------|---------|------------------|
------------------|--------|---------|---------|---------|------------------|
/getTemperature/i.运行我们的测试为我们正在测试的模块提供了
提供了部分覆盖但我们对该模块进行了模拟并且并不打算测试它所以这是预期的。
在现代软件开发中编写的任何非平凡程序都可能依赖于第三方软件库。
例如如果您在
这样的前端框架。
包管理器使查找和安装这些第三方包变得容易。
同样重要的是包管理器会跟踪代码所依赖的包并将此信息保存到文件中以便其他人想要尝试您的程序时他们可以下载您的代码和依赖项列表然后使用自己的包管理器安装代码所需的所有第三方包。
install。
这会读取package.json文件中列出的依赖项并下载项目需要的第三方包并将其保存在*node_modules/*目录中。
package-name来将特定包安装到项目的*node_modules/*目录中
还会在项目的package.json文件中记录依赖关系。
以这种方式记录依赖关系是让其他人通过输入npm
另一种依赖关系是开发人员工具的依赖这些工具是开发人员想要在项目上工作时需要的但实际上不需要运行代码。
例如如果项目使用
dependency”您可以使用--save-dev来安装和记录其中之一
prettier有时您可能希望全局安装开发工具以便它们可以在任何地方访问即使不是正式项目的代码也可以使用package.json文件和*node_modules/*目录。
为此您可以使用-g全局选项
/usr/local/lib/node_modules/eslint/bin/eslint.js
/usr/local/lib/node_modules/jest/bin/jest.jsjest24.9.0eslint6.7.2
/usr/local/bin/jest除了“install”命令npm
还支持“uninstall”和“update”命令其功能如其名称所示。
npm
还有一个有趣的“audit”命令您可以使用它来查找并修复依赖项中的安全漏洞
脚本会出现在*./node_modules/.bin/eslint*中这使得运行命令变得笨拙。
幸运的是npm
捆绑了一个名为“npx”的命令您可以使用它来运行本地安装的工具如npx
背后的公司还维护着https://npmjs.com包仓库其中包含数十万个开源包。
但您不必使用
程序以在网络浏览器中运行那么您可能需要使用一个代码捆绑工具特别是如果您使用作为模块交付的外部库。
多年来网络开发人员一直在使用
模块(§10.3)早在网络上支持import和export关键字之前。
为了做到这一点程序员使用一个代码捆绑工具从程序的主入口点或入口点开始并遵循import指令的树以找到程序所依赖的所有模块。
然后它将所有这些单独的模块文件组合成一个
代码的单个捆绑包并重写import和export指令使代码在这种新形式下工作。
结果是一个单个的代码文件可以加载到不支持模块的网络浏览器中。
模块现在几乎被所有的网络浏览器支持但网络开发人员在发布生产代码时仍倾向于使用代码捆绑工具。
开发人员发现当用户首次访问网站时加载一个中等大小的代码捆绑包比加载许多小模块时用户体验更好。
网络性能是一个众所周知的棘手话题有很多要考虑的变量包括浏览器供应商的持续改进因此确保加载代码的最快方式的唯一方法是进行彻底的测试和仔细的测量。
请记住有一个完全在您控制之下的变量代码大小。
较少的
捆绑工具可供选择。
常用的捆绑工具包括webpack、Rollup和Parcel。
捆绑工具的基本功能大致相同它们的区别在于可配置性或易用性。
Webpack
已经存在很长时间拥有庞大的插件生态系统可高度配置并且可以支持旧的非模块化库。
但它也可能复杂且难以配置。
另一端是
Parcel它被设计为一个零配置的替代方案只需简单地做正确的事情。
一些程序可能有多个入口点。
例如一个具有多个页面的网络应用程序可以为每个页面编写不同的入口点。
打包工具通常允许您为每个入口点创建一个捆绑包或者创建一个支持多个入口点的单个捆绑包。
程序可以使用import()的功能形式(§10.3.6)而不是静态形式在实际需要时动态加载模块而不是在程序启动时静态加载它们。
这通常是改善程序启动时间的好方法。
支持import()的捆绑工具可能能够生成多个输出捆绑包一个在启动时加载一个或多个在需要时动态加载。
如果动态加载的模块共享依赖关系那么确定要生成多少个捆绑包就变得棘手了您可能需要手动配置捆绑工具来解决这个问题。
捆绑工具通常可以输出一个源映射文件定义了捆绑包中代码行与原始源文件中对应行之间的映射关系。
这使得浏览器开发工具可以自动显示
有时当你将一个模块导入到你的程序中时你可能只使用其中的一部分功能。
一个好的打包工具可以分析代码以确定哪些部分是未使用的可以从捆绑包中省略。
这个功能被戏称为“tree-shaking”。
打包工具通常具有基于插件的架构并支持插件允许导入和捆绑实际上不是
兼容数据结构。
代码打包工具可以配置允许你将该数据结构移动到一个单独的
./big-widget-list.json的声明将其导入到你的程序中。
同样将
这样不需要编译的语言中运行一个打包工具感觉像是一个编译步骤每次在运行代码之前都必须运行一个打包工具这让人感到沮丧。
打包工具通常支持文件系统监视器检测项目目录中任何文件的编辑并自动重新生成必要的捆绑包。
有了这个功能你通常可以保存你的代码然后立即重新加载你的
一些打包工具还支持开发人员的“热模块替换”模式每次重新生成捆绑包时它会自动加载到浏览器中。
当这个功能起作用时对开发人员来说是一种神奇的体验但在幕后进行了一些技巧使其工作它并不适用于所有项目。
诸如**指数运算符和箭头函数之类的语言特性可以相对容易地转换为Math.pow()和function表达式。
其他语言特性如class关键字需要进行更复杂的转换而且一般来说Babel
可以生成源映射将转换后的代码位置映射回原始源位置这在处理转换后的代码时非常有帮助。
语言的演变今天几乎不需要编译掉箭头函数和类声明。
当你想要使用最新功能如数字文字中的下划线分隔符时Babel
定义了“预设”你可以根据想要使用的语言扩展和你想要多么积极地转换标准语言特性来选择。
Babel
的一个有趣的预设是用于通过缩小来进行代码压缩去除注释和空格重命名变量等。
文件。
如果是这样这可能是一个方便的选项因为它简化了生成可运行代码的过程。
例如Webpack
支持一个“babel-loader”模块你可以安装并配置它在捆绑时运行
仍然常用于支持语言的非标准扩展我们将在接下来的章节中描述其中的两个语言扩展。
JavaScript。
转换足够简单以至于一些开发人员选择在不使用
hidden/;当一个元素有一个或多个属性时它们成为传递给createElement()的第二个参数的对象的属性
classNamesidebarh1Title/h1hr/pThis
表达式转换为一组嵌套的createElement()调用。
当一个
content));React.createElement()返回的值是
React我们不会详细介绍返回的元素对象或呈现过程。
值得注意的是你可以配置
语法是表达其他类型嵌套数据结构的有用方式你可以将其用于自己的非
JavaScript。
这些嵌套表达式允许作为属性值和子元素。
例如
className{className}h1{title}/h1{
}这段代码易于阅读和理解花括号消失了生成的代码以自然的方式将传入的函数参数传递给React.createElement()。
请注意我们在这里使用drawLine参数和短路运算符的巧妙技巧。
如果你只用三个参数调用sidebar()那么drawLine默认为true并且外部createElement()调用的第四个参数是hr/元素。
但如果将false作为第四个参数传递给sidebar()那么外部createElement()调用的第四个参数将计算为false并且永远不会创建hr/元素。
这种使用运算符的习惯用法在
中是一种常见的习语根据某些其他表达式的值有条件地包含或排除子元素。
这种习惯用法在
简单地忽略false或null的子元素并且不为它们生成任何输出。
}此函数将对象字面量用作ul元素上style属性的值。
请注意这里需要双大括号。
ul元素只有一个子元素但该子元素的值是一个数组。
子数组是通过在输入数组上使用map()函数创建li元素数组而创建的数组。
这在
库在渲染时会展平元素的子元素。
具有一个数组子元素的元素与该元素的每个数组元素作为子元素相同。
最后请注意每个嵌套的li元素都有一个onClick事件处理程序属性其值是一个箭头函数。
JSX
中对象表达式的另一个用途是使用对象扩展运算符§6.10.4一次指定多个属性。
假设你发现自己编写了许多重复一组常见属性的
表达式。
你可以通过将属性定义为对象的属性并将它们“扩展到”你的
将其编译为使用_extends()函数此处省略将className属性与hebrew对象中包含的属性组合在一起
React.createElement(span,_extends({className:
hebrew),\u05E9\u05DC\u05D5\u05DD);最后还有一个
元素在开角括号后立即以标识符开头。
如果此标识符的第一个字母是小写就像在这里的所有示例中一样那么该标识符将作为字符串传递给createElement()。
但如果标识符的第一个字母是大写则将其视为实际标识符并将该标识符的
值作为createElement()的第一个参数传递。
这意味着
来说将非字符串值作为createElement()的第一个参数传递的能力使得创建组件成为可能。
组件是一种编写简单
对象就像作为createElement()的第二个参数传递的对象一样。
例如这里是我们sidebar()函数的另一种写法
}这个新的Sidebar()函数与之前的sidebar()函数非常相似。
但这个函数以大写字母开头的名称并接受一个对象参数而不是单独的参数。
这使它成为一个
工具分析您的代码并报告类型错误。
一旦您修复了错误并准备运行代码您可以使用
需要承诺但我发现对于中大型项目来说额外的努力是值得的。
为代码添加类型注解每次编辑代码时运行
Flow以及修复它报告的类型错误都需要额外的时间。
但作为回报Flow
将强制执行良好的编码纪律并不允许你采取可能导致错误的捷径。
当我在使用
的项目上工作时我对它在我的代码中发现的错误数量感到印象深刻。
在这些问题变成错误之前修复这些问题是一种很棒的感觉并让我对我的代码正确性更有信心。
时我发现有时很难理解它为什么会抱怨我的代码。
然而通过一些实践我开始理解它的错误消息并发现通常很容易对我的代码进行微小更改使其更安全并满足
项目中将推动你将编程技能提升到下一个水平。
这也是为什么我将这本书的最后一节专门用于
Flow几乎肯定会花时间阅读https://flow.org上的文档。
另一方面您不需要在掌握
flow-bin的命令。
如果使用-g全局安装工具那么可以使用flow运行它。
如果在项目中使用--save-dev本地安装它那么可以使用npx
flow注释而“选择加入”类型检查的文件报告类型错误。
这种选择加入的行为很重要因为这意味着您可以为现有项目采用
Flow然后逐个文件地开始转换代码而不会受到尚未转换的文件上的错误和警告的困扰。
类型检查工具仍然可以推断程序中的值并在您不一致地使用它们时提醒您。
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
variableReassignment.js:6:3Cannot
here在这种情况下我们声明变量i并将一个对象赋给它。
然后我们再次使用i作为循环变量覆盖了对象。
Flow
注意到这一点并在我们尝试像仍然保存对象一样使用i时标记错误。
一个简单的修复方法是写for(let
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
看到size()函数接受一个参数。
它不知道该参数的类型但可以看到该参数应具有length属性。
当看到使用数字参数调用此size()函数时它会正确地标记此为错误因为数字没有length属性。
也会知道这些变量的类型它可以看到您为每个变量分配的值并跟踪它们。
但是如果添加了类型注释Flow
既知道变量的类型又知道您已表达了该变量应始终为该类型的意图。
因此如果使用类型注释如果将不同类型的值分配给该变量Flow
将标记错误。
对于变量类型注释也特别有用如果您倾向于在函数使用之前在函数顶部声明所有变量。
函数参数的类型注释与变量的注释类似在函数参数名称后面跟着冒号和类型名称。
在注释函数时通常还会为函数的返回类型添加注释。
这在函数体的右括号和左花括号之间。
返回空值的函数使用
在前面的示例中我们定义了一个期望具有length属性的参数的size()函数。
下面是如何将该函数更改为明确指定它期望一个字符串参数并返回一个数字。
请注意即使在这种情况下函数可以正常工作Flow
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
console.log(size([1,2,3]));使用箭头函数的类型注解也是可能的尽管这可能会将这个通常简洁的语法变得更冗长
类型void。
但这两个值都不是任何其他类型的成员除非你明确添加它。
如果你声明一个函数参数为字符串那么它必须是一个字符串传递null、传递undefined或省略参数基本上与传递undefined相同都是错误的
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
console.log(size(null));如果你想允许null和undefined作为变量或函数参数的合法值只需在类型前加上问号。
例如使用?string或?number代替string或number。
如果我们将size()函数更改为期望类型为?string的参数那么当我们将null传递给函数时Flow
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
在这里告诉我们的是在我们的代码中写s.length是不安全的因为此处的s可能是null或undefined而这些值没有length属性。
这就是
会坚持要求我们在执行任何依赖于该值不是null的操作之前检查该情况。
}当函数首次调用时参数可以有多种类型。
但通过添加类型检查代码我们在代码中创建了一个块Flow
可以确定参数是一个字符串。
当我们在该块内使用s.length时Flow
语法允许在任何类型规范之前加上问号以指示除了指定的类型外null和undefined也是允许的。
问号也可以出现在参数名后以指示参数本身是可选的。
因此如果我们将参数s的声明从s:
string那意味着可以用没有参数调用size()或值为undefined这与省略它相同但如果我们用除undefined之外的参数调用它那么该参数必须是一个字符串。
在这种情况下null不是合法值。
到目前为止我们已经讨论了原始类型string、number、boolean、null和void并演示了如何在变量声明、函数参数和函数返回值中使用它们。
接下来的小节描述了
的内置类并允许你使用类名作为类型。
例如以下函数使用类型注解指示应使用一个
}如果你使用class关键字定义自己的类那些类会自动成为有效的
确实要求你在类中使用类型注解。
特别是类的每个属性必须声明其类型。
这里是一个简单的复数类示例演示了这一点
类型看起来很像对象字面量只是属性值被属性类型替换。
例如这里是一个期望具有数字
在对象类型中你可以在任何属性名称后面加上问号表示该属性是可选的可以省略。
例如你可以这样写一个表示
number}如果在对象类型中未标记属性为可选则该属性是必需的如果实际值中缺少适当的属性Flow
严格执行对象除了在其类型中明确声明的属性之外没有其他属性你可以通过在花括号中添加竖线来声明精确对象类型
的对象有时被用作字典或字符串值映射。
当以这种方式使用对象时属性名称事先不知道也不能在
来描述数据结构。
假设你有一个对象其中属性是世界主要城市的名称这些属性的值是指定这些城市地理位置的对象。
你可以这样声明这个数据结构
类型将会很长且难以输入。
即使相对较短的对象类型也可能令人困惑因为它们看起来非常像对象字面量。
一旦我们超越了像
类型。
一旦你这样做了该标识符将成为该类型的别名。
例如这里是我们如何使用显式定义的
类型是一个复合类型还包括数组元素的类型。
例如这里是一个期望数字数组的函数以及如果尝试使用具有非数字元素的数组调用该函数时
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
Array后面跟着尖括号中的元素类型。
你也可以通过在元素类型后面加上开放和关闭方括号来表示数组类型。
因此在这个例子中我们可以写成
Arraynumber。
我更喜欢尖括号表示法因为正如我们将看到的还有其他使用这种尖括号语法的
类型语法适用于具有任意数量元素的数组所有元素都具有相同的类型。
Flow
有一种不同的语法来描述元组的类型一个具有固定数量元素的数组每个元素可能具有不同的类型。
要表示元组的类型只需写出每个元素的类型用逗号分隔然后将它们都括在方括号中。
的类型别名功能使得元组易于处理以至于你可能会考虑它们作为简单数据类型的替代方案
3);现在我们有了一种表达数组类型的方法让我们回到之前的size()函数并修改它以接受一个数组参数而不是一个字符串参数。
我们希望函数能够接受任意长度的数组因此元组类型不合适。
但我们也不希望将函数限制为仅适用于所有元素类型相同的数组。
解决方案是类型Arraymixed
console.log(size([1,true,three]));元素类型mixed表示数组的元素可以是任何类型。
如果我们的函数实际上对数组进行索引并尝试使用其中的任何元素Flow
将坚持要求我们使用typeof检查或其他测试来确定元素的类型然后再执行任何不安全的操作。
如果你愿意放弃类型检查也可以使用any代替mixed它允许你对数组的值做任何想做的事情而不必确保这些值是你期望的类型。
要求您还必须在尖括号内指定数组元素的类型。
这个额外的类型被称为类型参数而
类是一个元素集合就像数组一样你不能单独使用Set作为一种类型而是必须在尖括号内包含一个类型参数来指定集合中包含的值的类型。
尽管如果集合可能包含多种类型的值你可以使用mixed或any。
以下是一个示例
是另一种参数化类型。
在这种情况下必须指定两个类型参数键的类型和值的类型
类型对该类进行参数化。
我们在代码中使用占位符E和V来表示这些类型参数。
当这个类的用户声明一个
类型的变量时他们将指定实际类型来替换E和V。
变量声明可能如下所示
定义了一些特殊的参数化“实用类型”其名称以$开头。
这些类型中的大多数都有我们这里不打算涵盖的高级用例。
但其中两个在实践中非常有用。
如果你有一个对象类型
T并想要创建该类型的只读版本只需编写$ReadOnlyT。
类似地您可以编写$ReadOnlyArrayT来描述一个具有类型
使用这些类型的原因不是因为它们可以提供任何对象或数组不能被修改的保证如果你想要真正的只读对象请参见
Object.freeze()而是因为它可以帮助你捕捉由无意修改引起的错误。
如果你编写一个接受对象或数组参数并且不改变对象的任何属性或数组的元素的函数那么你可以用
的只读类型注释函数参数。
如果你这样做那么如果你忘记并意外修改输入值Flow
我们已经看到如何添加类型注释来指定函数参数和返回类型的类型。
但是当函数的一个参数本身是一个函数时我们需要能够指定该函数参数的类型。
表达函数的类型需要写出每个参数的类型用逗号分隔将它们括在括号中然后跟上一个箭头和函数的返回类型。
这里是一个期望传递回调函数的示例函数。
请注意我们为回调函数的类型定义了一个类型别名
函数。
创建一个什么都不做只返回数组长度的函数并没有太多意义。
数组有一个完全好用的
Map并返回集合中元素的数量那么它可能会有用。
在常规的未类型化
类型并允许你通过简单列出所需类型并用竖线字符分隔它们来表达它们
Arraymixed|Setmixed|Mapmixed,mixed):
不会允许你使用该值直到你进行足够的测试以确定实际值的类型。
在我们刚刚看过的
之外的任何值。
定义只有一个成员的类型通常不太有用但字面量类型的联合可能会有用。
你可能可以想象出这些类型的用途例如
0|1|2|3|4|5|6|7|8|9;如果你使用由字面量组成的类型你需要理解只有字面值是允许的
检查你的类型时它实际上并不执行计算它只检查计算的类型。
Flow
运算符在数字上返回一个数字。
尽管我们知道这两个计算返回的值都在类型内但
像Answer和Digit这样的字面类型的联合类型是枚举类型的一个例子。
枚举类型的一个典型用例是表示扑克牌的花色
Found新手程序员经常听到的建议之一是避免在代码中使用字面量而是定义符号常量来代表这些值。
这样做的一个实际原因是避免拼写错误的问题如果你拼错了一个字符串字面量比如“Diamonds”JavaScript
可能不会抱怨但你的代码可能无法正常工作。
另一方面如果你拼错了一个标识符JavaScript
字面类型的另一个重要用途是创建辨别联合体。
当你使用联合类型由实际不同类型组成而不是字面量时通常需要编写代码来区分可能的类型。
在前一节中我们编写了一个函数它可以接受一个数组、一个
输入。
如果你想创建一个对象类型的联合体可以通过在每个单独的对象类型中使用字面类型来使这些类型易于区分。
中使用工作线程§16.11并且正在使用postMessage()和“message”事件在主线程和工作线程之间发送基于对象的消息。
工作线程可能想要向主线程发送多种类型的消息但我们希望编写一个描述所有可能消息的
handleMessageFromReticulator(message:
property.console.log(message.result);}
here.console.info(message.splinesPerSecond);}
是当今世界上使用最广泛的编程语言。
它是一种活跃的语言不断发展和改进周围有着繁荣的库、工具和扩展生态系统。
本章介绍了其中一些工具和扩展但还有许多其他内容需要了解。
JavaScript
开发者社区活跃而充满活力同行们通过博客文章、视频和会议演讲分享他们的知识。
当你结束阅读这本书加入这个社区时你会发现有很多信息源可以让你与
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback