Node.js:深入探秘 CommonJS 模块化的奥秘

在Node.js出现之前,服务端JavaScript基本上处于一片荒芜的境况,而当时也没有出现ES6的模块化规范。因此,Node.js采用了当时比较先进的一种模块化规范来实现服务端JavaScript的模块化机制,它就是CommonJS,有时也简称为CJS。

        本文由Node.js部署神器-Servbay 工具赞助,开发环境管理神器!3分钟部署好你的项目开发环境。

一、CommonJS规范

在Node.js采用CommonJS规范之前,还存在以下缺点:

  • 没有模块系统
  • 标准库很少
  • 没有标准接口
  • 缺乏包管理系统

这些问题的存在导致Node.js难以构建大型项目,生态环境也十分贫乏,亟待解决。CommonJS的提出主要是为了弥补当前JavaScript没有模块化标准的缺陷,以达到像Java、Python、Ruby那样能够构建大型应用的阶段,而不是仅仅作为一门脚本语言。Node.js能够拥有今天这样繁荣的生态系统,CommonJS功不可没。

1.1 CommonJS的模块化规范

CommonJS对模块的定义十分简单,主要分为模块引用、模块定义和模块标识三个部分。

1.1.1 模块引用

示例如下:

const fs = require('fs');

在CommonJS规范中,存在一个require全局方法,它接受一个标识,然后把标识对应的模块的API引入到当前模块作用域中。

1.1.2 模块定义

在Node.js上下文环境中提供了一个module对象和一个exports对象。module代表当前模块,exports是当前模块的一个属性,代表要导出的一些API。一个文件就是一个模块,把方法或者变量作为属性挂载在exports对象上即可将其作为模块的一部分进行导出。

// add.js
exports.add = function(a, b) {return a + b;
};

在另一个文件中,我们可以通过require引入之前定义的这个模块:

const { add } = require('./add.js');
add(1, 2); // 输出 3
1.1.3 模块标识

模块标识就是传递给require函数的参数,在Node.js中就是模块的id。它必须是符合小驼峰命名的字符串,或者是以...开头的相对路径,或者绝对路径,可以不带后缀名。

模块的定义十分简单,接口也很简洁。它的意义在于将类聚的方法和变量限定在私有的作用域中,同时支持引入和导出功能以顺畅的连接上下游依赖。CommonJS这套模块导出和引入的机制使得用户完全不必考虑变量污染。

二、Node.js的模块化实现

Node.js在实现中并没有完全按照规范实现,而是对模块规范进行了一定的取舍,同时也增加了一些自身需要的特性。接下来我们会探究一下Node.js是如何实现CommonJS规范的。

在Node.js中引入模块会经过以下三个步骤:

  1. 路径分析
  2. 文件定位
  3. 编译执行

在了解具体的内容之前我们先了解两个概念:

  • 核心模块:Node.js提供的内置模块,比如fsurlhttp等。
  • 文件模块:用户自己编写的模块,比如Koa、Express等。

核心模块在Node.js源代码的编译过程中已经编译进了二进制文件,Node.js启动时会被直接加载到内存中,所以在我们引入这些模块的时候就省去了文件定位、编译执行这两个步骤,加载速度比文件模块要快很多。

文件模块是在运行的时候动态加载,需要走一套完整的流程:路径分析、文件定位、编译执行等,所以文件模块的加载速度比核心模块要慢。

2.1 优先从缓存加载

在讲解具体的加载步骤之前,我们应当知晓的一点是,Node.js对于已经加载过一遍的模块会进行缓存,模块的内容会被缓存到内存当中,如果下次加载了同一个模块的话,就会从内存中直接取出来,这样就省去了第二次路径分析、文件定位、加载执行的过程,大大提高了加载速度。无论是核心模块还是文件模块,require()对同一文件的第二次加载都一律会采用缓存优先的方式,这是第一优先级的。但是核心模块的缓存检查优先于文件模块的缓存检查。

我们在Node.js文件中所使用的require函数,实际上就是在Node.js项目中的lib/internal/modules/cjs/loader.js所定义的Module.prototype.require函数,只不过在后面的makeRequireFunction函数中还会进行一层封装,Module.prototype.require源码如下:

