您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

Nodejs 的 CommonJS 规范实现原理

时间:2023-11-24 12:00:56  来源:京东云开发者  作者:

了解 Node.js

 

Node.js 是一个基于 ChromeV8 引擎的 JAVAScript 运行环境,使用了一个事件驱动、非阻塞式 I/O 模型,让 JavaScript 运行在服务端的开发平台,它让 JavaScript 成为与 phpPython/ target=_blank class=infotextkey>Python、Perl、Ruby 等服务端语言平起平坐的脚本语言。Node 中增添了很多内置的模块,提供各种各样的功能,同时也提供许多第三方模块。

模块的问题

为什么要有模块

复杂的前端项目需要做分层处理,按照功能、业务、组件拆分成模块, 模块化的项目至少有以下优点:

  1. 便于单元测试
  2. 便于同事间协作
  3. 抽离公共方法,开发快捷
  4. 按需加载,性能优秀
  5. 高内聚低耦合
  6. 防止变量冲突
  7. 方便代码项目维护

几种模块化规范

  • CMD (SeaJS 实现了 CMD)
  • AMD (RequireJS 实现了 AMD)
  • UMD (同时支持 AMD 和 CMD)
  • IIFE (自执行函数)
  • CommonJS (Node 采用了 CommonJS)
  • ES Module 规范 (JS 官方的模块化方案)

Node 中的模块

Node 中采用了 CommonJS 规范

实现原理:

Node 中会读取文件,拿到内容实现模块化, Require 方法 同步引用

tips:Node 中任何 js 文件都是一个模块,每一个文件都是模块

Node 中模块类型

  1. 内置模块,属于核心模块,无需安装,在项目中不需要相对路径引用, Node 自身提供。
  2. 文件模块,程序员自己书写的 js 文件模块。
  3. 第三方模块, 需要安装, 安装之后不用加路径。

Node 中内置模块

fs filesystem

操作文件都需要用到这个模块

const path = require('path'); // 处理路径

const fs = require('fs'); // file system

// // 同步读取

let content = fs.readFileSync(path.resolve(__dirname, 'test.js'), 'utf8');

console.log(content);

let exists = fs.existsSync(path.resolve(__dirname, 'test1.js'));

console.log(exists);

path 路径处理

const path = require('path'); // 处理路径

// join / resolve 用的时候可以混用

console.log(path.join('a', 'b', 'c', '..', '/'))

// 根据已经有的路径来解析绝对路径, 可以用他来解析配置文件

console.log(path.resolve('a', 'b', '/')); // resolve 不支持/ 会解析成根路径

console.log(path.join(__dirname, 'a'))

console.log(path.extname('1.js'))

console.log(path.dirname(__dirname)); // 解析父目录

vm 运行代码

字符串如何能变成 JS 执行呢?

1.eval

eval 中的代码执行时的作用域为当前作用域。它可以访问到函数中的局部变量。

let test = 'global scope'

global.test1 = '123'

function b(){

test = 'fn scope'

eval('console.log(test)'); //local scope

new Function('console.log(test1)')() // 123

new Function('console.log(test)')() //global scope

}

2.new Function

new Function () 创建函数时,不是引用当前的词法环境,而是引用全局环境,Function 中的表达式使用的变量要么是传入的参数要么是全局的值

Function 可以获取全局变量,所以它还是可能会有变量污染的情况出现

function getFn() {

let value = "test"

let fn = new Function('console.log(value)')

return fn

}

getFn()()

global.a = 100 // 挂在到全局对象global上

new Function("console.log(a)")() // 100

3.vm

前面两种方式,我们一直强调一个概念,那就是变量的污染

VM 的特点就是不受环境的影响,也可以说他就是一个沙箱环境

在 Node 中全局变量是在多个模块下共享的,所以尽量不要在 global 中定义属性

所以,vm.runInThisContext 可以访问到 global 上的全局变量,但是访问不到自定义的变量。而 vm.runInNewContext 访问不到 global,也访问不到自定义变量,他存在于一个全新的执行上下文

const vm = require('vm')

global.a = 1

// vm.runInThisContext("console.log(a)")

vm.runInThisContext("a = 100") // 沙箱,独立的环境

console.log(a) // 1

vm.runInNewContext('console.log(a)')

console.log(a) // a is not defined

Node 模块化的实现

node 中是自带模块化机制的,每个文件就是一个单独的模块,并且它遵循的是 CommonJS 规范,也就是使用 require 的方式导入模块,通过 module.export 的方式导出模块。

