Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,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 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
《深入浅出Node.js》-读书笔记
不知不觉 Node 已经更新到第十个版本了,本人使用 Node 也有两年多时间,之前学习的东西一直零零散散,没有形成系统的知识体系,于是最近又抽时间回顾这本经典的 《深入浅出Node.js》,阅读的过程中,难免有些东西不易理解或者容易忘记,因此选择博客的形式记录。
李振
2021/11/26
5740
《深入浅出Node.js》-读书笔记
深聊Nodejs模块化
我们知道 JavaScript 这门语言诞生之初主要是为了完成网页上表单的一些规则校验以及动画制作,所以布兰登.艾奇(Brendan Eich)只花了一周多就把 JavaScript 设计出来了。可以说 JavaScript 从出生开始就带着许多缺陷和缺点,这一点一直被其他语言的编程者所嘲笑。随着 BS 开发模式渐渐地火了起来,JavaScript 所要承担的责任也越来越大,ECMA 接手标准化之后也渐渐的开始完善了起来。
coder2028
2022/10/18
1.6K0
深度阐述Nodejs模块机制
我们都知道Nodejs遵循的是CommonJS规范,当我们require('moduleA')时,模块是怎么通过名字或者路径获取到模块的呢?首先要聊一下模块引用、模块定义、模块标识三个概念。
coder2028
2022/09/28
6390
CommonJS
在js发展前期,它主要是在浏览器环境发光发热,由于ES规范规范化的时间比较早,所以涵盖的范畴比较小,但是在实际应用中,js的表现取决于宿主环境对ES规范的支持程度,随着web2.0的推进,HTML5崭露头角,它将web从网页时代带进了应用时代,并且在ES标准中出现了更多、更强大的api,在浏览器中也出现了更多、更强大的api供js调用,这需要感谢各大浏览器厂商对规范的大力支持,然而,浏览器的更新迭代和api的升级只出现在前端,后端的js规范却远远落后,对于js自身而言,它的规范依然是十分薄弱的,还存在一些严重的缺陷,比如:没有模块标准。
Karl Du
2023/10/20
2260
CommonJS
Node理论笔记:模块实现
这个笔记是基于《深入浅出nodeJs》的,这本书出版较早是基于v0.6.0版本的,而现在node已经更新到v10的版本了,所以很多东西可能在新的版本都已经不适用了,但这本书偏理论居多,这些思想应该不会变的,所以do it吧。
Ashen
2020/06/01
7410
深入浅出 Nodejs( 三 ):Nodejs 核心模块机制
该文介绍了如何使用Express框架构建Web应用程序。文章首先介绍了Express的基本概念,然后详细阐述了如何安装Express、编写Express应用程序以及使用Express路由、中间件和插件。最后,文章通过一个示例展示了如何构建一个基本的Web应用程序。
serena
2017/08/25
2.3K0
深入浅出 Nodejs( 三 ):Nodejs 核心模块机制
模块机制
· require()通过分析文件扩展名之后,可能没有查找到对应文件,得到一个目录
李才哥
2021/02/26
4850
模块机制
《深入浅出Node.js》:node的模块规范与模块实现
Node的目标是成为一个构建快速、可伸缩的网络应用平台,通过通信协议来组织许多Node,非常容易通过扩展来达成构建大型网络应用的目的。
前端_AWhile
2019/08/29
1.2K0
深入浅出NodeJS随记 (一)
yiuanli最近在研读书籍 深入浅出nodejs , 随手写下的一些笔记, 和大家分享~ 如有错误,欢迎指正~
邱邱邱邱yf
2021/12/10
6380
Node的模块儿查找机制
上篇文章主要分享了的一个commonJS规范的问题,那么今天接着昨天的话题继续聊一聊nodejs的模块儿查找机制
terrence386
2022/07/14
4490
「万字进阶」深入浅出 Commonjs 和 Es Module
今天我们来深度分析一下 Commonjs 和 Es Module,希望通过本文的学习,能够让大家彻底明白 Commonjs 和 Es Module 原理,能够一次性搞定面试中遇到的大部分有关 Commonjs 和 Es Module 的问题。
用户6835371
2021/09/03
2.4K0
「万字进阶」深入浅出 Commonjs 和 Es Module
浅谈 Node.js 模块机制及常见面试问题解答
Node.js 模块机制采用了 Commonjs 规范,弥补了当前 JavaScript 开发大型应用没有标准的缺陷,类似于 Java 中的类文件,Python 中的 import 机制,Node.js 中可以通过 module.exports、require 来导出和引入一个模块.
五月君
2019/08/29
1.5K0
浅谈 Node.js 模块机制及常见面试问题解答
Node.js的模块解析机制
Node.js的模块解析机制基于CommonJS规范,该规范定义了如何在JavaScript中实现模块功能。在Node.js中,每个文件都被视为一个独立的模块,拥有自己的作用域。模块之间通过require()函数来引入依赖,并通过exports或module.exports来导出模块成员。
jack.yang
2025/04/05
1220
带你重新认识Node
Ryan Dahl也曾评估过使用C、Lua、Haskell、Ruby等语言作为备选实现,得出以下结论:
前端LeBron
2021/12/08
7270
带你重新认识Node
NodeJS学习二CommonJS规范
Node程序由许多个模块组成,每个模块就是一个文件。Node模块采用了CommonJS规范。
空空云
2018/09/27
6110
深入学习 Node.js Module
Node.js 遵循 CommonJS规范,该规范的核心思想是允许模块通过 require 方法来同步加载所要依赖的其他模块,然后通过 exports 或 module.exports 来导出需要暴露的接口。CommonJS 规范是为了解决 JavaScript 的作用域问题而定义的模块形式,可以使每个模块它自身的命名空间中执行。
阿宝哥
2019/11/06
1.1K0
深入学习 Node.js Module
JavaScript模块化发展
简介 在最开始学习前端的时候只需要一个js文件就能玩转一个小的练手应用,但是随着自己不断的学习,ajax、jQuery等广泛应用,使得我们的代码量变得巨大,代码变得格外的混乱。现在迫切的需要我们将大段的代码分离开来。 前端最开始并没有像java中package概念以及import那样的引包工具。JavaScript源生代码是在ES6的时候才正式的引入import这个API,来调用其他文件。在这之前也同样出现了很多社区来实现模块化开发。 ---- 发展历程 注意下面会讲历史上面出现的一些类库,有一些现在已经没
pitaojin
2018/05/25
1.7K0
【Nodejs】838- Nodejs 模块化你所需要知道的事
我们知道,Node.js是基于CommonJS规范进行模块化管理的,模块化是面对复杂的业务场景不可或缺的工具,或许你经常使用它,但却从没有系统的了解过,所以今天我们来聊一聊Node.js模块化你所需要知道的一些事儿,一探Node.js模块化的面貌。
pingan8787
2021/01/28
5840
【Nodejs】838- Nodejs 模块化你所需要知道的事
Node.js学习笔记——模块加载机制及npm指令详解
模块化:是指解决一个复杂问题时,自顶向下逐层把系统划分成若干模块的过程。对于整个系统来说,模块是可组合、分解和更换的单元 编程领域中的模块化,就是遵守固定的规则,把一个大文件拆成独立并互相依赖的多个小模块。 把代码进行模块化拆分的好处
timerring
2022/07/20
1.2K0
Node.js学习笔记——模块加载机制及npm指令详解
【云+社区年度征文】webpack 学习笔记系列01-基础命令与常见配置
webpack 命令可以在 package.json 中的 script 字段中添加命令,再使用 npm 执行:
CS逍遥剑仙
2020/12/19
1.3K0
相关推荐
《深入浅出Node.js》-读书笔记
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验