Node.js Web应用代码热更新的另类思路
ccwgpt 2025-04-09 14:29 7 浏览 0 评论
背景
相信使用 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.js
function 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.js
module.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.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 等方式来触发更新。
如何释放老模块的资源
使用了新的 cleanCache 函数后,常规的使用就没有问题,然而并非就可以高枕无忧了。在 Node.js 中,除了 require 系统会添加引用外,通过 EventEmitter 进行事件监听也是大家常用的功能,并且 EventEmitter 有非常大的嫌疑会出现模块间的互相引用。那么 EventEmitter 能否正确的释放资源呢?答案是肯定的。
// code.js
var 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 这类框架,只需要按照文中的方法结合自身业务需要,对主路由进行一些改造,就可以很好的应用这项技术。
相关推荐
- NestJS入门教程系列一
-
介绍Nest(NestJS)是用于构建高效,可扩展的Node.js服务器端应用程序的框架。它使用渐进式JavaScript,内置并完全支持TypeScript(但开发人员仍然能够使用JavaScrip...
- 【推荐】一个网盘资源搜索与转存工具,支持移动端与PC端!
-
如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!项目介绍CloudSaver是一个基于Vue3和Express的网盘资源搜索与转存开源实用工具。它支持...
- Appium原理精讲
-
目前使用Appium新版本和旧版本的企业数目都很多,而两个版本的安装过程和api的使用又有较大的区别。但是无论表面上的东东如何变化,内部原理都是一样的。在这里我给大家介绍一下appium的核心,增进大...
- Kubernetes最小部署单元Pod
-
一、Kubernetes与Pod简介在当今云计算和容器化技术盛行的时代,Kubernetes已然成为容器编排领域的中流砥柱。它是一个开源的容器编排平台,由Google基于其内部使用的Bo...
- 最常用的四种跨域解决方案
-
前置知识什么是跨域?浏览器发送的请求地址(URL)与所在页面的地址不同(端口/协议/域名其一不同)。简言之,浏览器发出的请求url,与其所在页面的url不一样。此时,同源策略会让浏览器拒收服务器...
- Bolt.New —— 全栈AI Web自动编程
-
Bolt.New是由StackBlitz公司推出的,全栈AI工具,代码编辑、运行、部署,通通一站式搞定。它使用WebContainers技术,无需任何本地安装或配置,在浏览器中,就可以运行完整的No...
- Nodejs Express新手教程&高手进阶
-
NodejsExpress新手教程&高手进阶Express是一个NodeJS平台的框架,主要用于构于Web服务器项目。本文将通过示例介绍适合新手入门的Express基础使用,以及高手进阶知识,如:c...
- Express.js 创建Node.js Web应用
-
Express.js是一个基于Node.js的Web应用框架,框架的设计目的是构建应用的架构和简化应用的开发。框架会解决一些通用的问题,在Express.js中,Express框架会处理如:中间件、代...
- JavaScript 的 Express.js 功能及应用场景详解
-
Express.js是一个基于Node.js的轻量级Web应用框架,主要用于快速构建服务器端应用和API。它的核心功能包括以下关键点:1.路由管理URL路径与HTTP方法映射:通过...
- nodejs的express4文件下载
-
在nodejs的express框架中,下载变得非常简单,就一个方法,res.download()首先express命令行生成项目基本框架:不会的看这里:http://blog.csdn.net/zz...
- Express 系列:快速生成一个项目
-
系列预告本系列将以一个项目入手结合相关技术细节来带领大家一起学习Express这个基于Node.js的后端框架。本文首先将介绍:如何快速的生成一个具有一定结构的Express项目。Express项目结...
- nodejs的express自动生成项目框架
-
nodejs版本为:4.X,express版本为4.X1.全局安装2个模块express、express-generator在命令行输入:npminstall-gexpressnpminsta...
- express开发(一)简介与搭建
-
上周末去了趟上海书城,不愧是上海数得上号的书城,流行的科技书应有尽有,话不多说直接上图。最经典的C语言O(∩_∩)O最流行的java(づ ̄3 ̄)づ超酷的R语言/(ㄒoㄒ)/~~然而,身为一个坚定的前...
- Vue+Echarts可视化大屏系统后端框架搭建(附代码)
-
各位同学,大家好。上节课,前面我们讲解了Vue+Echarts前端部分的设计方法。这节课程,我们开始讲解使用Express进行后端设计的方法。01项目相关理论介绍什么是expressExpress是...
- Shopify电商API接口开发
-
Shopify电商API接口开发上线流程主要包括以下步骤。北京木奇移动技术有限公司,专业的软件外包开发公司,欢迎洽谈合作。前期准备-注册Shopify账号:在Shopify官网注册,用于后续开发测试...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- MVC框架 (46)
- spring框架 (46)
- 框架图 (58)
- bootstrap框架 (43)
- flask框架 (53)
- quartz框架 (51)
- abp框架 (47)
- jpa框架 (47)
- laravel框架 (46)
- express框架 (43)
- scrapy框架 (52)
- beego框架 (42)
- java框架spring (43)
- grpc框架 (55)
- 前端框架bootstrap (42)
- orm框架有哪些 (43)
- ppt框架 (48)
- 内联框架 (52)
- winform框架 (46)
- gui框架 (44)
- cad怎么画框架 (58)
- ps怎么画框架 (47)
- ssm框架实现登录注册 (49)
- oracle字符串长度 (48)
- oracle提交事务 (47)