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模块系统的深入理解,开发者可以更有效地组织和管理项目代码,提高开发效率。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

react18中使用redux管理公共数据仓库实现数据immutable更新

Immutable.js出自Facebook,是最流行的不可变数据结构的实现之一。它实现了完全的持久化数据结构,使用结构共享。所有的更新操作都会返回新的值,但是在内部结构是共享的,来减少内存占用。Immutablejs官网 在上一篇介绍redux的文章&…

数字IC后端实现 | Innovus各个阶段常用命令汇总

应各位读者要求,小编最近按照Innovus流程顺序整理出数字IC后端项目中常用的命令汇总。限于篇幅,这次只更新到powerplan阶段。有了这份Innovus常用命令汇总,学习数字IC后端从此不再迷路!如果大家觉得这个专题还不错,想继…

实验:使用Oxygen发布大型手册到Word格式

此前,我曾发表过一篇文章《结构化文档发布的故事和性能调优》,文中讨论了在将大型DITA手册转换为PDF格式时可能遇到的性能挑战及相应的优化策略。 近日,有朋友咨询,若将同样的大型手册输出为MS Word格式,是否也会面临…

MongoDB Shell 基本命令(三)生成学生脚本信息和简单查询

一、生成学生信息脚本 利用该脚本可以生成任意个学生信息,包括学号、姓名、班级、年级、专业、课程名称、课程成绩等信息,此处生成2万名学生,学生所有信息都是给定范围后随机生成。 生成学生信息后,再来对学生信息进行简单查询。…

Java 开发——(上篇)从零开始搭建后端基础项目 Spring Boot 3 + MybatisPlus

一、概述 记录时间 [2024-10-23] 本文是一个基于 Spring Boot 3 MybatisPlus 的项目实战开发,主要涵盖以下几个方面: 从零开始的项目创建IDEA 中开发环境的热部署Maven、Swagger3、MybatisPlus 等的配置路由映射知识静态资源访问文件上传功能实现拦截器…

探寻闲鱼libsgmain加解密算法(4) ——JNI入口跳转

关注我的人都知道我一直在学习阿里的加密和算法,除了研究逆向问题,还会把学来的阿里技术用在自己的应用上。 为什么?因为学习大厂的应用,是进步最快的方法。而大厂在安全和加密方面的技术,个人觉得阿里做的是最好的。 …

个体能量的勇气层级是否容易达到?

没有勇气面对现实,没有勇气改变自我,没有勇气改变环境,没有勇气创新创造。 这是常态。 如何找寻高质量免费机器人工程资源自学提升-CSDN博客 个人能力的提升,也包括个体能量的提升。 个体能量是个人能力的一个非常重要的衡量指…

Spring Boot整合Stripe订阅支付指南

在当今的在线支付市场中,Stripe 作为一款一体化的全球支付平台,因其易用性和广泛的支付方式支持,得到了许多企业的青睐。本文将详细介绍如何在 Spring Boot 项目中整合 Stripe 实现订阅支付功能。 1.Stripe简介 Stripe 是一家为个人或公司提…

C语言实现Go的defer功能

之前笔者写了一篇博文C实现Go的defer功能,介绍了如何在C语言中实现Go的defer功能,那在C语言中是否也可以实现这样的功能呢?本文就将介绍一下如何在C语言中实现Go的defer功能。 我们还是使用C实现Go的defer功能中的示例: void te…

【每日一题】LeetCode - 判断回文数

今天我们来看一道经典的回文数题目,给定一个整数 x ,判断它是否是回文整数。如果 x 是一个回文数,则返回 true,否则返回 false。 回文数 是指从左往右读和从右往左读都相同的整数。例如,121 是回文,而 123 …

nuxt3项目创建

安装 npx nuxilatest init <project-name> 此时会出现报错&#xff0c;需要在host文件中加入 185.199.108.133 raw.githubusercontent.com 再次执行命令&#xff0c;进入安装 此处选择npm&#xff0c;出现下图表示安装成功 启动项目 执行npm run dev&#xff0c;访…