node 模块的运行机制也很简单,其实就是在每一个模块外层包裹了一层函数,有了函数的包裹就可以实现代码间的作用域隔离。

我们先在一个 js 文件中直接打印 arguments,得到的结果如下图所示,我们先记住这些参数。

console.log(arguments) // exports, require, module, __filename, __dirname

Nodejs 的 CommonJS 规范实现原理

Node 中通过 modules.export 导出,require 引入。其中 require 依赖 node 中的 fs 模块来加载模块文件,通过 fs.readFile 读取到的是一个字符串。

在 javascrpt 中可以通过 eval 或者 new Function 的方式来将一个字符串转换成 js 代码来运行。但是前面提到过,他们都有一个致命的问题,就是变量的污染

实现 require 模块加载器

首先导入依赖的模块 path,fs,vm, 并且创建一个 Require 函数,这个函数接收一个 modulePath 参数,表示要导入的文件路径

const path = require('path');

const fs = require('fs');

const vm = require('vm');

// 定义导入类,参数为模块路径

function Require(modulePath) {

}

在 Require 中获取到模块的绝对路径,使用 fs 加载模块,这里读取模块内容使用 new Module 来抽象,使用 tryModuleLoad 来加载模块内容,Module 和 tryModuleLoad 稍后实现,Require 的返回值应该是模块的内容,也就是 module.exports。

// 定义导入类,参数为模块路径

function Require(modulePath) {

// 获取当前要加载的绝对路径

let absPathname = path.resolve(__dirname, modulePath);

// 创建模块,新建Module实例

const module = new Module(absPathname);

// 加载当前模块

tryModuleLoad(module);

// 返回exports对象

return module.exports;

}

Module 的实现就是给模块创建一个 exports 对象,tryModuleLoad 执行的时候将内容加入到 exports 中,id 就是模块的绝对路径。

// 定义模块, 添加文件id标识和exports属性

function Module(id) {

this.id = id;

// 读取到的文件内容会放在exports中

this.exports = {};

}

node 模块是运行在一个函数中,这里给 Module 挂载静态属性 wrApper,里面定义一下这个函数的字符串,wrapper 是一个数组,数组的第一个元素就是函数的参数部分,其中有 exports,module,Require,__dirname,__filename, 都是模块中常用的全局变量.

第二个参数就是函数的结束部分。两部分都是字符串,使用的时候将他们包裹在模块的字符串外部就可以了。

// 定义包裹模块内容的函数

Module.wrapper = [

"(function(exports, module, Require, __dirname, __filename) {",

"})"

]

_extensions 用于针对不同的模块扩展名使用不同的加载方式,比如 JSON 和 javascript 加载方式肯定是不同的。JSON 使用 JSON.parse 来运行。

javascript 使用 vm.runInThisContext 来运行,可以看到 fs.readFileSync 传入的是 module.id 也就是 Module 定义时候 id 存储的是模块的绝对路径,读取到的 content 是一个字符串,使用 Module.wrapper 来包裹一下就相当于在这个模块外部又包裹了一个函数,也就实现了私有作用域。

使用 call 来执行 fn 函数,第一个参数改变运行的 this 传入 module.exports,后面的参数就是函数外面包裹参数 exports, module, Require, __dirname, __filename。/

// 定义扩展名,不同的扩展名,加载方式不同,实现js和json

Module._extensions = {

'.js'(module) {

const content = fs.readFileSync(module.id, 'utf8');

const fnStr = Module.wrapper[0] + content + Module.wrapper[1];

const fn = vm.runInThisContext(fnStr);

fn.call(module.exports, module.exports, module, Require,__filename,__dirname);

},

'.json'(module) {

const json = fs.readFileSync(module.id, 'utf8');

module.exports = JSON.parse(json); // 把文件的结果放在exports属性上

}

}

tryModuleLoad 函数接收的是模块对象,通过 path.extname 来获取模块的后缀名,然后使用 Module._extensions 来加载模块。

// 定义模块加载方法

function tryModuleLoad(module) {

// 获取扩展名

const extension = path.extname(module.id);

// 通过后缀加载当前模块

Module._extensions[extension](module); // 策略模式???

}

到此 Require 加载机制基本就写完了。Require 加载模块的时候传入模块名称,在 Require 方法中使用 path.resolve (__dirname, modulePath) 获取到文件的绝对路径。然后通过 new Module 实例化的方式创建 module 对象,将模块的绝对路径存储在 module 的 id 属性中,在 module 中创建 exports 属性为一个 json 对象。

