发布于 2026-01-06 4 阅读
0

Node.js 内外详解 - 模块 API 重温……Node.js 究竟是什么?Node.js API 模块 我们才刚刚开始!DEV 全球展示挑战赛,由 Mux 呈现:展示你的项目!

Node.js 由内而外——模块 API 的重新发现……

Node.js究竟是什么?

Node.js API

模块

我们才刚刚起步!

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

这篇文章摘自我的博客,记得去看看博客上的最新内容哦😉

自2011 年首次发布以来Node.js极大地改变了JavaScript 开发及其应用场景,或者更确切地说,是彻底革新了JavaScript 开发。许多 Web 开发人员能够用自己喜欢的语言编写代码,并在服务器端运行,因此很快就意识到了该项目的巨大潜力。快进到 2019 年,Node.js 已成为整个编程市场中最受欢迎和使用最广泛的运行时之一。它速度快(得益于V8 引擎)、可靠且易于使用。此外,凭借其自身的NPM包管理器,它拥有全球最大的开源库和工具集合!仅凭这些事实就足以说明 Node.js 的流行程度。🚀

基于以上原因,在本系列文章“Node.js 内幕详解”中,我们将深入探索 Node.js 项目的方方面面。我们将详细讲解Node.js本质以及提供的API。随着 NPM 的兴起和 Node.js 框架的涌现,许多开发者更倾向于使用框架,而不是Node.js 本身提供的底层功能。👏 别误会,使用各种工具来提升开发体验当然无可厚非只是有时候,当你需要进一步提升性能或想要了解底层机制时,回归基础就显得尤为重要。遗憾的是,许多人在学习 Node.js 时会忽略这一步(与 Web 开发不同——JS、HTML 和 CSS 是初学者的必修课),直接使用各种框架,却并未真正理解它们及其真正的潜力。

既然介绍部分已经结束,正如我之前所说,我希望这个系列能以一种更易于接受的方式,为初学者提供一个更深入、更友好的Node.js入门指南。😉 希望你们喜欢,并且能学到一些新东西!

Node.js究竟是什么?

首先,我们来了解一些理论知识……不过一点也不枯燥。😃 Node.js 本身是一个JavaScript运行时环境。它是开源的,并且是跨平台的。它的开发始于 2009 年,第一个正式版本于 2011 年发布。它的理念很简单——让 JavaScript 能够在浏览器以外的环境中运行。这并非什么新鲜事,只是之前从未取得过如此巨大的成功。如今,Node.js 的开发由Node.js 基金会负责,并得到了众多贡献者的帮助。它被业内许多知名企业所使用,并且似乎一直在不断发展和完善。

作为运行时环境,Node.js 由V8 🔋提供支持——V8 是目前市场上速度最快的开源 JavaScript 引擎,最初由 Google创建。因此,它的工作方式与任何基于 Chromium 的浏览器类似。代码在一个事件循环中运行,并且是在单线程上执行。异步 I/O使其能够处理多个并发操作。⚡ 这种方法也有其缺点,但这些缺点与 JavaScript 本身的固有特性有关。

Node.js 还为开发者提供了大量的API。这些 API 允许开发者访问一些普通浏览器无法实现的功能。它们以模块的形式提供,作为处理核心功能的标准方式。这些 API 的功能非常丰富,涵盖了从文件系统访问和加密C++ 插件子进程V8 访问等方方面面。我们将在本系列的后续文章中逐一探讨这些功能。👍

随着 Node.js 的飞速发展,越来越多有趣的工具涌现出来。凭借其强大的架构,您可以创建服务器端代码CLI 工具实时应用程序,例如游戏、社交媒体等等!当然,这一切都离不开客户端的配合,而客户端也可以利用您现有的 JavaScript 知识编写!但我指的不仅仅是浏览器!基于 Node.js、受其启发或使用 Node.js 构建的工具,如ElectronNativeScript应运而生。它们的主要功能是让您能够使用 JavaScript 创建完全原生的桌面或移动应用程序!在我看来,这正是 Node.js 成功的关键——一种语言,统领一切!✊

Node.js API

