前端技术探秘 -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
操作文件都需要用到这个模块
path 路径处理
vm 运行代码
字符串如何能变成 JS 执行呢?
1.eval
eval 中的代码执行时的作用域为当前作用域。它可以访问到函数中的局部变量。
1.new Function
new Function()创建函数时,不是引用当前的词法环境,而是引用全局环境,Function 中的表达式使用的变量要么是传入的参数要么是全局的值
Function 可以获取全局变量,所以它还是可能会有变量污染的情况出现
1.vm
前面两种方式,我们一直强调一个概念,那就是变量的污染
VM 的特点就是不受环境的影响,也可以说他就是一个沙箱环境
在 Node 中全局变量是在多个模块下共享的,所以尽量不要在 global 中定义属性
所以,vm.runInThisContext
可以访问到global
上的全局变量,但是访问不到自定义的变量。而vm.runInNewContext
访问不到global
,也访问不到自定义变量,他存在于一个全新的执行上下文
Node 模块化的实现
node
中是自带模块化机制的,每个文件就是一个单独的模块,并且它遵循的是CommonJS
规范,也就是使用require
的方式导入模块,通过module.export
的方式导出模块。
node
模块的运行机制也很简单,其实就是在每一个模块外层包裹了一层函数,有了函数的包裹就可以实现代码间的作用域隔离。
我们先在一个 js 文件中直接打印 arguments,得到的结果如下图所示,我们先记住这些参数。
Node 中通过 modules.export 导出,require 引入。其中require
依赖node
中的fs
模块来加载模块文件,通过fs.readFile
读取到的是一个字符串。
在javascrpt
中可以通过eval
或者new Function
的方式来将一个字符串转换成js
代码来运行。但是前面提到过,他们都有一个致命的问题,就是变量的污染。
实现 require 模块加载器
首先导入依赖的模块path
,fs
,vm
, 并且创建一个Require
函数,这个函数接收一个modulePath
参数,表示要导入的文件路径
在Require
中获取到模块的绝对路径,使用fs
加载模块,这里读取模块内容使用new Module
来抽象,使用tryModuleLoad
来加载模块内容,Module
和tryModuleLoad
稍后实现,Require
的返回值应该是模块的内容,也就是module.exports
。
Module
的实现就是给模块创建一个exports
对象,tryModuleLoad
执行的时候将内容加入到exports
中,id
就是模块的绝对路径。
node
模块是运行在一个函数中,这里给Module
挂载静态属性wrapper
,里面定义一下这个函数的字符串,wrapper
是一个数组,数组的第一个元素就是函数的参数部分,其中有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
。/
tryModuleLoad
函数接收的是模块对象,通过path.extname
来获取模块的后缀名,然后使用Module._extensions
来加载模块。
到此Require
加载机制基本就写完了。Require
加载模块的时候传入模块名称,在Require
方法中使用path.resolve(__dirname, modulePath)
获取到文件的绝对路径。然后通过 new Module 实例化的方式创建module
对象,将模块的绝对路径存储在module
的id
属性中,在module
中创建exports
属性为一个json
对象。
使用tryModuleLoad
方法去加载模块,tryModuleLoad
中使用path.extname
获取到文件的扩展名,然后根据扩展名来执行对应的模块加载机制。
最终将加载到的模块挂载module.exports
中。tryModuleLoad
执行完毕之后module.exports
已经存在了,直接返回就可以了。
接下来,我们给模块添加缓存。就是文件加载的时候将文件放入缓存中,再去加载模块时先看缓存中是否存在,如果存在直接使用,如果不存在再去重新加载,加载之后再放入缓存。
增加功能:省略模块后缀名。
自动给模块添加后缀名,实现省略后缀名加载模块,其实也就是如果文件没有后缀名的时候遍历一下所有的后缀名看一下文件是否存在。
源代码调试
我们可以通过 VSCode 调试 Node.js
步骤
创建文件 a.js
1.文件 test.js
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 方法的整个流程代码已经调试完毕,通过对源代码的调试,可以帮助我们学习其实现思路,代码风格及规范,有助于帮助我们实现工具库,提升我们的代码思路,同时我们知道相关原理,也对我们解决日常开发工作中遇到的问题提供帮助。
版权声明: 本文为 InfoQ 作者【京东科技开发者】的原创文章。
原文链接:【http://xie.infoq.cn/article/662c1c45b4db628b597850a40】。文章转载请联系作者。
评论