使用 tryModuleLoad 方法去加载模块,tryModuleLoad 中使用 path.extname 获取到文件的扩展名,然后根据扩展名来执行对应的模块加载机制。

最终将加载到的模块挂载 module.exports 中。tryModuleLoad 执行完毕之后 module.exports 已经存在了,直接返回就可以了。

接下来,我们给模块添加缓存。就是文件加载的时候将文件放入缓存中,再去加载模块时先看缓存中是否存在,如果存在直接使用,如果不存在再去重新加载,加载之后再放入缓存。

// 定义导入类,参数为模块路径

function Require(modulePath) {

// 获取当前要加载的绝对路径

let absPathname = path.resolve(__dirname, modulePath);

// 从缓存中读取,如果存在,直接返回结果

if (Module._cache[absPathname]) {

return Module._cache[absPathname].exports;

}

// 创建模块,新建Module实例

const module = new Module(absPathname);

// 添加缓存

Module._cache[absPathname] = module;

// 加载当前模块

tryModuleLoad(module);

// 返回exports对象

return module.exports;

}

增加功能:省略模块后缀名。

自动给模块添加后缀名,实现省略后缀名加载模块,其实也就是如果文件没有后缀名的时候遍历一下所有的后缀名看一下文件是否存在。

// 定义导入类,参数为模块路径

function Require(modulePath) {

// 获取当前要加载的绝对路径

let absPathname = path.resolve(__dirname, modulePath);

// 获取所有后缀名

const extNames = Object.keys(Module._extensions);

let index = 0;

// 存储原始文件路径

const oldPath = absPathname;

function findExt(absPathname) {

if (index === extNames.length) {

return throw new Error('文件不存在');

}

try {

fs.accessSync(absPathname);

return absPathname;

} catch(e) {

const ext = extNames[index++];

findExt(oldPath + ext);

}

}

// 递归追加后缀名,判断文件是否存在

absPathname = findExt(absPathname);

// 从缓存中读取,如果存在,直接返回结果

if (Module._cache[absPathname]) {

return Module._cache[absPathname].exports;

}

// 创建模块,新建Module实例

const module = new Module(absPathname);

// 添加缓存

Module._cache[absPathname] = module;

// 加载当前模块

tryModuleLoad(module);

// 返回exports对象

return module.exports;

}

源代码调试

我们可以通过 VSCode 调试 Node.js

步骤

创建文件 a.js

module.exports = 'abc'

1. 文件 test.js

let r = require('./a')

console.log(r)

1. 配置 debug,本质是配置.vscode/launch.json 文件,而这个文件的本质是能提供多个启动命令入口选择。

一些常见参数如下:

  • program 控制启动文件的路径(即入口文件)
  • name 下拉菜单中显示的名称(该命令对应的入口名称)
  • request 分为 launch(启动)和 attach(附加)(进程已经启动)
  • skipFiles 指定单步调试跳过的代码
  • runtimeExecutable 设置运行时可执行文件,默认是 node,可以设置成 nodemon,ts-node,npm 等

修改 launch.json,skipFiles 指定单步调试跳过的代码

Nodejs 的 CommonJS 规范实现原理

  1. 将 test.js 文件中的 require 方法所在行前面打断点
  2. 执行调试,进入源码相关入口方法

梳理代码步骤

1. 首先进入到进入到 require 方法:Module.prototype.require

Nodejs 的 CommonJS 规范实现原理

Nodejs 的 CommonJS 规范实现原理

2. 调试到 Module._load 方法中,该方法返回 module.exports,Module._resolveFilename 方法返回处理之后的文件地址,将文件改为绝对地址,同时如果文件没有后缀就加上文件后缀。

Nodejs 的 CommonJS 规范实现原理

3. 这里定义了 Module 类。id 为文件名。此类中定义了 exports 属性

Nodejs 的 CommonJS 规范实现原理

4. 接着调试到 module.load 方法,该方法中使用了策略模式,Module._extensions [extension](this, filename) 根据传入的文件后缀名不同调用不同的方法

Nodejs 的 CommonJS 规范实现原理

5. 进入到该方法中,看到了核心代码,读取传入的文件地址参数,拿到该文件中的字符串内容,执行 module._compile

Nodejs 的 CommonJS 规范实现原理