Module.prototype.require = function(id) {validateString(id, 'id');if (id === '') {throw new ERR_INVALID_ARG_VALUE('id', id, 'must be a non-empty string');}requireDepth++;try {return Module._load(id, this, /* isMain */ false);} finally {requireDepth--;}
};

可以看到它最终使用了Module._load方法来加载我们的标识符所指定的模块,找到Module._load

Module._cache = Object.create(null);// Check the cache for the requested file.
Module._load = function(request, parent, isMain) {let relResolveCacheIdentifier;if (parent) {const filename = relativeResolveCache[relResolveCacheIdentifier];if (filename !== undefined) {const cachedModule = Module._cache[filename];if (cachedModule !== undefined) {updateChildren(parent, cachedModule, true);return cachedModule.exports;}delete relativeResolveCache[relResolveCacheIdentifier];}}const filename = Module._resolveFilename(request, parent, isMain);const cachedModule = Module._cache[filename];if (cachedModule !== undefined) {updateChildren(parent, cachedModule, true);return cachedModule.exports;}const mod = loadNativeModule(filename, request, experimentalModules);if (mod && mod.canBeRequiredByUsers) return mod.exports;const module = new Module(filename, parent);if (isMain) {process.mainModule = module;module.id = '.';}Module._cache[filename] = module;if (parent !== undefined) {relativeResolveCache[relResolveCacheIdentifier] = filename;}let threw = true;try {module.load(filename);threw = false;} finally {if (threw) {delete Module._cache[filename];if (parent !== undefined) {delete relativeResolveCache[relResolveCacheIdentifier];}}}return module.exports;
};

Node.js先会根据模块信息解析出文件路径和文件名,然后以文件名作为Module._cache对象的键查询该文件是否已经被缓存,如果已经被缓存的话,直接返回缓存对象的exports属性。否则就会使用Module._resolveFilename重新解析文件名,再查询一遍缓存对象。否则就会当做核心模块来加载,核心模块使用loadNativeModule方法进行加载。

如果经过了以上几个步骤之后,在缓存中仍然找不到require加载的模块对象,那么就使用Module构造方法重新构造一个新的模块对象。加载完毕之后还会缓存到Module._cache对象中,以便下一次加载的时候可以直接从缓存中取到。

2.2 路径分析与文件定位

在Node.js中,路径分析与文件定位是通过Module._resolveFilename方法来实现的。该方法会根据传入的模块标识符和当前模块路径,确定模块文件的完整路径。

  1. 路径分析

    如果模块标识符是核心模块的名称,例如fshttp等,那么Module._resolveFilename会直接返回该核心模块的名称,而不需进一步分析。

  2. 文件定位

    如果是文件模块,Node.js会按照以下顺序进行文件定位:

    • 相对路径:如果标识符以./../开头,Node.js会将其视为相对路径,从当前模块文件所在目录开始解析。
    • 绝对路径:如果标识符以/开头,Node.js会将其视为绝对路径。
    • 模块路径:如果标识符不是以./开头,Node.js会将其视为一个模块路径,按顺序在node_modules目录中查找。

    Node.js会尝试为文件模块添加.js.json.node后缀进行匹配,直到找到一个存在的文件为止。

2.3 编译执行

一旦文件定位完成,Node.js会根据文件扩展名选择不同的编译执行策略:

  • JavaScript 文件:通过fs模块读取文件内容,并使用vm模块将内容包装在一个函数中执行。
  • JSON 文件:通过fs模块读取文件内容,并使用JSON.parse解析。
  • C/C++ 扩展文件:使用process.dlopen加载并执行。

Node.js将模块的内容包装在一个函数中,以提供模块作用域隔离。这个函数接收exportsrequiremodule__filename__dirname作为参数,使得模块内部可以使用这些变量。

三、模块加载优化与扩展

3.1 模块缓存

如前所述,Node.js使用Module._cache缓存已加载的模块,以提高加载速度。缓存机制确保每个模块文件在一次加载后,后续的加载请求都能直接从缓存中获取,避免重复加载。

3.2 扩展模块加载

Node.js允许用户自定义模块加载行为,通过require.extensions扩展模块加载方式。虽然不推荐在生产环境中使用,但在某些场景下可以用于加载自定义格式的文件。

require.extensions['.txt'] = function(module, filename) {const content = fs.readFileSync(filename, 'utf8');module.exports = content;
};

