前端技术探秘-Nodejs的CommonJS规范实现原理 | 京东物流技术团队

了解Node.js

Node.js是一个基于ChromeV8引擎的JavaScript运行环境,使用了一个事件驱动、非阻塞式I/O模型,让JavaScript 运行在服务端的开发平台,它让JavaScript成为与PHP、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 scopenew Function('console.log(test1)')() // 123new Function('console.log(test)')() //global scope
}
b()

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

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指定单步调试跳过的代码

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

梳理代码步骤

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

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

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

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

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

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

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

作者:京东物流 乔盼盼

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

免费商用字体,进来领取!!!

如果你不知道去哪里找免费可商用字体,那一定要收藏好这几个网站,全部都是免费无版权字体,以后再也不用担心侵权问题了。 1、免费字体网 https://font.sucai999.com/ 一个免费可商用字体搬运工,实时跟新市面上免费商用的字体。网站…

国家万亿资金助力城市生命线城市内涝积水监测系统

自2023年年初以来,我国多个地区遭遇了洪涝、干旱、台风、风雹等灾害的侵袭,部分地区灾情严重,经济损失较大。为应对灾后恢复重建工作的艰巨任务,本次国债将主要投向灾后恢复重建以及提升防灾减灾救灾能力。其中,将全面…

12V 全桥驱动芯片GC9008,0.1A 持续驱动输出电流,可替代MX6208

GC9008 是一款 12V 全桥驱动芯片,为 摄像机、消费类产品提供高性价比的方案。能提供 0.1A 的持续输出电流。 可以工作在 4.5~15V 的电源电压上。 GC9008 具有 PW ( IN1/IN2 )输入接口 , 与行业标准器件兼容. GC9008S 是 SOP8 封装&a…

【JavaEE初阶】 博客系统项目--前端页面设计实现

文章目录 🌲主要内容🎍预期效果🚩博客列表页效果🚩博客详情页🚩博客登录页🚩博客编辑页 🍀实现博客列表页🚩实现导航栏🎈页面主体部分 🎄实现博客详情页&…

【多线程】-- 04 静态代理模式

多线程 3 静态代理 这里以一个现实生活中的例子来解释并实现所谓的静态代理模式,即结婚者雇用婚庆公司来帮助自己完成整个婚礼过程: package com.duo.lambda;interface Marry {void HappyMarry();//人生四大乐事:久旱逢甘霖;他…

文件元数据批量修改:mp3音频和mp4视频的元数据如何批量修改

在数字媒体处理和管理的日常工作中,文件元数据的批量修改是一个常见的需求。元数据,或者称为文件信息,可以包括文件的创建日期、修改日期、文件名、文件大小、标签等。在音乐和视频处理领域,例如对mp3音频和mp4视频文件&#xff0…

linux下的工具---gdb

一、gdb简介 GDB,是The GNU Project Debugger 的缩写,是 Linux 下功能全面的调试工具。 GDB支持断点、单步执行、打印变量、观察变量、查看寄存器、查看堆栈等调试手段。 程序的发布方式有两种,debug模式和release模式 Linux gcc/g出来的二进制程序&am…

自驾游汽车托运是交智商税吗?

自驾游汽车托运是交智商税吗? 亲爱的小伙伴们 你们有没有遇到过这样的困扰: 自驾游时,车辆的运输问题让你头疼不已? 是选择自己驾驶还是托运呢? 今天,我就来给大家种草一下汽车托运的好处, 让你的自驾游之旅更加轻松愉快! 1️.…

安防视频监控/磁盘阵列/集中云存储平台EasyCVR设备录像保活不生效原因是什么?该如何解决?

安防视频监控/视频集中存储/云存储/磁盘阵列EasyCVR平台可拓展性强、视频能力灵活、部署轻快,可支持的主流标准协议有国标GB28181、RTSP/Onvif、RTMP等,以及支持厂家私有协议与SDK接入,包括海康Ehome、海大宇等设备的SDK等。平台既具备传统安…

【Linux】安卓端JuiceSSH结合内网穿透实现远程连接服务器

目录 前言1. Linux安装cpolar2. 创建公网SSH连接地址3. JuiceSSH公网远程连接4. 固定连接SSH公网地址5. SSH固定地址连接测试 前言 处于内网的虚拟机如何被外网访问呢?如何手机就能访问虚拟机呢? 本文介绍 cpolarJuiceSSH 实现手机端远程连接Linux虚拟…

elk日志分析系统:

elk日志分析系统: elk是一套完整的日志集中处理方案,由三个开源的软件简称组成; E:Easticsearch 简称ES是一个开源的,分布式的存储检索引擎,(索引型的非关系数据库)存储日志 由java代码开发的&#xff0…

CSC公派博士后|管理学老师赴韩国首尔大学达成目标

J老师自身背景正好卡在CSC公派博士后申报条件的边缘,为增大通过概率,其提出优选亚洲范围内的世界知名高校、专业相符、2年博士后职位的要求。最终我们用韩国首尔大学的邀请函助其顺利获批CSC,实现了所有既定目标。 J老师背景: 申…

【Tiny_CD】Tiny_CD变化检测网络详解(含python代码)

题目:TinyCD: A (Not So) Deep Learning Model For Change Detection 论文:paper 代码:code 目录 🍟 🍟1.摘要 🍗🍗 2.贡献 🍖🍖 3.网络结构

前端必学——实现电商图片放大镜效果(附代码)

放大镜可以说是前端人必须学会的程序之一,今天的案例为大家展示一下怎么实现放大镜的效果! 效果图展示 整个效果就是当鼠标放到展示图上的时候,会出现一个遮罩层以及弹出来一个框展示一个详情图,并且鼠标移动的时候详情图跟着移动&#xff0…

家乡旅游推广软文怎么写?媒介盒子分享

随着各地政策的放开,旅行已经成为很多消费者生活中不可缺少的一项,各地景区也在宣传上纷纷发力,希望能够吸引游客。只要文案写得好,没有景点火不了,今天媒介盒子就来和大家聊聊:家乡旅游推广软文怎么写。 一…

独乐乐不如众乐乐(二)-某汽车零部件厂商IC EMC企业规范

前言:该汽车零部件厂商关于IC EMC的规范可能是小编看过的企业标准里要求最明确的一份企业标准了,充分说明了标准方法不是死的,可以灵活应用。 先看看这份规范的抬头: 与其他企业规范一样,该汽车零部件厂商的IC EMC规范…

TDA4开发环境Docker化

文章目录 背景1. TDA4X Linux SDK编译环境镜像构建1.1 安装SDK1.2 验证制卡1.2.1 出现的问题:1.3 验证编译1.3.1 出现的问题2. TDA4X Linux-RT SDK编译环境镜像构建2.1 安装SDK2.2 出现的问题参考背景 开始阅读本篇前,假设你已经对docker有了一定了解,且有过docker换件搭建…

1、Linux_介绍和安装

1. Linux概述 Linux:是基于Unix的一个开源、免费的操作系统,其稳定性、安全性、处理多并发能力强,目前大多数企业级应用甚至是集群项目都部署运行在linux操作系统之上,在我国软件公司得到广泛的使用 Unix:是一个强大…

聊聊如何进行代码混淆加固

​ 聊聊如何进行代码混淆 前言什么是代码混淆代码混淆,是指将计算机程序的代码,转换成一种功能上等价,但是难于阅读和理解的形式的行为。 代码混淆常见手段1、名称混淆 将有意义的类,字段、方法名称更改为无意义的字符串。生成…