6. 此方法中执行 wrapSafe 方法。将字符串前后添加函数前后缀,并用 Node 中的 vm 模块中的 runInthisContext 方法执行字符串,便直接执行到了传入文件中的 console.log 代码行内容。

Nodejs 的 CommonJS 规范实现原理

Nodejs 的 CommonJS 规范实现原理

至此,整个 Node 中实现 require 方法的整个流程代码已经调试完毕,通过对源代码的调试,可以帮助我们学习其实现思路,代码风格及规范,有助于帮助我们实现工具库,提升我们的代码思路,同时我们知道相关原理,也对我们解决日常开发工作中遇到的问题提供帮助。

 

作者:京东物流 乔盼盼
来源:京东云开发者社区 自猿其说 Tech 转载请注明来源


Tags:Nodejs   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
Nodejs 的 CommonJS 规范实现原理
了解 Node.js Node.js 是一个基于 ChromeV8 引擎的 JavaScript 运行环境,使用了一个事件驱动、非阻塞式 I/O 模型,让 JavaScript 运行在服务端的开发平台,它让 JavaScript 成为...【详细内容】
2023-11-24  Search: Nodejs  点击:(201)  评论:(0)  加入收藏
NodeJS堆溢出的原因,及解决办法。
使用NodeJS开发的应用,如果需要处理大量数据,可能导致堆溢出。错误提示中,会有“JavaScript heap out of memory”,如下图:通过调试分析,会发现:在出错之前,其使用的内存量是一直增...【详细内容】
2022-12-11  Search: Nodejs  点击:(453)  评论:(0)  加入收藏
autojs的rhino和nodejs写悬浮窗对比
第一行:rhino// 这里是空的nodejs"nodejs ui-thread";"ui"和"ui-thread"是有区别的: ui: 用于显示界面(Activity)的情况,比如启动后展示一个Web页面用于用户操作,参见UI模块的文...【详细内容】
2022-09-23  Search: Nodejs  点击:(650)  评论:(0)  加入收藏
七爪源码:使用 NodeJs 观看文件系统
监视文件系统意味着监视特定目录或文件的更改。 有时您可能需要持续观察特定文件或目录的更改。出于这个原因,我们使用像 chokidar 这样的文件系统 Watcher 或内置的 NodeJs...【详细内容】
2022-09-17  Search: Nodejs  点击:(478)  评论:(0)  加入收藏
理解Nodejs(V8)和Spring Webflux(Netty)——基于Event Loop设计
在过去的五个月里,我一直在我当前的项目中使用 Spring Webflux,我还编写了很多 Nodejs 应用程序,并且使用 Promise 样式编码(async/await)的方式也几乎相同,而 Webflux 使用 Mon...【详细内容】
2022-06-27  Search: Nodejs  点击:(630)  评论:(0)  加入收藏
挖洞经验之nodejs 中的漏洞技巧
零基础学黑客领资料搜公众号:白帽子左一关于原型链在javascript中,继承的整个过程就称为该类的原型链。每个对象的都有一个指向他的原型(prototype)的内部链接,这个原型对象又...【详细内容】
2022-05-20  Search: Nodejs  点击:(969)  评论:(0)  加入收藏
手写一个仿微信登录的nodejs程序
前言 首先,我们看一下微信开放文档中的一张图: 上面的一幅图中清楚地介绍了微信登录整个过程,下面对图上所示进行总结: 一、二维码的获得 用户打开登录网页后,登录网页后台根据微...【详细内容】
2022-05-06  Search: Nodejs  点击:(395)  评论:(0)  加入收藏
基于NodeJS的KOA2框架实现restful API网站后台
在此前写的文章“从零基础入门进行小程序开发实战”中,已经介绍过背单词的小程序,因为没有备案的服务器资源只能使用系统后台提供的缓存功能存储用户数据。缓存有大小限制,而且...【详细内容】
2021-07-27  Search: Nodejs  点击:(413)  评论:(0)  加入收藏
Nodejs读取Google Drive里面的文件
最近在工作中遇到了一个场景:要做一个静态的网站,里面的内容是由设计编写的.md格式的内容。设计将编好的文档统一放在常用的Google Drive里面,如下图 然后我需要将这些文档下载...【详细内容】
2021-06-09  Search: Nodejs  点击:(600)  评论:(0)  加入收藏
nodejs版本大文件之断点下载
框架语言:nodejs 包:express fs目的通过nodejs实现一个大文件的断点下载的服务。代码由于没啥复杂的,这里直接上代码。 http断点下载是通过range来判断文件的起始位置的。其基...【详细内容】
2021-04-27  Search: Nodejs  点击:(488)  评论:(0)  加入收藏
▌简易百科推荐
即将过时的 5 种软件开发技能!
作者 | Eran Yahav编译 | 言征出品 | 51CTO技术栈(微信号:blog51cto) 时至今日,AI编码工具已经进化到足够强大了吗?这未必好回答,但从2023 年 Stack Overflow 上的调查数据来看,44%...【详细内容】
2024-04-03    51CTO  Tags:软件开发   点击:(5)  评论:(0)  加入收藏
跳转链接代码怎么写?
在网页开发中,跳转链接是一项常见的功能。然而,对于非技术人员来说,编写跳转链接代码可能会显得有些困难。不用担心!我们可以借助外链平台来简化操作,即使没有编程经验,也能轻松实...【详细内容】
2024-03-27  蓝色天纪    Tags:跳转链接   点击:(12)  评论:(0)  加入收藏
中台亡了,问题到底出在哪里?
曾几何时,中台一度被当做“变革灵药”,嫁接在“前台作战单元”和“后台资源部门”之间,实现企业各业务线的“打通”和全域业务能力集成,提高开发和服务效率。但在中台如火如荼之...【详细内容】
2024-03-27  dbaplus社群    Tags:中台   点击:(8)  评论:(0)  加入收藏
员工写了个比删库更可怕的Bug!
想必大家都听说过删库跑路吧,我之前一直把它当一个段子来看。可万万没想到,就在昨天,我们公司的某位员工,竟然写了一个比删库更可怕的 Bug!给大家分享一下(不是公开处刑),希望朋友们...【详细内容】
2024-03-26  dbaplus社群    Tags:Bug   点击:(5)  评论:(0)  加入收藏
我们一起聊聊什么是正向代理和反向代理
从字面意思上看,代理就是代替处理的意思,一个对象有能力代替另一个对象处理某一件事。代理,这个词在我们的日常生活中也不陌生,比如在购物、旅游等场景中,我们经常会委托别人代替...【详细内容】
2024-03-26  萤火架构  微信公众号  Tags:正向代理   点击:(10)  评论:(0)  加入收藏
看一遍就理解:IO模型详解
前言大家好,我是程序员田螺。今天我们一起来学习IO模型。在本文开始前呢,先问问大家几个问题哈~什么是IO呢?什么是阻塞非阻塞IO?什么是同步异步IO?什么是IO多路复用?select/epoll...【详细内容】
2024-03-26  捡田螺的小男孩  微信公众号  Tags:IO模型   点击:(8)  评论:(0)  加入收藏
为什么都说 HashMap 是线程不安全的?
做Java开发的人,应该都用过 HashMap 这种集合。今天就和大家来聊聊,为什么 HashMap 是线程不安全的。1.HashMap 数据结构简单来说,HashMap 基于哈希表实现。它使用键的哈希码来...【详细内容】
2024-03-22  Java技术指北  微信公众号  Tags:HashMap   点击:(11)  评论:(0)  加入收藏
如何从头开始编写LoRA代码,这有一份教程
选自 lightning.ai作者:Sebastian Raschka机器之心编译编辑:陈萍作者表示:在各种有效的 LLM 微调方法中,LoRA 仍然是他的首选。LoRA(Low-Rank Adaptation)作为一种用于微调 LLM(大...【详细内容】
2024-03-21  机器之心Pro    Tags:LoRA   点击:(12)  评论:(0)  加入收藏
这样搭建日志中心,传统的ELK就扔了吧!
最近客户有个新需求,就是想查看网站的访问情况。由于网站没有做google的统计和百度的统计,所以访问情况,只能通过日志查看,通过脚本的形式给客户导出也不太实际,给客户写个简单的...【详细内容】
2024-03-20  dbaplus社群    Tags:日志   点击:(4)  评论:(0)  加入收藏
Kubernetes 究竟有没有 LTS?
从一个有趣的问题引出很多人都在关注的 Kubernetes LTS 的问题。有趣的问题2019 年,一个名为 apiserver LoopbackClient Server cert expired after 1 year[1] 的 issue 中提...【详细内容】
2024-03-15  云原生散修  微信公众号  Tags:Kubernetes   点击:(5)  评论:(0)  加入收藏
站内最新
站内热门
站内头条