上面的代码示例展示了如何扩展.txt文件的加载方式,使得文本文件可以被require引入。

3.3 包装与作用域

在Node.js中,每个模块的代码实际上都被包装在一个函数中。这个函数提供了模块作用域隔离,防止变量污染全局作用域。模块包装器类似于以下形式:

(function(exports, require, module, __filename, __dirname) {// 模块代码在这里
});

这种机制确保每个模块都有自己的私有作用域,同时可以通过exports对象导出模块接口。

四、核心模块与文件模块的区别

  1. 加载速度

    核心模块在Node.js启动时已经加载到内存中,可以立即使用,加载速度非常快。文件模块需要经过路径解析、文件定位和编译执行等步骤,速度相对较慢。

  2. 优先级

    在解析模块标识符时,Node.js会优先检查核心模块。如果标识符匹配核心模块,则直接返回核心模块,而不进行文件系统操作。

  3. 缓存机制

    核心模块和文件模块都使用缓存机制,但核心模块的缓存检查优先于文件模块。

五、总结

Node.js的模块系统基于CommonJS规范,但在实现上进行了优化和扩展。通过模块缓存、路径解析、文件定位和编译执行等机制,Node.js实现了高效的模块加载。同时,Node.js的模块系统支持自定义扩展,允许开发者根据需要调整模块加载行为。

这种模块化设计不仅提升了代码的可维护性和可复用性,还支持了Node.js在服务器端的广泛应用。通过对Node.js模块系统的深入理解,开发者可以更有效地组织和管理项目代码,提高开发效率。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/56398.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2024ideaUI切换和svn与git的切换,svn的安装和配置,idea集成svn ,2024-10-18日

2024-10-18日 2024的UI实在很不舒服,隐藏了很多按键; 第一步: 视图 -》 外观 -》 工具栏选出来; 结果出来: 运行的按键和设置的按钮 第二步 点击设置的按钮,选择最后一个,重启就行 结果 舒服&…

论文阅读(二十四):SA-Net: Shuffle Attention for Deep Convolutional Neural Networks

文章目录 Abstract1.Introduction2.Shuffle Attention3.Code 论文:SA-Net:Shuffle Attention for Deep Convolutional Neural Networks(SA-Net:置换注意力机制)   论文链接:SA-Net:Shuffle Attention for Deep Convo…

九州未来亓绚亮相丽台Solution Day 2024,共建AI赋能教育新时代

在数字化浪潮席卷全球的当下,生成式人工智能正迅速渗透至数字世界的每一个角落,而AI技术的物理化应用也正成为新的趋势。10月22日,丽台解决方案日Solution Day 2024:物理AI推动行业数字变革在上海绿地外滩中心顺利举行。 大会聚焦…

报表工具怎么选?山海鲸VS帆软,哪个更适合你?

概述 在国产报表软件市场中,山海鲸报表和帆软这两款工具都占有一席之地,许多企业在选择报表工具时常常在它们之间徘徊。然而,随着企业对数据分析需求的不断增长和复杂化,如何选取一款高效、易用且性价比高的报表工具,…

“摄像机”跟随及攻击抖动实现

学习Unity的摄像机功能,可以帮助我们实现摄像机对人物的跟随移动,还可以使用这个工具自带的插件,摄像机震动,颤动,增强打击感; 首先来安装一下这个插件,window菜单--packageManage--左上角Unit…

vcpkg 从清单文件安装依赖项

vcpkg 有两种运行模式:经典模式和清单模式。清单文件有自己的 vcpkg_installed 目录,可在其中安装依赖项,与所有包都安装在通用 %VCPKG_ROOT%/installed 目录中的经典模式不同。 因此,每个项目都可以有自己的清单和自己的一组依赖…

R语言机器学习算法实战系列(十)自适应提升分类算法 (Adaptive Boosting)

禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍原理步骤教程下载数据加载R包导入数据数据预处理数据描述数据切割调节参数构建模型预测测试数据评估模型模型准确性混淆矩阵模型评估指标ROC CurvePRC Curve特征的重要性保存模型总…

生发产品哪个效果最好?油秃头秋冬季养发搭子

如果你是大额头 或者 M型发际线,无论是天生的 亦或者是后天造成的,养发防脱一定要重视起来,因为防脱育发是需要循序渐进坚持的,今天就给大家分享一下几个特别有效的育发液,选对产品养发那真是稳了~ 1、露卡菲娅防脱育发…

