在上篇文章 Node.js 模块机制及源码分析 中,通过分析 Node.js 中模块的加载源码,基本理解了 Node.js 的模块加载原理,其中 Module.prototype._compile 函数主要用于对于第三方 js 文件进行编译加载,所以我们可以巧妙的在 Module.prototype._compile 运行前后执行一些自己的代码,就能实现出意向不到的效果。
最近在看赵坤大神的 《Node.js调试指南》,其中它在讲调试工具和日志定位问题时,引用自己写的两个库: proxy-hot-reload 和 koa-await-breakpoint,proxy-hot-reload 用于实现 Node.js 的热更新,koa-await-breakpoint 用于实现在每个 await 执行的语句前后进行打点工作。这两个库都巧妙的使用了 Module.prototype._compile 函数,下面我们通过分析这两个库的源码来学习如何妙用 Module.prototype._compile 函数。
写这篇文章我只是想借用 proxy-hot-reload 和 koa-await-breakpoint 两个模块来了解一下 Module.prototype._compile 的原理,以及如何去 wrap 这个函数,对于这两个模块的使用,正如 @hyj1991 评论里所说,会有性能开销和内存泄漏:
1.第一个包 delete require.cache 依旧会有内存泄漏的问题,只能在开发环境下使用(话说很多框架都实现了文件变动热重启,也不需要这样的热更新方案)
2.async/await 在目前的机制下有性能损耗的问题(而且是不能忽视的性能损耗),用于大流量的线上服务器一样得关注性能损耗
所以建议不要在生产环境使用,可以作为本地调试工具或者学习 Node 模块机制使用。
proxy-hot-reload 源码分析
使用 proxy—hot-reload 实现热加载功能很简单,只需要在文件中引入如下代码便可:
require
我们知道 Node 模块加载是有缓存的,第一次启动加载成功后便将编译结果放入缓存中,也就是 require.cache 中,后面即使文件内容有更新也不会重新加载,除非你重启整个 Node 进程,如果你想实现对文件的热加载,必须删除 require.cache 中缓存,让其重新加载编译。而 proxy-hot-reload 便是借助 Module.prototype._compile 函数在编译时通过 Proxy 代理 exports 的属性进行拦截实现的:
module
koa-await-breakpoint 源码分析
koa-await-breakpoint 是一个 koa2 的中间件,用于对 await 执行的表达式进行打点,记录当前 await 处于第几步,耗时多长时间,执行结果是多少,并且可以很方便找到当前请求的 ctx,用法如下:
const
它的实现原理同样是通过对 Module.prototype._compile 函数进行包装改造,Module.prototype._compile,在模块加载的前后修改原始文件的代码,在其中加入打点代码便可实现动态自动打点。
该模块使用到了模块如下:
- node-glob: 根据模式匹配查找文件
- esprima:对代码进行语法分析,解析成语法树
- escodegen:根据语法树重新生成代码
- async_hooks:于跟踪 Node.Js 中的异步资源的生命周期
- shimmer:wrap 对象的函数属性
下面的源码分析也是根据这几个模块展开的:
1. node-glob 模块查找需要打点的文件
node-glob 模块可以根据文件模式匹配规则找到你需要的文件:
const
2. 使用 esprima 生成代码语法树
使用 esprima 模块对每个需要监听的文件的 content 进行语法分析,content 其实就是 Module.prototype._compile 函数的第一个参数的,对 content 调用 esprima.parse 函数得到语法树:
parsedCodes
分析 parsedCodes 找到 AwaitExpression 节点,包装每个 AwaitExpression 节点的表达式,加入打点代码:
function
3. 使用 escodegen 重新生成 code content
findAwaitAndWrapLogger 生成了新的语法树,我们可以使用 escodegen 将新的语法树转变成 code content:
const
4. global[loggerName] 打点记录:
//该函数会对每个 await 节点的代码进行 wrap, fn 是原先在 await 要执行的函数, ctx 是 fn 所在上下文,fnStr 是 await 后的表达式字符串
5. 巧妙使用 async_hooks 获取每个节点对应请求的 ctx
关于 async_hooks 模块如何使用,我在这篇博客 学习使用 Node.js 中 async-hooks 模块 中有详细的介绍,我们主要看一下如何利用 async_hooks 获取每个函数对应的请求上下文:
async_hooks
6. 使用 shimmer 包装 Module.prototype._compile 完成最终需求
shimmer
参考文献
- Node.js 调试指南
- Node.js 模块机制及源码分析
- 学习使用 Node.js 中 async-hooks 模块