本系列文章的剩余部分将着重探索 Node.js 的各种 API。我们将从ECMAScript模块API开始。我认为这对于入门来说是个不错的选择。😃 模块几乎无处不在,但你可能会惊讶于它们在 Node.js 中拥有如此多(可能)不为人知的功能。不过,如果你觉得这太简单了,别担心!我们将在后续文章中探索更高级的 API!另外,需要说明的是,本教程的其余部分将使用Node.js v10.15.3——最新的稳定LTS 版本。现在,让我们开始吧!🎉

模块

如今几乎所有 Web 开发人员都会使用某种模块系统来更好地组织代码。最流行的选择是 ES 模块(最新标准)和 CommonJS 格式(Node.js 使用的格式)。但 Node.js 模块系统远不止导入导出那么简单。😅 这就是我们接下来要探索的内容!

CommonJS

我们先来回顾一下CommonJS ( CJS ) 模块格式的规则——Node.js 的主要模块格式。在 Node.js 中,与其他任何基于 TypeScript 或 Babel 的前端工作流程不同,模块是真实存在的。你的导入和导出是在运行时解析的,而不是在任何转译步骤中解析的。你实际上得到的是一个真正的模块系统。😮 当然,这有利有弊。但是,转译仍然是一个不错的选择(尤其是在进行微优化并且不想在运行时解析模块时),你可以随时随地轻松地使用 Babel 或任何其他你想要的工具!😉

我猜很多人把CJS称为带有require() 语法的版本。这是因为这个特定的关键字可能是这种格式中最容易识别的符号。

进出口

导出时,您可以将值赋给module.exports对象对应的特定属性。对于第二个目的,您也可以使用exports对象本身——这是一个快捷方式。但请注意,在赋值时切勿混淆两者——导出功能无法处理这种情况!这是因为exports实际上,它是对 `Object` 的引用modules.exports,而 `Object` 的默认值为空对象

// module1.js
exports.numberValue = 10;
exports.stringValue = 'str';
Enter fullscreen mode Exit fullscreen mode
// module2.js
module.exports = () => {
   // code 
}
Enter fullscreen mode Exit fullscreen mode

