深入了解Nodejs模块机制

深入了解Nodejs模块机制

我们都知道Nodejs遵循的是CommonJS规范,当我们require('moduleA')时,模块是怎么通过名字或者路径获取到模块的呢?首先要聊一下模块引用、模块定义、模块标识三个概念。

CommonJS规范

1.1 模块引用

模块上下文提供require()方法来引入外部模块,看似简单的require函数, 其实内部做了大量工作。示例代码如下:

//test.js //引入一个模块到当前上下文中 
const math = require('math'); math.add(1, 2);`

1.2 模块定义

模块上下文提供了exports对象用于导入导出当前模块的方法或者变量,并且它是唯一的导出出口。模块中存在一个module对象,它代表模块自身,exports是module的属性。一个文件就是一个模块,将方法作为属性挂载在exports上就可以定义导出的方式:

//math.js
exports.add = function () {     
let sum = 0, i = 0, args = arguments, l = args.length;    while(i < l) {      
	sum += args[i++];  
}
return sum; }

这样就可像test.js里那样在require()之后调用模块的属性或者方法了。

1.3 模块标识

模块标识就是传递给require()方法的参数,它必须是符合小驼峰命名的字符串,或者以...开头的相对路径或者绝对路径,可以没有文件后缀名.js.

2. Node的模块实现

在Node中引入模块,需要经历如下四个步骤:

  • 路径分析
  • 文件定位
  • 编译执行
  • 加入内存

2.1 路径分析

Node.js中模块可以通过文件路径或名字获取模块的引用。模块的引用会映射到一个js文件路径。 在Node中模块分为两类:

  • 一是Node提供的模块,称为核心模块(内置模块),内置模块公开了一些常用的API给开发者,并且它们在Node进程开始的时候就预加载了。
  • 另一类是用户编写的模块,称为文件模块。如通过NPM安装的第三方模块(third-party modules)或本地模块(local modules),每个模块都会暴露一个公开的API。以便开发者可以导入。如
const mod = require('module_name') const { methodA } = require('module_name')

执行后,Node内部会载入内置模块或通过NPM安装的模块。require函数会返回一个对象,该对象公开的API可能是函数、对象或者属性如函数、数组甚至任意类型的JS对象。

核心模块是Node源码在编译过程中编译进了二进制执行文件。在Node启动时这些模块就被加载进内存中,所以核心模块引入时省去了文件定位和编译执行两个步骤,并且在路径分析中优先判断,因此核心模块的加载速度是最快的。文件模块则是在运行时动态加载,速度比核心模块慢。

这里列下node模块的载入及缓存机制:

1、载入内置模块(A Core Module)

2、载入文件模块(A File Module)

3、载入文件目录模块(A Folder Module)

4、载入node_modules里的模块

5、自动缓存已载入模块

1、载入内置模块

Node的内置模块被编译为二进制形式,引用时直接使用名字而非文件路径。当第三方的模块和内置模块同名时,内置模块将覆盖第三方同名模块。因此命名时需要注意不要和内置模块同名。如获取一个http模块

const http = require('http')

返回的http即是实现了HTTP功能Node的内置模块。

2、载入文件模块
绝对路径的

const myMod = require('/home/base/my_mod')

或相对路径的

const myMod = require('./my_mod')

注意,这里忽略了扩展名.js,以下是对等的

const myMod = require('./my_mod')
const myMod = require('./my_mod.js')

3、载入文件目录模块

可以直接require一个目录,假设有一个目录名为folder,如

const myMod = require('./folder')

此时,Node将搜索整个folder目录,Node会假设folder为一个包并试图找到包定义文件package.json。如果folder目录里没有包含package.json文件,Node会假设默认主文件为index.js,即会加载index.js。如果index.js也不存在, 那么加载将失败。

4、载入node_modules里的模块

如果模块名不是路径,也不是内置模块,Node将试图去当前目录的node_modules文件夹里搜索。如果当前目录的node_modules里没有找到,Node会从父目录的node_modules里搜索,这样递归下去直到根目录。

5、自动缓存已载入模块

对于已加载的模块Node会缓存下来,而不必每次都重新搜索。下面是一个示例

// modA.js
console.log('模块modA开始加载...') 
exports = function() {console.log('Hi') }
console.log('模块modA加载完毕')
//init.js 
var mod1 = require('./modA')
var mod2 = require('./modA') 
console.log(mod1 === mod2)

命令行node init.js执行:
模块modA开始加载... 模块modA加载完毕 true

可以看到虽然require了两次,但modA.js仍然只执行了一次。mod1和mod2是相同的,即两个引用都指向了同一个模块对象。

优先从缓存加载

和浏览器会缓存静态js文件一样,Node也会对引入的模块进行缓存,不同的是,浏览器仅仅缓存文件,而nodejs缓存的是编译和执行后的对象(缓存内存) require()对相同模块的二次加载一律采用缓存优先的方式,这是第一优先级的,核心模块缓存检查先于文件模块的缓存检查。

基于这点:我们可以编写一个模块,用来记录长期存在的变量。例如:我可以编写一个记录接口访问数的模块:

let count = {}; 
// 因模块是封闭的,这里实际上借用了js闭包的概念
exports.count = function(name){ 
  if(count[name]){  
       count[name]++;   
  }else{         
      count[name] = 1;     
 }      
console.log(name + '被访问了' + count[name] + '次。'); 
};

我们在路由的 action 或 controller里这样引用:

let count = require('count');
export.index = function(req, res){count('index'); };

以上便完成了对接口调用数的统计,但这只是个demo,因为数据存储在内存,服务器重启后便会清空。真正的计数器一定是要结合持久化存储器的。

在进入路径查找之前有必要描述一下module path这个Node.js中的概念。对于每一个被加载的文件模块,创建这个模块对象的时候,这个模块便会有一个paths属性,其值根据当前文件的路径 计算得到。我们创建modulepath.js这样一个文件,其内容为:

// modulepath.jS
console.log(module.paths);

我们将其放到任意一个目录中执行node modulepath.js命令,将得到以下的输出结果。

[ 
'/home/ikeepstudying/research/node_modules', '/home/ikeepstudying/node_modules', '/home/node_modules', '/node_modules' 
]

2.2 文件定位

1.文件扩展名分析

调用require()方法时若参数没有文件扩展名,Node会按.js.json.node的顺寻补足扩展名,依次尝试。

在尝试过程中,需要调用fs模块阻塞式地判断文件是否存在。因为Node的执行是单线程的,这是一个会引起性能问题的地方。如果是.node或者·.json·文件可以加上扩展名加快一点速度。另一个诀窍是:同步配合缓存。

2.目录分析和包

require()分析文件扩展名后,可能没有查到对应文件,而是找到了一个目录,此时Node会将目录当作一个包来处理。

首先, Node在挡墙目录下查找package.json,通过JSON.parse()解析出包描述对象,从中取出main属性指定的文件名进行定位。若main属性指定文件名错误,或者没有pachage.json文件,Node会将index当作默认文件名。

简而言之,如果require绝对路径的文件,查找时不会去遍历每一个node_modules目录,其速度最快。其余流程如下:

1.从module path数组中取出第一个目录作为查找基准。

2.直接从目录中查找该文件,如果存在,则结束查找。如果不存在,则进行下一条查找。

3.尝试添加.js.json.node后缀后查找,如果存在文件,则结束查找。如果不存在,则进行下一条。

4.尝试将require的参数作为一个包来进行查找,读取目录下的package.json文件,取得main参数指定的文件。

5.尝试查找该文件,如果存在,则结束查找。如果不存在,则进行第3条查找。

6.如果继续失败,则取出module path数组中的下一个目录作为基准查找,循环第1至5个步骤。

7.如果继续失败,循环第1至6个步骤,直到module path中的最后一个值。

8.如果仍然失败,则抛出异常。

整个查找过程十分类似原型链的查找和作用域的查找。所幸Node.js对路径查找实现了缓存机制,否则由于每次判断路径都是同步阻塞式进行,会导致严重的性能消耗。

一旦加载成功就以模块的路径进行缓存

2.3 模块编译

每个模块文件模块都是一个对象,它的定义如下:

function Module(id, parent) {
	this.id = id;
	this.exports = {};
	this.parent = parent;
	if(parent && parent.children) {      parent.children.push(this); 
	}
	this.filename = null;   
	this.loaded = false;   
	this.children = []; 
}

对于不同扩展名,其载入方法也有所不同:

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

其他当作.js

每一个编译成功的模块都会将其文件路径作为索引缓存在Module._cache对象上。

json 文件的编译

.json文件调用的方法如下:其实就是调用JSON.parse

//Native extension for .json 
Module._extensions['.json'] = function(module, filename) {     var content = NativeModule.require('fs').readFileSync(filename, 'utf-8');     try {         module.exports = JSON.parse(stripBOM(content));     } catch(err) {         err.message = filename + ':' + err.message;         throw err;     } }

Module._extensions会被赋值给require()extensions属性,所以可以用:console.log(require.extensions);输出系统中已有的扩展加载方式。 当然也可以自己增加一些特殊的加载:

require.extensions['.txt'] = function(){ //code };。

但是官方不鼓励通过这种方式自定义扩展名加载,而是期望先将其他语言或文件编译成JavaScript文件后再加载,这样的好处在于不讲烦琐的编译加载等过程引入Node的执行过程。

js模块的编译 在编译的过程中,Node对获取的javascript文件内容进行了头尾包装,将文件内容包装在一个function中:

(function (exports, require, module, __filename, __dirname) {     var math = require(‘math‘);  								   exports.area = function(radius) {        
     return Math.PI * radius * radius;     
 }
})

包装之后的代码会通过vm原生模块的runInThisContext()方法执行(具有明确上下文,不污染全局),返回一个具体的function对象,最后传参执行,执行后返回module.exports.

核心模块编译

核心模块分为C/C++编写和JavaScript编写的两个部分,其中C/C++文件放在Node项目的src目录下,JavaScript文件放在lib目录下。

1.转存为C/C++代码

Node采用了V8附带的js2c.py工具,将所有内置的JavaScript代码转换成C++里的数组,生成node_natives.h头文件:

namespace node {     
const char node_native[] = { 47, 47, ..};  
const char dgram_native[] = { 47, 47, ..};   
const char console_native = { 47, 47, ..};  
const char buffer_native = { 47, 47, ..};   
const char querystring_native = { 47, 47, ..};    
const char punycode_native = { 47, 47, ..};     
...   
struct _native {         
const char* name; 
const char* source;   
size_t source_len;   
}
static const struct _native natives[] = {      
{ 
  "node", 
   node_native, 
   sizeof(node_native)-1},   
   {
     "dgram",
     dgram_native, 
     sizeof(dgram_native)-1
     },     
  ...     
 }; }

在这个过程中,JavaScript代码以字符串形式存储在node命名空间中,是不可直接执行的。在启动Node进程时,js代码直接加载到内存中。在加载的过程中,js核心模块经历标识符分析后直接定位到内存中。

2.编译js核心模块

lib目录下的模块文件也在引入过程中经历了头尾包装的过程,然后才执行和导出了exports对象。与文件模块的区别在于:获取源代码的方式(核心模块从内存加载)和缓存执行结果的位置。

js核心模块源文件通过process.binding('natives')取出,编译成功的模块缓存到NativeModule._cache上。代码如下:

function NativeModule() {     
	this.filename = id + '.js';   
	this.id = id;     
	this.exports = {};     
	this.loaded = fales; 
} 
NativeModule._source = process.binding('natives'); NativeModule._cache = {};

importrequire

简单的说一下importrequire的本质区别

import是ES6的模块规范,require是commonjs的模块规范,详细的用法我不介绍,我只想说一下他们最基本的区别,import是静态(编译时)加载模块,require(运行时)是动态加载,那么静态加载和动态加载的区别是什么呢?

静态加载时代码在编译的时候已经执行了,动态加载是编译后在代码运行的时候再执行,那么具体点是什么呢? 先说说import,如下代码

import { name } from 'name.js'
// name.js文件 
export let name = 'jinux' export let age = 20

上面的代码表示main.js文件里引入了name.js文件导出的变量,在代码编译阶段执行后的代码如下:

let name = 'jinux'

这个是我自己理解的,其实就是直接把name.js里的代码放到了main.js文件里,好比是在main.js文件中声明一样。 再来看看require

var obj = require('obj.js');
// obj.js文件 
var obj = {   name: 'jinux',   age: 20 } module.export obj;

require是在运行阶段,需要把obj对象整个加载进内存,之后用到哪个变量就用哪个,这里再对比一下importimport是静态加载,如果只引入了name,age是不会引入的,所以是按需引入,性能更好一点。

4 nodejs清除require缓存

开发nodejs应用时会面临一个麻烦的事情,就是修改了配置数据之后,必须重启服务器才能看到修改后的结果。

于是问题来了,挖掘机哪家强?噢,no! no! no!怎么做到修改文件之后,自动重启服务器。

server.js中的片段:

const port = process.env.port || 1337;
app.listen(port); 
console.log("server start in " + port); 
exports.app = app;

假定我们现在是这样的, app.js的片段:

const app = require('./server.js');

如果我们在server.js中启动了服务器,我们停止服务器可以在app.js中调用

app.app.close()

但是当我们重新引入server.js

app =  require('./server.js')

的时候会发现并不是用的最新的server.js文件,原因是require的缓存机制,在第一次调用require('./server.js')的时候缓存下来了。

这个时候怎么办?

下面的代码解决了这个问题:

delete require.cache[require.resolve('./server.js')]; app = require('./server.js');

小平果118

÷

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

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

相关文章

009-Linux的管道和重定向

文章目录 前言 一、重定向 1.1、FD简介 1.2、FD举例 1.3、重定向简介 1.3.1、输出重定向 正确输出&#xff1a; 错误输出 案例1&#xff1a;正确输出重定向 案例2&#xff1a;错误输出重定向 ​编辑 案例3&#xff1a;正确和错误都输出重定向到相同位置 1.3.2、输…

Redis - 缓存场景

学习资料 学习的黑马程序员哔站项目黑马点评&#xff0c;用作记录和探究原理。 Redis缓存 缓存 &#xff1a;就是数据交换的缓冲区&#xff0c;是存储数据的临时地方&#xff0c;读写性能较高 缓存常见的场景: 数据库查询加速&#xff1a;通过将频繁查询的数据缓存起来&…

C从零开始实现贪吃蛇大作战

个人主页&#xff1a;星纭-CSDN博客 系列文章专栏 : C语言 踏上取经路&#xff0c;比抵达灵山更重要&#xff01;一起努力一起进步&#xff01; 有关Win32API的知识点在上一篇文章&#xff1a; 目录 一.地图 1.控制台基本介绍 2.宽字符 1.本地化 2.类项 3.setlocale函…

推荐10款优秀的组件库(一)

1.Ant Desgin UI 网址&#xff1a; https://ant-design-mobile.antgroup.com/zh Ant Design - 一套企业级 UI 设计语言和 React 组件库 "Ant Design Mobile"是一个在线的移动端Web体验平台&#xff0c;让你探索移动端Web的体验极限。 添加图片注释&#xff0c;不…

5月26(信息差)

&#x1f30d; 珠峰登顶“堵车”后冰架断裂 5人坠崖 2人没爬上来&#xff01; 珠峰登顶“堵车”后冰架断裂 5人坠崖 2人没爬上来&#xff01; &#x1f384; Windows 11 Beta 22635.3646 预览版发布&#xff1a;中国大陆地区新增“微软电脑管家”应用 ✨ 成都限购解除即将满…

5.23.12 计算机视觉的 Inception 架构

1. 介绍 分类性能的提升往往会转化为各种应用领域中显着的质量提升&#xff0c;深度卷积架构的架构改进可用于提高大多数其他计算机视觉任务的性能&#xff0c;这些任务越来越依赖于高质量的学习视觉特征。在 AlexNet 功能无法与手工设计、制作的解决方案竞争的情况下&#xf…

能找伴侣的相亲婚恋平台有哪些?6款值得信赖的恋爱交友软件体验测评

在这个超快节奏的社会里&#xff0c;好多人都忙着搞事业和搞钱&#xff0c;却把终身大事给忽略了。但是随着年龄越来越大&#xff0c;来自长辈和社会的压力也越来越大&#xff0c;因此网络上的相亲交友软件&#xff0c;就成了大多数单身贵族的脱单首选了。下面就来给大家讲讲我…

Day06:Flex 布局

目标&#xff1a;熟练使用 Flex 完成结构化布局 一、标准流 标准流也叫文档流&#xff0c;指的是标签在页面中默认的排布规则&#xff0c;例如&#xff1a;块元素独占一行&#xff0c;行内元素可以一行显示多个。 二、浮动 1、基本使用 作用&#xff1a;让块元素水平排列。 …

【C++题解】1698. 请输出带有特殊尾数的数

问题&#xff1a;1698. 请输出带有特殊尾数的数 类型&#xff1a; 题目描述&#xff1a; 请输出1∼n 中所有个位为 1、3、5、7中任意一个数的整数&#xff0c;每行 1 个。( n<1000 ) 比如&#xff0c;假设从键盘读入 20&#xff0c;输出结果如下&#xff1a; 1 3 5 7 11 1…

树莓派4B 有电但无法启动

试过多个SD卡&#xff0c;反复烧系统镜像都无法启动。接HDMI显示器没有信号输出&#xff0c;上电后PWR红灯长亮&#xff0c;ACT绿灯闪一下就不亮了&#xff0c;GPIO几个电源脚有电&#xff0c;芯片会发热&#xff0c;测量多个TP点电压好像都正常。 ……

N进制计数器【01】

N进制计数器 前面介绍过二进制计数器和十进制计数器&#xff0c;但是在很多时候需要到其他进制的计数器&#xff0c;我们把这些任意进制的计数器简称为 N 进制计数器 设计 N 进制计数器的方法有两种&#xff1a; 用时钟触发器和门电路设计&#xff08;前面常用的方法&#xf…

【Telemac】Telemac相关报错记录

文章目录 1.下载BlueKenue后缀为man解决办法2.运行Telemac项目提示Fortran报错解决办法1.下载BlueKenue后缀为man BlueKenue官方下载链接: 可以看到下载器请求时出现了问题,下载BlueKenue后缀为man. 解决办法 修改下载后的文件后缀为msi即可 2.运行Telemac项目提示Fortr…

Git时光机、Git标签、Git分支、GitHub协作

Git时光机&#xff08;切换版本&#xff09; 1.查看提交历史 HEAD指针指向这次分支的最后一次提交 版本信息一行显示【git log --prettyoneline】 2.引用日志【git reflog】 &#xff08;只在自己的工作区中存在&#xff09; 非常重要&#xff1a;当HEAD指针进行切换之后&…

el-switch自动触发更新事件

比如有这样一个列表&#xff0c;允许修改单条数据的状态。希望在更改el-switch状态时能够有个弹框做二次确认&#xff0c;没问题&#xff0c;el-switch已经帮我们想到了&#xff0c;所以它提供了beforeChange&#xff0c;根据beforeChange的结果来决定是否修改状态。一般确认修…

qt-C++笔记之使用QtConcurrent异步地执行槽函数中的内容,使其不阻塞主界面

qt-C笔记之使用QtConcurrent异步地执行槽函数中的内容&#xff0c;使其不阻塞主界面 code review! 文章目录 qt-C笔记之使用QtConcurrent异步地执行槽函数中的内容&#xff0c;使其不阻塞主界面1.QtConcurrent::run基本用法基本用法启动一个全局函数或静态成员函数使用 Lambda…

C++进阶之路:何为拷贝构造函数,深入理解浅拷贝与深拷贝(类与对象_中篇)

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

脚注:书籍的小秘密,躲藏在脚注间

脚注&#xff1a;书籍的小秘密&#xff0c;躲藏在脚注间 脚注是一种在文本中提供补充信息、引用出处或注解的方式&#xff0c;有助于读者更全面地理解文中内容&#xff0c;并为进一步研究提供参考和跳转点。 在一书本中&#xff0c;脚注是额外提供给读者的文字信息&#xff0…

SpringCloud系列(31)--使用Hystrix进行服务降级

前言&#xff1a;在上一章节中我们创建了服务消费者模块&#xff0c;而本节内容则是使用Hystrix对服务进行服务降级处理。 1、首先我们先对服务提供者的服务进行服务降级处理 (1)修改cloud-provider-hystrix-payment8001子模块的PaymentServiceImpl类 注&#xff1a;HystrixP…

Stream流的使用

目录 一&#xff0c;Stream流 1.1 概述 1.2 Stream代码示例 二&#xff0c;Stream流的使用 2.1 数据准备 2.2 创建流对象 2.3 中间操作 filter map distinct sorted limit skip flatMap 2.4 终结操作 foreach count max&min collect 2.5 查找与匹配 a…

秒级达百万高并发框架Disruptor

1、起源 Disruptor最初由lmax.com开发&#xff0c;2010年在Qcon公开发表&#xff0c;并于2011年开源&#xff0c;企业应用软件专家Martin Fowler专门撰写长文介绍&#xff0c;同年它还获得了Oracle官方的Duke大奖。其官网定义为&#xff1a;“High Performance Inter-Thread M…