Node.js巧妙实现Web应用代码热更新(2)

要实现热加载,和 Erlang 中不允许的基础库一样,我们需要一些无法进行热更新的基础代码控制更新流程。而且类似 app.listen 这类操作如果重新执行了,那么和重启 Node.js 进程也没太大的区别了。因此我们需要一些巧妙的代码将频繁更新的业务代码与不频繁更新的基础代码隔离开。

// app.js 基础代码 var express = require('express'); var app = express(); var router = require('./router.js'); app.use(router); app.listen(3000); // router.js 业务代码 var express = require('express'); var router = express .Router(); // 此处加载的中间件也可以自动更新 router.use(express.static('public')); router.get('https://www.jb51.net/', function(req, res){ res.send('hello world'); }); module.exports = router;

然而很遗憾,经过这样处理之后,虽然成功的分离了核心代码, router.js 依然无法进行热更新。首先,由于缺乏对更新的触发机制,服务无法知道应该何时去更新模块。其次, app.use 操作会一直保存老的 router.js 模块,因此即使模块被更新了,请求依然会使用老模块处理而非新模块。

那么继续改进一下,我们需要对 app.js 稍作调整,启动文件监听作为触发机制,并且通过闭包来解决 app.use 的缓存问题

// app.js var express = require('express'); var fs = require('fs'); var app = express(); var router = require('./router.js'); app.use(function (req, res, next) { // 利用闭包的特性获取最新的router对象,避免app.use缓存router对象 router(req, res, next); }); app.listen(3000); // 监听文件修改重新加载代码 fs.watch(require.resolve('./router.js'), function () { cleanCache(require.resolve('./router.js')); try { router = require('./router.js'); } catch (ex) { console.error('module update failed'); } }); function cleanCache(modulePath) { require.cache[modulePath] = null; }

再试着修改一下 router.js 就会发现我们的代码热更新已经初具雏形了,新的请求会使用最新的 router.js 代码。除了修改 router.js 的返回内容外,还可以试试看修改路由功能,也会如预期一样进行更新。

当然,要实现一个完善的热更新方案需要更多结合自身方案做一些改进。首先,在中间件的使用上,我们可以在 app.use 处声明一些不需要热更新或者说每次更新不希望重复执行的中间件,而在 router.use 处则可以声明一些希望可以灵活修改的中间件。其次,文件监听不能仅监听路由文件,而是要监听所有需要热更新的文件。除了文件监听这种手段外,还可以结合编辑器的扩展功能,在保存时向 Node.js 进程发送信号或者访问一个特定的 URL 等方式来触发更新。

如何释放老模块的资源

要解释清楚老模块的资源如何释放的问题,实际上需要先了解 Node.js 的内存回收机制,本文中并不准备详加描述,解释 Node.js 的内存回收机制的文章和书籍很多,感兴趣的同学可以自行扩展阅读。简单的总结一下就是当一个对象没有被任何对象引用的时候,这个对象就会被标记为可回收,并会在下一次GC处理的时候释放内存。

那么我们的课题就是,如何让老模块的代码更新后,确保没有对象保持了模块的引用。首先我们以 如何更新模块代码 一节中的代码为例,看看老模块资源不回收会出现什么问题。为了让结果更显著,我们修改一下 code.js

// code.js var array = []; for (var i = 0; i < 10000; i++) { array.push('mem_leak_when_require_cache_clean_test_item_' + i); } module.exports = array; // app.js function cleanCache (module) { var path = require.resolve(module); require.cache[path] = null; } setInterval(function () { var code = require('./code.js'); cleanCache('./code.js'); }, 10);

好~我们用了一个非常笨拙但是有效的方法,提高了 router.js 模块的内存占用,那么再次启动 main.js 后,就会发现内存出现显著的飙升,不到一会 Node.js 就提示 process out of memory。然而实际上从 app.js 与 router.js 的代码中观察的话,我们并没发现哪里保存了旧模块的引用。

我们借助一些 profile 工具如 node-heapdump 就可以很快的定位到问题所在,在 module.js 中我们发现 Node.js 会自动为所有模块添加一个引用

function Module(id, parent) { this.id = id; this.exports = {}; this.parent = parent; if (parent && parent.children) { parent.children.push(this); } this.filename = null; this.loaded = false; this.children = []; }

因此相应的,我们可以调整一下cleanCache函数,将这个引用在模块更新的时候一并去除。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wgzppz.html