Unity之XR Interaction Toolkit 射线拖拽3DUI

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、想实现的功能二、实现原理1.UI挂在XRGrabInteractable、刚体、BoxCollder2.修改刚体属性3.加BoxCollder 总结 前言 VR项目里正常情况有放置两种3DUI的方式…

Ovis: 多模态大语言模型的结构化嵌入对齐

论文题目:Ovis: Structural Embedding Alignment for Multimodal Large Language Model 论文地址:https://arxiv.org/pdf/2405.20797 github地址:https://github.com/AIDC-AI/Ovis/?tabreadme-ov-file 今天,我将分享一项重要的研…

关于使用 C# 处理水位数据多种格式的统一转换

关于使用 C# 处理水位数据多种格式的统一转换 1、前言2、水位数据的多种格式3、水位数据多种格式的统一转换程序展示4、水位数据多种格式的统一转换 C# 代码4.1、声明引用命名空间4.2、多种格式的统一转换 C# 代码4.3、多种格式的统一转换 C# 代码,文件输出保存 1、…

微知-Lecroy力科的PCIe协议分析仪型号命名规则(PCIe代,金手指lanes数量)

文章目录 要点主要型号命名规则各代主要产品图片Summit M616 协议分析仪/训练器Summit T516 分析仪Summit T416 分析仪Summit T3-16分析仪Summit T28 分析仪 综述 要点 LeCroy(力科)成立于1964年,是一家专业生产示波器厂家。在美国纽约。一直把重点放在研制改善生产…

Hallo2 长视频和高分辨率的音频驱动的肖像图像动画 (数字人技术)

HALLO2: LONG-DURATION AND HIGH-RESOLUTION AUDIO-DRIVEN PORTRAIT IMAGE ANIMATION 论文:https://arxiv.org/abs/2410.07718 代码:https://github.com/fudan-generative-vision/hallo2 模型:https://huggingface.co/fudan-generative-ai/h…

TikTok营销实用技巧与数据分析工具:视频洞察

TikTok凭借其独特的机制和庞大的流量,成为了众多品牌和卖家对产品进行宣传推广的必要平台之一。要在TikTok上优化营销效果、提升推广效率,可以使用平台提供的重要工具——视频洞察(Video Insights)。 一、视频洞察功能与技巧 视频…

线性回归(一)

线性回归 1.基本术语 ①特征:预测所依据的自变量称为特征或协变量 ②标签:试图预测的目标称为标签或目标 2.举个栗子 线性假设是指目标(房屋价格)可以表示为特征(面积和房龄)的加权和,如下面…

YOLOv11入门到入土使用教程(含结构图)

一、简介 YOLOv11是Ultralytics公司在之前的YOLO版本上推出的最新一代实时目标检测器,支持目标检测、追踪、实力分割、图像分类和姿态估计等任务。官方代码:ultralytics/ultralytics:ultralytics YOLO11 🚀 (github.com)https://g…

解决跨域问题

跨域是浏览器受同源策略的限制,同源策略是浏览器为确保资源安全,而遵循的一种策略,该策略对访问资源进行了一些限制(如发送 ajax 请求,操作 dom,读取 cookie)。 最常见的影响就是发送 ajax 请求…

【微知】如何通过命令行在非串口界面触发sysrq的help信息?(echo h > /proc/sysrq-trigger)

背景 在服务器上,触发sysrq通常需要在串口执行sysrq热键,比如 ~相关的操作 如何通过在ssh界面触发sysrq触发一些操作? 命令 通过sysrq指定的/proc接口文件进行操作 echo h > /proc/sysrq-trigger dmesg #产看输出的帮助信息然后根据打…

Junit + Mockito保姆级集成测试实践

一、做好单测,慢即是快 对于单元测试的看法,业界同仁理解多有不同,尤其是在业务变化快速的互联网行业,通常的问题主要有,必须要做吗?做到多少合适?现在没做不也挺好的吗?甚至一些大…

MYSQL-SQL-01-DDL(Data Definition Language,数据定义语言)

DDL(数据定义语言) DDL(Data Definition Language),数据定义语言,用来定义数据库对象(数据库,表,字段) 。 一、数据库操作 1、 查询mysql数据库管理系统的所有数据库 语法&#…