前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >深入浅出 Nodejs( 二 ):Nodejs 文件模块机制

深入浅出 Nodejs( 二 ):Nodejs 文件模块机制

原创
作者头像
serena
修改于 2021-08-03 06:56:07
修改于 2021-08-03 06:56:07
2.5K00
代码可运行
举报
文章被收录于专栏:社区的朋友们社区的朋友们
运行总次数:0
代码可运行

作者:郭泽豪

本篇教程关于Nodejs的文件模块机制,具体讲CommonJs规范以及Nodejs文件模块的实现原理。

本章的重点内容:

  • CommonJs的模块规范,包括模块引用,模块定义以及模块标识
  • 核心模块与文件模块加载过程的区别
  • 文件模块加载过程中的路径分析、文件定位以及编译过程

一、CommonJs规范

1.1 CommonJs的出发点

CommonJs规范的提出对于Node的发展具有里程碑的意义,CommonJs规范为JavaScript制定一个美好的愿景,希望JavaScript能够在任何地方运行。从事JavaScript的开发者都知道ECMAScript,它是JavaScipt的官方规范,但是缺陷是ECMAScript规范涵盖的范畴非常小。

随着Web2.0时代的来临,在浏览器中出现了更多更强大的API给JavaScript使用,包括W3C组织对HTML5规范的推进以及各大浏览器产商对规范的大力支持,JavaScript的规范得到很好的发展,但是这些规范都局限在前端,后端JavaScript的规范却远远落后,直到CommonJs规范的出现。CommonJs规范涵盖了模块、二进制、Buffer、字符串编码、I/O流、进程环境、文件系统、套接字、单元测试、Web服务器网关接口、包管理等。

Node借鉴CommonJs的模块规范实现了一套非常易用的模块系统,NPM对于Packages规范的完成支持使得Node应用在开发中事半功倍。

1.2 CommonJs的模块规范

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

(1)模块引用

模块引用的示例代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var math = require('math');

在CommonJs规范中,存在require方法,这个方法接收一个模块标识,即math,以此引入一个模块的API到当前上下文中。

(2)模块定义

在模块中,上下文提供了require方法来引入外部模块。对应引入的功能,外部模块通过exports对象导出模块内定义的方法和对象,它是唯一导出的出口。在模块中,还存在一个module对象,它代表模块本身,而exports是module的属性。下面的示例是通过exports导出模块内定义的add方法,然后在program.js引入add模块并调用它的add方法。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//add.js
exports.add = function(){
    var sum = 0,
        i = 0,
        args = arguments,
        l = args.length;
    while(i < l){
        sum += args[i++];
    }
    return sum;
};


//program.js
var math = require('./add.js');
increment= function(val){
    return math.add(val, 1);
};
console.log(increment(5));

(3)模块标识

模块标识其实就是传递给require()方法的参数,它必须是小驼峰命名的字符串,或者是.、..开头的相对路径,或者绝对路径。它可以没有文件名后缀,我们后面会讲没有文件名后缀怎么找到对应的模块。

每个模块具有独立的空间,它们互不干扰,导出和引用都很简单。CommonJs构建的这套模块导出和引入机制使得用户完全不必考虑变量污染的问题。

二、Node的模块实现

2.1 模块加载过程

尽管规范中exports、require和module听起来十分简单,但是Node在实现它们的过程中究竟经历了什么,这个过程需要知晓。在Node中引入文件模块,需要经历如下路径分析、文件定位、编译执行3个步骤,但并不是全部模块都需要经历,比如C/C++扩展模块即.node文件没有编译的过程,因为.node本身就是编译后的文件。

在Node中,模块分为两类:一类是Node本身提供的模块,称为核心模块;另一类是用户编写的模块,叫文件模块。

(1)核心模块部分在Node源代码编译的过程中,编译进了二进制执行文件。在Node进程启动时,部分核心模块就被直接加载到内存中,所以这部分核心模块的引入时,文件定位和编译执行这两个步骤可以省略掉,并且在路径分析时会优先判断,所以它的加载速度是最快的。

