相信利用 Node.js 开拓过 Web 运用的同学一定苦恼过新修正的代码必须要重启 Node.js 进程后才能更新的问题。习气利用 PHP 开拓的同学更会非常的不适用,大呼果真还是我大PHP才是天下上最好的编程措辞。手动重启进程不仅仅是非常恼人的重复劳动,当运用规模稍大往后,启动韶光也逐渐开始不容忽略。
当然作为程序猿,无论利用哪种措辞,都不会让这样的事情折磨自己。办理这类问题最直接和普适的手段便是监听文件修正并重启进程。这个方法也已经有很多成熟的办理方案供应了,比如已经被弃坑的 node-supervisor,以及现在比较火的 PM2 ,或者比较轻量级的 node-dev 等等均是这样的思路。
本文则供应了其余一种思路,只须要很小的改造,就可以实现真正的0重启热更新代码,办理 Node.js 开拓 Web 运用时恼人的代码更新问题。

总体思路
提及代码热更新,当下最有名确当属 Erlang 措辞的热更新功能,这门措辞的特色在于高并发和分布式编程,紧张的运用处景则是类似证券交易、游戏做事端等领域。这些场景都或多或少哀求做事拥有在运行中运维的手段,而代码热更新便是个中非常主要的一环,因此我们可以先大略的理解一下 Erlang 的做法。
由于我也没有利用过 Erlang ,以下内容均为道听途说,如果希望深入和准确的理解 Erlang 的代码热更新实现,最好还是查阅官方文档。
Erlang
的代码加载由一个名为code_server
的模块管理,除了启动时的一些必要代码外,大部分的代码均是由code_server
加载。当code_server
创造模块代码被更新后,会重新加载模块,此后的新要求会利用新模块实行,而原有还在实行的要求则连续利用老模块实行。老模块会在新模块加载后,被打上old
标签,新模块则是current
标签。当下一次热更新的时候,Erlang
会扫描还在实行老模块的进行并杀掉,再连续按照这个逻辑更新模块。Erlang
中并非所有代码均许可热更新,如kernel, stdlib, compiler
等根本模块默认是不许可更新的我们可以创造 Node.js 中也有与code_server类似的模块,即 require 体系,因此 Erlang 的做法该当也可以在 Node.js 上做一些考试测验。通过理解 Erlang 的做法,我们可以大概的总结出在 Node.js 中办理代码热更新的关键问题点
如何更新模块代码如何利用新模块处理要求如何开释老模块的资源那么接下来我们就逐个的解析这些问题点。
如何更新模块代码
要办理模块代码更新的问题,我们就须要去阅读 Node.js 的模块管理器实现,直接上链接 module.js。通过大略的阅读,我们可以创造核心的代码就在于 Module._load ,轻微精简一下代码贴出来。
// Check the cache for the requested file.// 1. If a module already exists in the cache: return its exports object.// 2. If the module is native: call `NativeModule.require` with the// filename and return the result.// 3. Otherwise, create a new module for the file and save it to the cache.// Then have it load the file contents before returning its exports// object.Module._load = function(request, parent, isMain) {var filename = Module._resolveFilename(request, parent);var cachedModule = Module._cache[filename];if (cachedModule) {return cachedModule.exports;}var module = new Module(filename, parent);Module._cache[filename] = module;module.load(filename);return module.exports;};require.cache = Module._cache;
可以创造个中的核心便是 Module._cache ,只要打消了这个模块缓存,下一次 require 的时候,模块管理器就会重新加载最新的代码了。
写一个小程序验证一下:
// main.jsfunction cleanCache (module) {var path = require.resolve(module);require.cache[path] = null;}setInterval(function {cleanCache('./code.js');var code = require('./code.js');console.log(code);}, 5000);// code.jsmodule.exports = 'hello world';
我们实行一下 main.js ,同时取修正 code.js 的内容,就可以创造掌握台中,我们代码成功的更新为了最新的代码。
那么模块管理器更新代码的问题已经办理了,接下来再看看在 Web 运用中,我们如何让新的模块可以被实际实行。
如何利用新模块处理要求
为了更符合大家的利用习气,我们就直接以 Express 为例来展开这个问题,实际上利用类似的思路,绝大部分 Web运用 均可适用。
首先,如果我们的做事是像 Express 的 DEMO 一样所有的代码均在同一模块内的话,我们是无法针对模块进行热加载的
var express = require('express');var app = express;app.get('/', function(req, res){res.send('hello world');});app.listen(3000);
要实现热加载,和 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('/', function(req, res){res.send('hello world');});module.exports = router;
然而很遗憾,经由这样处理之后,虽然成功的分离了核心代码, router.js 依然无法进行热更新。首先,由于缺少对更新的触发机制,做事无法知道该当何时去更新模块。其次, app.use 操作会一贯保存老的 router.js 模块,因此纵然模块被更新了,要求依然会利用老模块处理而非新模块。
那么连续改进一下,我们须要对 app.js 稍作调度,启动文件监听作为触发机制,并且通过闭包来办理 app.use 的缓请安题
// app.jsvar 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 等办法来触发更新。
如何开释老模块的资源
利用了新的 cleanCache 函数后,常规的利用就没有问题,然而并非就可以无忧无虑了。在 Node.js 中,除了 require 系统会添加引用外,通过 EventEmitter 进行事宜监听也是大家常用的功能,并且 EventEmitter 有非常大的嫌疑会涌现模块间的相互引用。那么 EventEmitter 能否精确的开释资源呢?答案是肯定的。
// code.jsvar moduleA = require('events').EventEmitter;moduleA.on('whatever', function {});
当 code.js 模块被更新,并且所有引用被移出后,只要 moduleA 没有被其他未开释的模块引用, moduleA 也会被自动开释,包括我们在其内部的事宜监听。
只有一种畸形的 EventEmitter 运用处景在这套体系下无法应对,即 code.js 每次实行的时候都会去监听一个全局工具的事宜,这样会造玉成局工具上一直的挂载事宜,同时 Node.js 会很快的提示检测到过多的事宜绑定,疑似内存透露。
至此,可以看到只要处理好了 require 系统中 Node.js 为我们自动添加的引用,老模块的资源回收并不是大问题,虽然我们无法做到像 Erlang 一样实现下一次热更新对还留存的老模块进行扫描这样细粒度的掌握,但是我们可以通过合理的规避手段,办理老模块资源开释的问题。
在 Web 运用下,还有一个引用问题便是未开释的模块或者核心模块对须要热更新的模块有引用,如 app.use,导致老模块的资源无法开释,并且新的要求无法精确的利用新模块进行处理。办理这个问题的手段便是掌握全局变量或者引用的暴露的入口,在热更新实行的过程中手动更新入口。如 如何利用新模块处理要求 中对 router 的封装便是一个例子,通过这一个入口的掌握,我们在 router.js 中无论如何引用其他模块,都会随着入口的开释而开释。
另一个会引起资源开释问题的便是类似 setInterval 这类操作,会保持工具的生命周期无法开释,不过在 Web 运用中我们极少会利用这类技能,因此方案中并未关注。
尾声
至此,我们就办理了 Node.js 在 Web 运用下代码热更新的三大问题,不过由于 Node.js 本身缺少对有效的留存工具的扫描机制,因此并不能100%的肃清类似 setInterval 导致的老模块的资源无法开释的问题。也是由于这样的局限性,目前我们供应的 YOG2 框架中,紧张还是将此技能运用于开拓调试期,通过热更新实现快速开拓。而生产环境的代码更新依然利用重启或者 PM2 的 hot reload 功能来担保线上做事的稳定性。
由于热更新实际上与框架和业务架构紧密干系,因此本文并未给出一个通用的办理方案。作为参考,大略的先容一下在 YOG2 框架中我们是如何利用这项技能的。由于 YOG2 框架本身就支持前后端子系统 App 拆分,因此我们的更新策略因此 App 为粒度更新代码。同时由于类似 fs.watch 这类操作会有兼容性问题,一些替代方案如 fs.watchFile 则会比较花费性能,因此我们结合了 YOG2 的测试机支配功能,通过上传支配新代码的形式奉告框架须要更新 App 代码。在以 App 为粒度更新模块缓存的同时,会更新路由缓存与模板缓存,来完成所有代码的更新事情。
如果你利用的是类似 Express 或者 Koa 这类框架,只须要按照文中的方法结合自身业务须要,对主路由进行一些改造,就可以很好的运用这项技能。