请注意,箭头函数➡ 语法(以及许多其他ES-Next 特性由 Node.js(以及其他基于 V8 的浏览器)原生支持。

可以使用众所周知的require()语法进行导入:

const module1 = require('module1.js');
const module2 = require('module2.js');

module1.numberValue; // 10
module1.stringValue; // 'str'
module2();
Enter fullscreen mode Exit fullscreen mode

我认为大家都知道,上面的语法可以自由地用于导入核心 Node.js 模块(例如fspath)、位于相对路径(例如 `/etc/node/node` ./)、node_modules目录以及全局模块。此外,您可以省略.js.json.node(对于原生插件)文件扩展名。或者,您可以将index.js文件用作文件夹的主文件等等。这些都是与 JS 模块相关的常见操作。大多数情况下,这些细节都不会被注意到……⚡

包装器和全局变量

以上内容都只是基础知识。即使不深入了解,你也能轻松上手。但是,在本系列文章中,我们将深入探讨!因此,我们想知道它们究竟是require()什么moduleexports

在执行之前,每个导入模块中的代码都会被放入一个包装函数🌯中,看起来像这样:

((exports, require, module, __filename, __dirname) => {
    // module code
});
Enter fullscreen mode Exit fullscreen mode

这是一个非常重要的概念,理解它至关重要,原因有二:

  1. 所有看似全局变量以及不同模块顶层作用域中的其他用户自定义变量,实际上都只保留在模块内部的作用域中。你需要使用module.exports`/`exports才能将结果输出到外部。📤
  2. 这完美地展示了我们的require()函数和module对象实际来源于何处。它还巧妙地将函数包装器隐藏在看似全局变量的形式中,从而避免了开发者直接接触到它们。👍

综上所述,我认为现在正是探究我们顶级封装器各项参数实际作用的最佳时机:

  • 出口——仅指module.exports(如前所述);
  • require()函数用于导入模块。它还有一些自身的附加属性:
    • cache - 所有已加载模块的缓存对象(稍后会详细介绍);
    • main - 指向表示入口模块的Module 对象的引用;
    • resolve() - 返回使用相同参数 时,模块将从中导入的文件的确切文件名(完整路径):require()
      • paths() - 返回一个数组,其中包含查找所提供模块时搜索过的路径;
  • 模块- 指向表示当前模块的对象(模块实例)的引用:
    • children -给定模块中首次导入的模块数组;
    • exports - 用于从给定模块导出值的对象;
    • filename -给定模块的绝对路径;
    • id -给定模块的标识符。通常等于文件名(索引文件除外);
    • loaded - 指示模块是否已加载。当在不同位置(例如循环中)使用多个 require 语句时,这一点尤为重要。请确保它们首先已正确加载;
    • 父模块- 指向首先加载给定模块的模块的引用;
    • paths - 查找给定模块时要搜索的路径数组;
    • require() - 提供了一种调用 require 的方式,就像是从给定的模块中调用一样;
  • filename -模块的绝对路径;
  • dirname -模块的目录名称;

感觉有点像官方文档,对吧?😅 希望不会太糟糕。我尽量用比官方文档更易懂、更简洁的方式来呈现重点在于理解这些看似全局变量的来源和作用。你可能几乎用不到上面提到的任何属性。值得注意的例外情况(除了导入/导出语法之外)包括 ` __dirnameand`,__filename很多初学者可能不知道它们的来源和含义。好了,现在你知道了。😉

模块(特意大写)是上述所有模块实例都继承自的结构。Node.js 也允许你以核心模块的形式访问它😂 ( require('module'))。它的使用场景甚至比上面的 API 还要少,因为它只提供了两个额外的属性:

  • builtinModules - Node.js 内置模块名称数组;
  • createRequireFromPath() - 允许创建相对依赖关系,该关系从提供的路径(例如文件夹)开始解析。当需要从同一目录导入多个文件且不影响代码可读性时,此函数非常有用;

如你所见,上述属性都有其非常具体的使用场景。因此,我更倾向于将它们视为内部属性,而非通用属性。😅 不过,如果你正在开发一个 Node.js 框架……谁知道呢?🤔

缓存

关于模块,最后一点需要注意的是它们会被缓存。这会对模块的运行方式和实际加载性能产生巨大影响。模块一旦加载完毕,第二次访问时无需重新加载。取而代之的是,系统会使用其缓存版本(存储在 `<module>` 引用的对象中require.cache)。这不仅能提升性能,还会带来一些额外的、有时被人们忽略的副作用。你看,当一个模块首次加载(顺便一提,这就是模块的 `children` 和 `parent` 属性存在的原因,因为它们指示了模块之间的特定关系,即模块首次加载并被缓存的位置)、缓存并随后被访问时,它的所有代码都已被执行一次,并且该模块的所有导出项都会被传递到所有导入该模块的文件中。这使得一些巧妙的技巧成为可能,例如可以使用一个专门的模块来存储半全局变量(可以在任何地方导入和更改的值,从而影响其他模块)。🛸

当然,你可以通过修改require.cache对象并移除指定的模块(通过其 ID)来强制重新加载模块。但是,除非你确定这正是你想要的效果,否则并不推荐这样做。

ECMAScript 模块

到目前为止,我们只讨论了 CJS 模块。但是,正如许多 Web 开发者现在应该知道的那样,2015 年ES6引入了一个新的标准(现在也不算新了吧?😅),它被称为ECMAScript 模块(简称ESM)。正是 ES6 为我们带来了这种优秀importexport语法,并最终使其成为一个行业级标准!遗憾的是,正如我们在 Node.js 中看到的那样,即使在像 Node 这样活跃的开发环境中,旧标准(CJS、AMD 等)仍然占据主导地位。但是,随着Node v8.x的发布,这种情况终于发生了改变,该版本引入了对 ESM 的支持,尽管当时使用的是实验性标志☢(目前为止,该标志一直沿用到 v11.x)。但这并不妨碍我们更深入地了解 Node.js 中的 ESM,对吧?👍

使能够

尽管实验性功能可能不会给您带来困扰(除了某些功能仍需实现或改进之外),但它也带来了一些额外的要求。截至撰写本文时,ESM 尚未默认支持。您需要--experimental-modules在运行 Node 时使用相应的标志来正确启用它。此外,您的文件必须使用.mjs扩展名,才能通过 ESM 系统正确加载。👉

坦白说,ESM 在很大程度上向下兼容CJS(只有一些 API 差异),这意味着您可以轻松地通过 ESM 导入 CJS 模块。另一方面,您不能使用 CJS 语法导入 ESM 模块。这是不允许的,因为 CJS 使用了不同的解析方法时序(不向前兼容😅)。当然,JSON 文件和 C++ 模块/原生插件可以与 ESM 语法一起使用。

差异

除了跨平台兼容性之外,Node.js CJS 和 ESM 实现之间还有一些其他区别。ESM 的解析系统完全不同,它基于URLfile: 协议。这意味着,例如,您可以传递额外的查询参数来指示应该重新加载以下模块(而不是使用其缓存版本)。💾

import module from './module1.js?id=1';
import moduleClone from './module1.js?id=2';
Enter fullscreen mode Exit fullscreen mode

目前,外部URL尚无法使用。不过,根据上述示意图,在不久的将来或许可以实现。

URL 格式也用于标识缓存中的模块(这就是上面示例有效的原因)。但是,由于我们无法像在 CJS 中那样访问相同的值(例如 `<module>` require()module`<module>` 等),缓存对象是单独存储的。此外,与 CJS 不同,ESM不会解析 `NODE_PATH`,这意味着无法导入全局安装的模块。

最后,就目前状态而言,import它提供了一个自身的属性。它是一个名为 `<module_name>` 的对象import.meta,该对象同样有一个名为 `<module_name>` 的属性import.meta.url,用于指示当前模块的绝对 URL 。

import.meta.url
Enter fullscreen mode Exit fullscreen mode

钩子

Node.js ESM 的最后一个新特性叫做加载器钩子(loader hooks)。⚡ 顾名思义,这些钩子允许你用自定义代码拦截 ESM 模块的加载过程。👏

您可以使用两个钩子:`on_hook`resolve()和 `on_hook` 。您可以将其中一个或两个钩子以异步函数dynamicInstantiate()的形式放在一个单独的 JS 文件中。之后,您可以使用简单的命令行参数加载和使用它们:

node --experimental-modules --loader ./loader.mjs ./index.mjs
Enter fullscreen mode Exit fullscreen mode

resolve()钩子函数接受 3 个参数:

  • 指定符-当前模块文件的绝对路径;
  • parentModuleURL -父模块的 URL(首先加载给定模块的模块)。它遵循 file: 协议,当用于入口模块时(没有父模块),默认值为 undefined;
  • defaultResolve() - 默认解析函数

经过适当处理后,您的解析钩子应该返回一个包含两个属性的对象:` file`url和 `module` format。第一个属性指示已处理模块的解析 URL(文件:),第二个属性指示模块的格式。📦 ` urlfile` 属性很容易理解,而 `module`format属性则是一个字符串,包含 6 个可能的值:

  • “esm”表示ESM模块;
  • “cjs”表示CJS模块;
  • “builtin” - 表示 Node.js 内置模块,例如 http 或 path;
  • "json"表示 JSON 文件;
  • “addon” ——表示 C++ 原生插件;
  • “dynamic”表示使用dynamicInstantiate钩子;

dynamicInstantiate()钩子允许您正确处理带有"dynamic"特定格式的模块。钩子本身是一个异步函数,它接受一个url参数(被处理模块的 URL),并应返回一个包含两个属性的对象:

  • exports - 导出属性的名称数组;
  • execute() - 函数以上述导出对象作为参数。它应该访问导出对象上先前定义的属性名称,并相应地使用 .get() 和 .set() 方法对其进行交互。它将在模块求值时执行

一般来说,这个钩子允许你为需要特殊格式的模块(例如,不同的文件扩展名)提供另一种格式。请记住,它不仅限于设置完全不同的属性——你可以使用提供的 URL 以你想要的方式加载和执行文件。正如编程中一贯的做法——选择几乎是无穷无尽的!😉

我们才刚刚起步!

是的,好久没更新了,我们才讲到模块 API——仅仅是基础模块而已!看似简单,实则蕴含着丰富的知识!🤔 别担心,我们还有更多精彩内容!我计划接下来讲解文件系统 API(这可是个大工程!),不过你们或许想看点别的?我非常乐意听取大家的意见!记住,我最终的目标是讲解所有 Node.js API!

所以,请在评论区告诉我你对这篇文章的看法,以及你接下来想看到什么!也请分享这篇文章给其他人,让更多人看到!😃 和往​​常一样,欢迎在TwitterFacebook上关注我 ,也欢迎访问我的个人博客,随时了解本系列文章和其他精彩的JS内容!🛸 感谢阅读,我们下篇文章见!✌

文章来源:https://dev.to/areknawo/node-js-inside-out-modules-api-rediscovery-3h2g