(2)文件模块则是在运行时加载,需要完整的路径分析、文件定位、编译执行过程。模块引入的速度比核心模块要慢。

2.2 优先从缓存加载

不论是核心模块还是文件模块,require()方法对相同模块的二次加载都一律采用缓存优先的方式,这是第一优先级的。核心模块的缓存检查的优先级要先于文件模块缓存检查。注意Node缓存的是模块编译和执行后的对象,即module对象,我们后续会讲到它的数据结构。下面代码的输出结果是1和2,在第一次通过require()方法引入模块后,模块对象即cache变量就会缓存在内存中,当第二次引入同样的模块时,会从缓存中直接取出,缓存的key值是模块的完整文件路径。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//cache.js
var i = 0;
exports.add = function(){
    i++;
    return i;
};


//cache_program.js
var cache = require('./cache.js');
var i = cache.add();
console.log(i);
cache = require('./cache.js');
i = cache.add();
console.log(i);

2.3 路径分析和文件定位

2.3.1 模块标识符分析

因为标识符有几种形式,对于不同的标识符,模块的查找和定位有所不同。

(1)模块标识符分析

前面提到过,require()方法接受一个标识符作为参数。在Node实现中,正是基于这样一个标识符进行模块查找的。模块标识符在Node中主要分为以下几类:

  • 核心模块,如http、fs、path等
  • 或..开始的相对路径文件模块
  • 以/开始的绝对路径模块
  • 非路径形式的文件模块,如自定义的connect模块

核心模块的优先级仅次于缓存加载,它在Node源代码编译过程中已经编译为二进制代码,其加载速度最快。另外试图加载一个与核心模块同名的自定义模块,那是不会成功的,比如说想通过require(‘http’)引入自己定义的http模块,可以通过更改模块名或者标识符的路径来解决。

.、..、/开始的标识符,这里都被当做文件模块来处理。在分析文件模块时,require()方法会将路径转为真实路径,找到对应的文件后进行编译执行,执行后的结果会以真实完整路径为索引将编译执行后生成的模块对象存放在缓存中。

自定义模块指的是非核心模块,也不是路径形式的标识符。它是一种特殊的文件模块,可能是一个文件,也可能是一个包。这类文件的查找是最费时的。

在介绍自定义模块的查找之前,我们先介绍模块路径的概念,即node_modules。我们尝试创建module_path.js文件,其内容是console.log(module.paths),输出结果如下;

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[
'C:\\Users\\Administrator\\Desktop\\nodejs\\node_modules',

  'C:\\Users\\Administrator\\Desktop\\node_modules',

  'C:\\Users\\Administrator\\node_modules',

  'C:\\Users\\node_modules',

  'C:\\node_modules' ]

可以看出,模块路径的生成规则如下所示。

  • 当前文件目录下的node_modules目录
  • 父目录下的node_modules目录
  • 父目录的父目录下的node_modules目录
  • 沿路径向上逐级递归,直到根目录下的node_modules目录。

对于自定义模块,在加载的过程中,Node会逐个尝试模块路径中的路径,知道找到目标文件或目录为止。可以看出当前文件的路径越深,模块查找耗时越长,这是自定义模块加载速度最慢的原因。

(2)文件定位

在文件的定位中,还有一些细节需要注意,主要包括文件扩展名的分析、目录和包的处理。

文件扩展名分析,require()在分析标识符的过程中,会出现标识符不包含文件扩展名的情况。CommonJs模块规范也允许标识符不包含文件扩展名,这种情况下,Node会按.js、.node、.json的顺序补全扩展名,依次尝试。

在尝试的过程中,需要调用fs模块同步阻塞式地判断文件是否存在。因为Node是单线程的,所以这里的文件定位会引起性能问题。这里有一个小诀窍,如果是.node和.json文件,在传递给require()的标识符中带上扩展名会快一些。另一个小诀窍,同步配合缓存,可以大幅度缓解Node单线程中阻塞性调用的缺陷。