《皮革制作与环保科技》是什么级别的期刊?是正规期刊吗?能评职称吗?

​问题解答 问&#xff1a;《皮革制作与环保科技》是不是核心期刊&#xff1f; 答&#xff1a;不是&#xff0c;是知网收录的正规学术期刊。 问&#xff1a;《皮革制作与环保科技》级别&#xff1f; 答&#xff1a;国家级。主管单位&#xff1a;中国轻工业联合会 …

深度学习-循环神经网络-LSTM对序列数据进行预测

项目简介: 使用LSTM模型, 对文本数据进行预测, 每次截取字符20, 对第二十一个字符进行预测, LSTM层: units100, activationrelu Dense层: units输入的文本中的字符种类, 比如我使用的文本有644个不同的字符, 那么units64 激活函数: 因为是多分类, 使用softmax 因为这是最…

已解决 django.db.utils.OperationalError: (1051, “Unknown table

报错信息&#xff1a; django.db.utils.OperationalError: (1051, "Unknown table bjybolg.tool_submission")python manage.py migrate --fake 命令用于告诉 Django 假装已经应用某个迁移&#xff0c;而不实际执行该迁移的操作。这通常在以下情况下非常有用&#x…

【大模型理论篇】大模型压缩技术之注意力层剪枝以及与MLP层联合剪枝

1. 背景分析 本来打算写一篇关于大模型蒸馏的文章&#xff0c;但刚好看到近期发表的一篇讨论大模型压缩的文章【1】&#xff0c;是关于注意力机制冗余性的讨论&#xff0c;比较有意思&#xff0c;作者分析得出并不是所有的注意力都是必须的&#xff0c;可以通过对模型去除冗余的…

鸿蒙中富文本编辑与展示

富文本在鸿蒙系统如何展示和编辑的&#xff1f;在文章开头我们提出这个疑问&#xff0c;带着疑问来阅读这篇文章。 富文本用途可以展示图文混排的内容&#xff0c;在日常App 中非常常见&#xff0c;比如微博的发布与展示&#xff0c;朋友圈的发布与展示&#xff0c;都在使用富文…

Elasticsearch 中的高效按位匹配

作者&#xff1a;来自 Elastic Alexander Marquardt 探索在 Elasticsearch 中编码和匹配二进制数据的六种方法&#xff0c;包括术语编码&#xff08;我喜欢的方法&#xff09;、布尔编码、稀疏位位置编码、具有精确匹配的整数编码、具有脚本按位匹配的整数编码以及使用 ESQL 进…

Maven 不同环境灵活构建

需求: 使用 Maven根据不同的构建环境&#xff08;如开发、测试、生产&#xff09;来定义不同的配置&#xff0c;实现灵活的构建管理。 需要Demo项目的可以参考&#xff1a;我的demo项目 一、项目分层 一般的初创项目不会有特别多的配置文件&#xff0c;所以使用 spring.profile…

【333基于Java Web的考编论坛网站的设计与实现

毕 业 设 计&#xff08;论 文&#xff09; 考编论坛网站设计与实现 摘 要 传统办法管理信息首先需要花费的时间比较多&#xff0c;其次数据出错率比较高&#xff0c;而且对错误的数据进行更改也比较困难&#xff0c;最后&#xff0c;检索数据费事费力。因此&#xff0c;在计…

linux下gpio模拟spi三线时序

目录 前言一、配置内容二、驱动代码实现三、总结 前言 本笔记总结linux下使用gpio模拟spi时序的方法&#xff0c;基于arm64架构的一个SOC&#xff0c;linux内核版本为linux5.10.xxx&#xff0c;以驱动三线spi(时钟线sclk&#xff0c;片选cs&#xff0c;sdata数据读和写使用同一…