了解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 scope
new Function('console.log(test1)')() // 123
new 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 转载请注明来源

前端技术探秘-Nodejs的CommonJS规范实现原理的更多相关文章

  1. 【转发】网易邮箱前端技术分享之javascript编码规范

    网易邮箱前端技术分享之javascript编码规范 发布日期:2013-11-26 10:06 来源:网易邮箱前端技术中心 作者:网易邮箱 点击:533 网易邮箱是国内最早使用ajax技术的邮箱.早在 ...

  2. NodeJS学习笔记—1.CommonJS规范

    由于现在web开发,越来越重视代码的复用和抽象的封装,为了解决代码的组织结构.管理.复用和部署等问题,现在普遍采用的机制是模块机制(module).CommonJS约定桌面应用程序和服务器应用程序需要 ...

  3. NodeJS新建服务器以及CommonJS规范

    1.什么是node.js?(1)Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境.Node.js 使用了一个事件驱动.非阻塞式 I/O 的模型,使其轻量又高效.( ...

  4. NodeJs的CommonJS模块规范

    前言 本人记忆力一般,为了让自己理解<深入浅出Node.js-朴灵>一书,会在博客里记录一些关键知识,以后忘了也可以在这里找到,快速回想起来 Node通过require.exports.m ...

  5. js前端技术

    一.前端技术 1.HTML HTML(hypertext markup language)超文本标记语言,不同于编程语言. 超文本就是超出纯文本的范畴,描述文本的颜色.大小.字体. HTML由一个个标 ...

  6. Javascript模块规范(CommonJS规范&&AMD规范)

    Javascript模块化编程(AMD&CommonJS) 前端模块化开发的价值:https://github.com/seajs/seajs/issues/547 模块的写法 查看 AMD规 ...

  7. Commonjs规范及Node模块实现

    前面的话 Node在实现中并非完全按照CommonJS规范实现,而是对模块规范进行了一定的取舍,同时也增加了少许自身需要的特性.本文将详细介绍NodeJS的模块实现 引入 nodejs是区别于java ...

  8. 【转】Commonjs规范及Node模块实现

    前言: Node在实现中并非完全按照CommonJS规范实现,而是对模块规范进行了一定的取舍,同时也增加了少许自身需要的特性.本文将详细介绍NodeJS的模块实现 引入 nodejs是区别于javas ...

  9. 一文读懂前端技术演进:盘点Web前端20年的技术变迁史

    本文原文由作者“司徒正美”发布于公众号“前端你别闹”,即时通讯网收录时有改动,感谢原作者的分享. 1.引言 1990 年,第一个Web浏览器的诞生:1991 年,WWW诞生,这标志着前端技术的开始. ...

  10. 19 — node 模块化 及 CommonJS规范 — CommonJS 的由来及各组织与 JS 的关系

    ECMAScript  对于不同的环境(运行平台),设计结构,理念,使用方式大相径庭. 1,浏览器 :DOM BOM 2,NodeJS :FS,HTTP 内置模块 :  第三方模块 : 内置模块 3, ...

随机推荐

  1. react18 hooks自定义移动端Popup弹窗组件RcPop

    基于React18 Hooks实现手机端弹框组件RcPop react-popup 基于react18+hook自定义多功能弹框组件.整合了msg/alert/dialog/toast及android ...

  2. Kubernetes: Kubectl 源码分析

    0. 前言 kubectl 看了也有一段时间,期间写了两篇设计模式的文章,是时候对 kubectl 做个回顾了. 1. kubectl 入口:Cobra kubectl 是 kubernetes 的命 ...

  3. python教程 入门学习笔记 第4天 数据类型 获取数据类型 字符串拼接

    数据类型 1.能直接处理的基本数据类型有5个:整型.浮点型.字符串.布尔值.空 1)整型(int)=整数,例如0至9,-1至-9,100,-8180等,人数.年龄.页码.门牌号等 没有小数位的数字,是 ...

  4. 常用c++ STL 汇总

    常用STL: vector 变长数组,倍增的思想 初始化: //初始化 vector<int> a; vector<int> a(n); vector<int> a ...

  5. [USACO22DEC] Cow College B 题解

    洛谷 P8897 AcWing 4821 题目描述 有\(n\)头奶牛,每头奶牛愿意交的最大学费为\(c_i\),问如何设置学费,可以使赚到的钱最多. \(1\le n\le 10^5,1\le c_ ...

  6. 发布关于PostGIS对于USD格式的拓展

    我们非常高兴的发布为了一年一度的SIGGRAPH 2023发布关于为PostGIS支持USD格式的新拓展. 新添加了3个函数 ST_AsUSDA(geom geometry, usd_root_nam ...

  7. WPF中非递归(无后台代码)动态实现TreeView

    在UI界面中,树形视图是比较常用的表示层级结构的方式,WPF中提供了TreeView控件.对于TreeView控件的基本使用已经有很多文章.大都是介绍如何在XAML中使用硬编码的固定信息填充Treev ...

  8. 将Python打包成exe

    使用以下命令首先安装包 pip install pyinstaller 参数以及用法 -F生成结果是一个exe文件,所有的第三方依赖.资源和代码均被打包进该exe内 -D生成结果是一个目录,各种第三方 ...

  9. VS2015项目.net-framework-4.5.2升级或新建项目无法选择framework 4.6.2(解决办法)

    VS2015里面没有.NET Framework 4.6.2 VS2015默认安装的目标框架最高是.NET Framework 4.6.1,但是我的项目里面某些NuGet软件包更新需要依赖.NET F ...

  10. 论文解读(MetaAdapt)《MetaAdapt: Domain Adaptive Few-Shot Misinformation Detection via Meta Learning》

    Note:[ wechat:Y466551 | 可加勿骚扰,付费咨询 ] 论文信息 论文标题:MetaAdapt: Domain Adaptive Few-Shot Misinformation De ...