(3)目录分析和包

在分析标识符的过程中,require()通过分析文件扩展名之后,可能没有查找到对应的文件,但却得到一个目录,这是很常见的事,此时Node会将目录当做一个包来处理。

首先,Node在当前目录下查找package.json,通过JSON.parse()解析出包描述对象,从中取出main属性指定的文件名进行定位。如果文件名缺少扩展名,将会进入扩展名分析的步骤。如果main属性指定的文件名错误,或者压根没有package.json文件,Node会将index当做默认文件名,然后在当前目录下依次查找index.js,index.node,index.json。

如果在目录分析的过程中没有定位成功任何文件,则自定义模块进入下一个模块路径进行查找。如果模块路径数组都被遍历完毕,依然没有查找到目标文件,则会抛出查找失败的异常。

2.4 模块编译

在Node中,每个文件模块都是一个Module对象。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function Module(id, parent){
    this.id = id;//模块id
    this.exports = {};//导出的功能或对象
    this.parent = parent;//调用自身模块的父模块
    if(parent && parent.children){
        parent.children.push(this);
    }
    this.filename = null;
    this.loaded = false;
    this.children = [];
}

编译和执行是引入文件模块的最后一个阶段,定位到具体的文件后,Node会新建一个模块对象,然后根据路径加载文件并编译。对于不同的文件扩展名,其载入方法也有所不同,具体如下所示。

  • js文件。通过fs模块同步读取文件后编译执行。
  • node文件。这是用C/C++编写的扩展文件,通过dlopen()方法加载最后编译生成的文件。
  • json文件。通过fs模块同步读取文件后,用JSON.parse()解析返回结果。
  • 其余扩展名文件,它们都被当成.js文件载入。

每一个编译并执行成功的模块都会将其完整文件路径为索引缓存在Module._cache对象上,以提高二次引入的性能。

根据不同的文件扩展名,Node会调用不同的读取方式,如.json文件的调用如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//.json
Module._extension['.json'] = function(module, filename){
    var content = NativeModule.require('fs').readFileSync(filename, 'utf8');
    try{
        module.exports = JSON.parse(content);
    }catch(err){
        err.message = filename + ':' + err.message;
        throw err;
    }
};

在确定文件的扩展名后,Node将调用具体的编译方式将文件执行完返回给调用者。

(1)JavaScript模块的编译,即js文件

我们知道每个模块文件中存在require、exports、module这3个变量,但是它们在模块文件中并没有定义,那么它们从何而来呢?在Node的API文档中,每个模块中还有__filename__dirname这两个变量,它们又从何而来?其实在编译过程中,Node对获取的JavaScript文件内容进行头尾包装。在头部添加(function (exports, require, module, __filename, __dirname){我们的自定义脚本});,以刚才的add.js为例,一个正常的JavaScript文件会被包装成如下的样子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
(function (exports, require, module, __filename, __dirname){
    exports.add = function(){
    var sum = 0,
        i = 0,
        args = arguments,
        l = args.length;
    while(i < l){
        sum += args[i++];
    }
    return sum;
    };
});

这样每个模块文件之间都进行了作用于隔离。包装之后的代码会通过vm模块的runInThisContext()方法执行,返回一个具体的Function对象。最后把当前新建的模块对象的exports属性、require()方法、module(模块对象本身)以及在文件定位中得到的完整文件路径__filename和文件目录__dirname作为参数传递给这个Function执行。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var vm = require('vm');

var func = vm.runInThisContext('(function (exports, require, module, __filename, __dirname){\
    exports.add = function(){\
    var sum = 0,\
        i = 0,\
        args = arguments,\
        l = args.length;\
    while(i < l){\
        sum += args[i++];\
    }\
    return sum;\
    };\
});')
console.log(func);//[Function]

这就是这些变量没有在模块文件定义但却存在的原因。编译执行后,模块对象的exports属性被返回给调用方。exports属性上的任何方法和属性都可以被外部调用到,但是模块中的其余变量或属性则不可直接被调用。

(2)C/C++模块的编译,即.node文件

Node调用process.dlopen()方法进行加载和执行。在Node的架构下,dlopen()方法在windows和*nix平台下有不同的实现,通过libuv兼容层进行了封装。

实际上,.node的模块文件并不需要编译,因为它是C/C++源码编译生成的,dlopen()是跨平台的,在windows通过visualC++编译器编译生成,在nix通过gcc/g++编译器编译生成,.node文件在windows平台实际上是一个.dll文件,在nix平台是一个.so文件。下一篇教程会提及。所以.node文件只有加载和执行的过程。在执行的过程中,模块的exports对象与.node模块产生联系,然后将exports对象返回给调用者。

(3)JSON文件的编译,即.json文件

.json文件的编译是3种文件模块编译方式中最简单的。Node利用fs模块同步读取JSON文件的内容之后,调用JSON.parse()方法得到对象,然后将它赋值给模块对象的exports,供外部模块调用。

JSON文件在用作项目的配置文件时比较有用。如果你定义一个JSON文件作为配置,那就不必调用fs模块去异步读取和解析,直接调用require()引入即可。此外,还可以享受到模块缓存的好处。

作者:MIG无线合作开发部实习生marcozhguo

电子邮箱:446882229@qq.com

参考资料:

《深入浅出Nodejs》

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
在云端使用仿真软件做仿真,或许是一个不错的想法
随着工程公司在设计过程和整个产品开发过程中越来越多地使用仿真,对仿真资源的需求已经扩大。这给许多公司的IT基础设施带来了负担,也让小公司在没有自己的高性能计算(HPC)资源的情况下,苦苦寻找运行更复杂模拟的方法
开物小编
2021/04/28
2.4K1
在云端使用仿真软件做仿真,或许是一个不错的想法
云计算的未来发展趋势与优势,你是否了解?
随着数字化转型的加速,越来越多的企业开始选择云计算作为信息技术应用的基础设施。那么,云计算究竟有哪些优势?未来发展趋势又是怎样的呢?今天我给大家来介绍一下。
网络豆
2023/10/15
8280
云计算的未来发展趋势与优势,你是否了解?
组织优化云计算使用的五种方式
根据市场研究机构“科技商业研究(TBR) ”公司的调查,2010年公共云计算的市场规模约为200亿美元。到2020年,这一数字预计将达到近1700亿美元。这种惊人的增长是由许多趋势驱动的,而且不会很快
静一
2018/03/15
7960
组织优化云计算使用的五种方式
云计算成本管理的6个技巧
为了避免每月云计算支出超出组织的预期,组织可以使用容器,容量预购和更多的云成本管理策略来控制失控的云支出。 在云中运营组织的业务与在本地部署数据中心运行相比是根本不同的。当运营不同时,其成本的策略也不同。 从财务角度来说,数据中心需要大量的建筑资本支出,服务器和软件许可证的额外资本支出,以及为服务器和冷却系统供电以及维护和管理的虽然较小但重要的运营支出。 在云计算中没有资本支出。相反,却有显著的运营支出,用于服务器虚拟机实例,存储,网络流量,软件许可证和其他细节。 从成本管理的角度来看,将计算负载转移到云
静一
2018/03/28
1.4K0
云计算成本管理的6个技巧
今天聊聊云计算
产生         2006年谷歌推出了“Google 101计划”,并正式提出“云”的概念和理论。 云计算思想的产生:        传统模式下,企业建立一套IT系统不仅仅需要购买硬件等基础设施,还有买软件的许可证,需要专门的人员维护。当企业的规模扩大时还要继续升级各种软硬件设施以满足需要。对于企业来说,计算机等硬件和软件本身并非他们真正需要的(需要的是服务,它们仅仅是完成工作、提供效率的工具而已。对个人来说,我们想正常使用电脑需要安装许多软件,而许多软件是收费的,对不经常使用该软件的用户来说购
互联网金融打杂
2018/04/03
3.4K0
今天聊聊云计算
如何为高性能计算应用程序提供云原生体验
高性能计算(HPC)是企业获得创新能力、洞察力、商业竞争力的动力,是这个数字时代不可或缺的资源。例如,采用高性能计算(HPC)运行的复杂计算机模型来测量和分析近几十年天气变化,帮助改善预测并模拟气候变化和其他破坏性事件(如飓风)的影响。
静一
2019/07/22
9440
如何为高性能计算应用程序提供云原生体验
超越提升和迁移 充分利用云计算的七种方法
云计算技术为企业带来了更具成本效益的敏捷性和可扩展性的承诺,因此很多企业希望以最快的方式实现这一目标:云迁移。而一站式迁移可将企业数据中心现有的工作负载转移到云平台中,而无需进行任何更改。
静一
2020/09/07
4230
超越提升和迁移 充分利用云计算的七种方法
云计算对阵高性能计算:谁更具竞争力?
最近一段时间以来,高性能计算集群方案到底应该自主构建还是直接购买的争论可谓如火如荼,其部分原因在于原本属于市场空白的性能与软件生态系统关键性组成部分如今已经逐渐落实到位。 经过数年的发展演变,如今高性能计算在云环境下的可行性终于得到了一定程度的肯定——至少针对一部分应用程序是如此。在大型云服务供应商已经利用更为强大的网络与处理器方案向高性能计算作出了试探性延伸的同时,以Rescale公司为代表的其它厂商也开始通过自己的许可模式帮助独立软件开发商接触高性能计算代码,进而揭开长久以来蒙住高性能计算软件的这层神秘
静一
2018/03/22
1.1K0
云计算对阵高性能计算:谁更具竞争力?
如何避免云计算的成本超支
优化云计算成本是2018年调查中受访者的首要举措,其中58%的受访者将其列为首要的云计算优先级。尽管如此,研究发现只有少数受访者已经实施了自动化策略来优化云计算成本,例如关闭未使用的工作负载或选择成本较低的云平台或云区域。
静一
2018/09/25
1.3K0
如何避免云计算的成本超支
黄仁勋的速度与激情:让深度学习反哺科学计算
“众星捧月”。上榜项目让NVIDIA在最新的HPC TOP500榜单中显得格外亮眼——或者准确地说,是在“榜单背后”。
IT创事记
2022/06/16
4380
黄仁勋的速度与激情:让深度学习反哺科学计算
为什么越来越多的企业选择云计算?
企业信息化,这也算是一个老生常谈的话题了,整个中国业内前前后后应该喊了有十多年了。不过到目前为止,我国很多企业公司都还没真正形成一个完整的信息化框架,或者只是运用了一个简单财务或客户管理系统。甚至还有很多公司企业根本连基础简单的SaaS系统也没使用,也有很多公司企业的信息管理系统已经严重落后、陈旧,使用非常卡顿、不灵活。不止如此,目前还有许多问题制约着企业信息化的发展,诸如:
互联网小阿祥
2023/05/28
5220
为什么越来越多的企业选择云计算?
什么是云计算?
云计算 由作为托管外部服务在互联网上提供的硬件和软件资源组成,这些服务依赖于高级软件应用程序和服务器计算机的高端网络。
网络技术联盟站
2021/11/03
3.5K0
什么是云计算?
企业是否需要高性能计算?
随着成本的下降和用例的增加,高性能计算正在吸引各种类型和各种规模的新用户。其扩展选项包括基于超级计算机的高性能计算(HPC)系统、基于集群的高性能计算(HPC)以及基于云计算的高性能计算(HPC)服务。
静一
2019/10/15
7900
企业是否需要高性能计算?
简单介绍下:云计算
传统模式下,企业建立一套IT系统不仅仅需要购买硬件等基础设施,还有买软件的许可证,需要专门的人员维护。当企业的规模扩大时还要继续升级各种软硬件设施以满足需要。对于企业来说,计算机等硬件和软件本身并非他们真正需要的,它们仅仅是完成工作、提供效率的工具而已。对个人来说,我们想正常使用电脑需要安装许多软件,而许多软件是收费的,对不经常使用该软件的用户来说购买是非常不划算的。可不可以有这样的服务,能够提供我们需要的所有软件供我们租用?这样我们只需要在用时付少量“租金”即可“租用”到这些软件服务,为我们节省许多购买软硬件的资金。
PM吃瓜
2023/03/02
6990
简单介绍下:云计算
云计算对于互联网基础设施意味着什么
由于云计算在IT世界得到广泛应用,开发人员和组织需要考虑在负载平衡、集成、安全性等方面采用最新技术。 如今的商业环境是以移动性和云计算为中心,那些未能将业务迁移到云计算的组织可能会失去竞争优势。因此,组织对架构上的巨大变化需要深谋远虑,以更多的关注来进行管理。 虽然云迁移已经成为大多数企业的必要条件,但将数据存储在他人的服务器上,这是许多行业和组织采用云计算的障碍。当然在Dropbox中存储休闲度假之类的照片无关紧要,但将个人医疗、财务记录或交易存储在公共云等领域则令人担忧。人们知道管理、存储或传输这种
静一
2018/03/27
7300
云计算对于互联网基础设施意味着什么
什么是混合IT?
混合IT是一种企业计算模型,其中组织通过传统的内部IT系统提供一些资源,同时还将云计算服务的某种组合用于其他资源。混合IT云架构(无论是公共的还是私有的)的特点是基于需求的可扩展性、用户易于提供以及测量和计量的使用。
静一
2019/05/08
9810
什么是混合IT?
Autodesk CFD 2019 - 全面解决工业流体动力学问题的仿真工具+Autodesk CFD 全版本安装包
Autodesk CFD 2019是一款全面解决工业流体动力学问题的仿真工具。该软件可以模拟并分析流体力学、热传递、化学反应和旋转机械等领域,帮助工程师们更好地设计优化流体系统、优化产品性能和减少设计成本。
用户10313071
2023/04/07
8540
Autodesk CFD 2019 - 全面解决工业流体动力学问题的仿真工具+Autodesk CFD 全版本安装包
2020年云计算10大预测
云计算已经进入了一个成熟阶段,其特点是标准化和更强的跨平台兼容性。那么,云计算在未来一年里会有什么发展呢?尽管该行业的发展速度可能快得令人发狂,但宏观的趋势和过去发生的事件为未来的预测提供了依据。 以
SDNLAB
2020/02/21
1.9K0
2020年云计算10大预测
面对云计算 企业巨头镀“云”还是改变基因?
IBM从NiSource拿到6亿美元的外包合同,包括公有和私有云基础架构。惠普推出云计算产品组合Helion,以OpenStack为中心但也涵括很多硬件和服务。SAP正进行重组,部分原因是要 “成为云公司”,但大家都知道SAP的蓬勃发展是拜许可证和维护营运收入所赐。这些都是过去一个星期以来有关各企业科技巨头的消息。 我可以将上面的公司名字换成甲骨文、戴尔、EMC、思科或其他有关公司的名字,这些公司的主要营运收入来自硬件或软件,但他们都有意以云计算推动未来的增长。 诸如亚马逊AWS、谷歌甚至微软的公司大可嘲笑
静一
2018/03/19
1.1K0
面对云计算 企业巨头镀“云”还是改变基因?
关于AI与高性能计算加速融合,这里有英伟达最新的4个应用案例
李根 发自 安徽合肥 量子位 报道 | 公众号 QbitAI AI和高性能计算正在加速交织融合。 最近的例证来自2017中国高性能计算(HPC)年会,在会上,AI成为了最核心话题。 英伟达对此肯定
量子位
2018/03/26
1.2K0
关于AI与高性能计算加速融合,这里有英伟达最新的4个应用案例
推荐阅读
相关推荐
在云端使用仿真软件做仿真,或许是一个不错的想法
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验