浅谈 Node.js 热更新

2023-11-06

e97ad0bd692da60474b63e4a1962bf64.png

大厂技术  高级前端  Node进阶

点击上方 程序员成长指北,关注公众号

回复1,加入高级Node交流群

记得在 15 16 年那会 Node.js 刚起步的时候,我在去前东家的入职面试也被问到了要如何实现 Node.js 服务的热更新。

其实早期从 Php-fpm / Fast-cgi 转过来的 Noder,肯定非常喜欢这种更新业务逻辑代码无需重启服务器即可生效的部署方案,它的优势也非常明显:

  • 无需重启服务意味着用户连接不会中断,尤其对于大量长链接 hold 的应用

  • 文件更新加载缓存是一个非常快的过程,可以完成毫秒级别的应用更新

热更新的副作用也非常多,比如常见的内存泄露(资源泄露),本文将以 clear-module 和 decache 这两个下载量比较高的热门热更辅助模块来探讨下热更究竟会给我们的应用带来哪些问题。

热更实现原理

在开始谈热更新的问题之前,我们首先要了解下 Node.js 的模块机制的概貌,这样对于后面它带来的问题将能有更加深刻的理解和认识。

Node.js 自己实现的模块加载机制如下图所示:

c285c996d23af213a39106cbb397b1da.png
简单地说父模块 A 引入子模块 B 的步骤如下:

  • 判断子模块 B 缓存是否存在

  • 如果不存在则对 B 进行编译解析

    • 添加 B 模块缓存至require.cache(其中 key 为模块 B 的全路径)

    • 添加 B 模块引用至父模块 A 的children数组中

  • 如果存在,判断父模块 A 的children数组中是否存在 B,如不存在则添加 B 模块引用。

其实到了这里,我们已经可以发现要实现没有内存泄露的热更新,需要断开待热更模块的以下引用链路:

f21520f96fd38d215331436a44235c2a.png
这样当我们再次去require子模块 B 的时候,就会重新从磁盘读取 B 模块的内容然后进行编译引入内存,据此实现了热更的能力。

实际上,第一节中提到的clear-moduledecache两个包都是按照这个思路实现的模块热更,当然它们考虑的会更加完善一些,比如将子模块 B 本身的依赖也一并清除,以及对于循环引用场景的处理。

那么,借助于这两个模块,Node.js 应用的热更新是不是就完美无缺了呢?我们接着看。

问题一:内存泄露

内存泄露是一个非常有意思的问题,凡是进入 Node.js 全栈开发深水区的同学基本或多或少都会遇到内存泄露的问题,那么从我个人的故障排查定位经验来说,开发者其实不需要畏惧内存泄露,因为相比其它摸不着头脑的问题,内存泄露是一个只要你熟悉代码并且肯花时间百分百可解的故障类型。

这里我们来看看看似清除了所有旧模块引用的热更方案,又会以怎样的形式产生内存泄露现象。

decache

考虑构造以下热更例子,先使用decache进行测试:

'use strict';

const cleanCache = require('decache');

let mod = require('./update_mod.js');
mod();
mod();

setInterval(() => {
  cleanCache('./update_mod.js');
  mod = require('./update_mod.js');
  mod();
}, 100);

这个例子中相当于在不断清理./update_mod.js这个模块的缓存进行热更,它的内容如下:

'use strict';

const array = new Array(10e5).fill('*');
let count = 0;

module.exports = () => {
  console.log('update_mod', ++count, array.length);
};

为了能快速观察到内存泄露现象,这里构造了一个大数组来替代常规的模块闭包引用。

为了方便观察我们可以在index.js中可以添加一个方法来定时打印当前的内存状况:

function printMemory() {
  const { rss, heapUsed } = process.memoryUsage();
  console.log(`rss: ${(rss / 1024 / 1024).toFixed(2)}MB, heapUsed: ${(heapUsed / 1024 / 1024).toFixed(2)}MB`);
}

printMemory();
setInterval(printMemory, 1000);

最后执行node index.js文件,可以看到内存迅速溢出:

update_mod 1 1000000
update_mod 2 1000000
rss: 34.59MB, heapUsed: 11.51MB
update_mod 1 1000000
rss: 110.20MB, heapUsed: 80.09MB
update_mod 1 1000000
...

rss: 921.63MB, heapUsed: 888.99MB
update_mod 1 1000000
rss: 998.09MB, heapUsed: 965.12MB
update_mod 1 1000000
update_mod 1 1000000

<--- Last few GCs --->

[50524:0x158008000]    13860 ms: Scavenge 1018.3 (1024.6) -> 1018.3 (1028.6) MB, 2.3 / 0.0 ms  (average mu = 0.783, current mu = 0.576) allocation failure 
[50524:0x158008000]    14416 ms: Mark-sweep (reduce) 1026.0 (1036.3) -> 1025.9 (1029.3) MB, 457.8 / 0.0 ms  (+ 86.6 ms in 77 steps since start of marking, biggest step 8.7 ms, walltime since start of marking 555 ms) (average mu = 0.670, current mu = 0.360

<--- JS stacktrace --->

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory

抓取堆快照后进行分析:
67264dcd2c43f62b194d4a538c04d27c.png
很明显Module@39215children数组中大量塞入了重复的热更模块update_mod.js的编译结果导致了内存泄露,而进一步查看Module@39215信息:
25cb712e2ea7fa389f6a466a6ff47400.png
可以看到其正是入口的index.js

阅读decache实现源代码后发现,产生泄露的原因则是我们在热更实现原理一节中提到的要去掉全部的三条引用,而遗憾的是decache仍然只断开了最基础的require.cache这一条引用链路:
fc515b9bf0bc32d566c26f3c36b57fdb.png

至此,decache由于最基本的热更内存问题都尚未解决,白瞎了其 94w 的月下载量,可以直接排出我们的热更方案参考。

参考:

  • decache 问题源码实际位置:https://github.com/dwyl/decache/blob/main/decache.js#L35

clear-module

接下来我们看看月下载量为 19w 的clear-module表现如何。

由于前一小节中的测试代码代表了最基础的模块热更场景,且clear-moduleAPI使用和decache基本一致,所以我们仅替换cleanCache引用即可进行本轮测试:

// index.js
const cleanCache = require('clear-module');

同样执行node index.js文件,可以看到内存变化如下:

update_mod 1 1000000
update_mod 2 1000000
rss: 35.00MB, heapUsed: 11.58MB
update_mod 1 1000000
rss: 110.69MB, heapUsed: 80.10MB
update_mod 1 1000000
rss: 187.36MB, heapUsed: 156.52MB
update_mod 1 1000000
rss: 256.28MB, heapUsed: 225.26MB
update_mod 1 1000000
rss: 332.78MB, heapUsed: 301.71MB
update_mod 1 1000000
rss: 401.61MB, heapUsed: 370.38MB
update_mod 1 1000000
rss: 42.67MB, heapUsed: 11.17MB
update_mod 1 1000000
rss: 65.63MB, heapUsed: 34.15MB
update_mod 1 1000000

这里可以发现,clear-module内存趋势呈现波浪形,说明它完美处理了原理一节中提到的旧模块的全部引用,使得热更前的旧模块可以被正常 GC 掉。

经过源代码查阅,发现clear-module确实将父模块对子模块的引用也一并清除:
767dc5590db85d15f28a4bc9f82d64c2.png

因此这个例子中热更不会导致进程内存泄露 OOM。

详细代码可以参见:https://github.com/sindresorhus/clear-module/blob/main/index.js#L25-L31

那么是不是认为clear-module就可以高枕无忧没有内存烦恼了呢?

其实不然,我们接着对上面的index.js进行一些小小的改造:

'use strict';

const cleanCache = require('clear-module');

let mod = require('./update_mod.js');
mod();
mod();

require('./utils.js');

setInterval(() => {
  cleanCache('./update_mod.js');
  mod = require('./update_mod.js');
  mod();
}, 100);

对比之前新增了一个utils.js,它的逻辑相当简单:

'use strict';

require('./update_mod.js')

setInterval(() => require('./update_mod.js'), 100);

对应的场景其实就是index.js中清理掉update_mod.js后,同样使用到的这个模块的utils.js也重新进行require引入保持使用最新的热更模块逻辑。

继续执行node index.js文件,可以看到这次又出现内存迅速溢出的现象:

update_mod 1 1000000
update_mod 2 1000000
rss: 34.59MB, heapUsed: 11.51MB
update_mod 1 1000000
rss: 110.20MB, heapUsed: 80.09MB
update_mod 1 1000000
...

rss: 921.63MB, heapUsed: 888.99MB
update_mod 1 1000000
rss: 998.09MB, heapUsed: 965.12MB
update_mod 1 1000000
update_mod 1 1000000

<--- Last few GCs --->

[53359:0x140008000]    13785 ms: Scavenge 1018.5 (1025.1) -> 1018.5 (1029.1) MB, 2.2 / 0.0 ms  (average mu = 0.785, current mu = 0.635) allocation failure 
[53359:0x140008000]    14344 ms: Mark-sweep (reduce) 1026.1 (1036.8) -> 1025.9 (1029.3) MB, 462.2 / 0.0 ms  (+ 87.7 ms in 89 steps since start of marking, biggest step 7.5 ms, walltime since start of marking 559 ms) (average mu = 0.667, current mu = 0.296

<--- JS stacktrace --->

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory

继续抓取堆快照进行分析:

f32255957b4fe6b3949d77f38ad47488.png
这次是在Module@37543children数组下有大量重复的热更模块upload_mod.js导致了内存泄露,我们来看下Module@37543的详细信息:

8b6b2ab325bdaceb41313977ad58364b.png
是不是感觉很奇怪,clear-module明明清理掉了父模块对热更子模块的引用(反应到这个例子中是index.js这个父模块),但是utils.js里面却还保留了这么多旧引用呢?

其实这里是因为,Node.js 的模块实现机制里,子模块和父模块其实本质上是多对多的关系,而又因为模块缓存的机制,子模块仅会在第一次被加载的时候执行构造函数初始化:

9600fba048ebb60223af963e6992b944.png
这样就意味着,clear-module里所谓的去掉父模块对热更模块的旧引用仅仅是第一次引入热更模块对应的这个父模块,在这个例子中就是index.js,所以index.js对应的children数组是干净的。

utils.js作为父模块引入热更模块时,读取的是热更模块最新版本的缓存,更新children引用:

6d245ec7896cbaa5a4f6640f9ba07ff9.png
它会去判断这个缓存对象在children数组中不存在的话则加入进去,显然热更前后两次编译update_mod.js得到的内存对象不是同一个,因此在utils.js中产生了泄露。

至此在稍微复杂的点逻辑下,clear-module也败下阵来,考虑到实际开发中的逻辑负载度会比这个高很多,显然在生产中使用热更新,除非作者对模块机制掌控十分透彻,否则还是在给自己给后人挖坑。

留一个有趣的思考clear-module在这种场景下的泄露也并非无解,有兴趣的同学可以参照原理思考下如何来规避在此场景下的热更内存泄露。

参考:

  • 设置父模块: https://github.com/nodejs/node/blob/v16.13.2/lib/internal/modules/cjs/loader.js#L176

  • 更新引用: https://github.com/nodejs/node/blob/v16.13.2/lib/internal/modules/cjs/loader.js#L167

lodash

可能有同学会觉得上面这个例子还不够典型,我们来看一个开发者完全无法控制的非幂等子依赖模块因为热更而导致重复加载产生的内存泄露案例。

这里也不去为了构造内存泄露特意去找很偏门的包,我们就以周下载量高达 3900w 的非常常用的工具模块  lodash 为例,继续修改我们的 uploda_mod.js:

'use strict';

const lodash = require('lodash');
let count = 0;
module.exports = () => {
  console.log('update_mod', ++count);
};

接着在 index.js 中去掉上面的 utils.js,保持只对 update_mod.js 进行重复热更:

'use strict';

const cleanCache = require('clear-module');

let mod = require('./update_mod.js');
mod();
mod();

setInterval(() => {
  cleanCache('./update_mod.js');
  mod = require('./update_mod.js');
  mod();
}, 10);

function printMemory() {
  const { rss, heapUsed } = process.memoryUsage();
  console.log(`rss: ${(rss / 1024 / 1024).toFixed(2)}MB, heapUsed: ${(heapUsed / 1024 / 1024).toFixed(2)}MB`);
}

printMemory();
setInterval(printMemory, 1000);

然后执行 node index.js 文件,可以看到这次又双叕泄露了,随着 update_mod.js 热更,堆内存迅速上升最后 OOM。

在这个案例中,非幂等执行的子模块产生泄露的原因稍微复杂一些,涉及到 lodash 模块重复编译执行会造成闭包循环引用。

其实会发现,引入模块对开发者是不可控的,换句话说开发者是无法确认自己是否引入了可以幂等执行的公共模块,那么对于像 lodash 这种无法幂等执行的库,热更就会造成其产生内存泄露。

问题二:资源泄露

讲完了热更可能引发的内存问题场景,我们来看看热更会导致的另一类相对更加无解一些资源泄露问题。

我们依旧以简单的例子来进行说明,首先还是构造index.js

'use strict';

const cleanCache = require('clear-module');

let mod = require('./update_mod.js');

setInterval(() => {
  cleanCache('./update_mod.js');
  mod = require('./update_mod.js');
  console.log('-------- 热更新结束 --------')
}, 1000);

这次我们直接使用clear-module进行热更新操作,引入待热更模块update_mod.js如下:

'use strict';

const start = new Date().toLocaleString();

setInterval(() => console.log(start), 1000);

update_mod.js中我们创建了一个定时任务,以 1s 的间隔输出模块第一次被引入时的时间。

最后执行node index.js可以看到如下结果:

2022/1/21 上午9:37:29
-------- 热更新结束 --------
2022/1/21 上午9:37:29
2022/1/21 上午9:37:30
-------- 热更新结束 --------
2022/1/21 上午9:37:29
2022/1/21 上午9:37:30
2022/1/21 上午9:37:31
-------- 热更新结束 --------
2022/1/21 上午9:37:29
2022/1/21 上午9:37:30
2022/1/21 上午9:37:31
2022/1/21 上午9:37:32
-------- 热更新结束 --------
2022/1/21 上午9:37:29
2022/1/21 上午9:37:30
2022/1/21 上午9:37:31
2022/1/21 上午9:37:32
2022/1/21 上午9:37:33
-------- 热更新结束 --------
2022/1/21 上午9:37:29
2022/1/21 上午9:37:30
2022/1/21 上午9:37:31
2022/1/21 上午9:37:32
2022/1/21 上午9:37:33
2022/1/21 上午9:37:34

显然,clear-module虽然正确清除了热更模块旧引用,但是旧模块内部的定时任务并没有被一起回收进而产生了资源泄露。

实际上,这里的定时任务只是资源中的一种而已,包括socketfd在内的各种系统资源操作,均无法在仅仅清除掉旧模块引用的场景下自动回收。

问题三:ESM 喵喵喵?

不管是decache还是clear-module,都是在 Node.js 实现的 CommonJS 模块机制的基础上进行的热更逻辑整合。

但是整个前端发展到今天,原生 ECMA 规范定义的模块机制为 ESModule(简称 ESM),因为是规范定义的,所以其实现是在引擎层面,对应到 Node.js 这一层则是由 V8 实现的,因此目前的热更无法作用于 ESM 模块。

不过在我看来,基于 CommonJS 的热更因为实现在更加上层,会暗藏各种坑所以非常不推荐在生产中使用,但是基于 ESM 的热更如果规范能定义完整的模块加载和卸载机制,反而是真正的热更新方案的未来。

Node.js 在这一块也有对应的实验特性可以加以利用,详情参见:ESM Hooks。(https://nodejs.org/dist/latest/docs/api/esm.html#esm_hooks)不过目前其仅处于 Stability: 1 的状态,需要持续观望下。

问题四:模块版本混乱

Node.js 的热更新实际上并不是很多同学想象中的那种全局旧模块替换,因为缓存机制可能会导致内存中同时存在多个被热更模块的不同版本,从而造成一些难以定位的奇怪 Bug。

我们继续构造一个小例子来进行说明,首先编写待热更模块update_mod.js

'use strict';

const version = 'v1';

module.exports = () => {
  return version;
};

然后添加一个utils.js来正常使用此模块:

'use strict';

const mod = require('./update_mod.js');

setInterval(() => console.log('utils', mod()), 1000);

接着编写启动入口index.js进行热更新操作:

'use strict';

const cleanCache = require('clear-module');

let mod = require('./update_mod.js');

require('./utils.js');

setInterval(() => {
  cleanCache('./update_mod.js');
  mod = require('./update_mod.js');
  console.log('index', mod())
}, 1000);

此时当我们执行node index.js且不更改update_mod.js时可以看到:

utils v1
index v1
utils v1
index v1

说明内存中的update_mod.js都是v1版本。

无需重启刚才的服务,我们修改update_mod.js中的version

// update_mod.js
const version = 'v2';

接着观察到输出变成了:

index v1
utils v1
index v2
utils v1
index v2
utils v1

index.js中进行了热更新操作,因此它重新require到的update_mod.js变成了最新的v2版本,而utils.js中并不会有任何变化。

类似这种一个模块多个版本的状况,不仅会增加线上故障的问题定位难度,某种程度上,它也造成了内存泄露。

适合热更新的场景

抛开场景谈问题都是耍流氓,虽然写了这么多热更新存在的问题,但是确实也有非常模块热更新的使用场景,我们从线上和线下两个维度来探讨下。

对于线下场景,轻微的内存和资源的泄露问题可以让位于开发效率,所以热更新非常适合于框架在 dev 模式下的单模块加载与卸载。

而对于线上场景,热更新也并非一无用处,比如明确父子依赖一对一且不创建资源属性的内聚逻辑模块,可以通过合适的代码组织来进行热插拔,达到无缝发布更新的目的。

最后总的来说,因为不熟悉而给应用下毒的风险与热更的收益,就目前我个人还是比较反对将热更新技术用户线上的生产环境中;而如果后面对 ESM 模块的加载与卸载机制能明确下沉至规范由引擎实现,可能才是热更新真正可以广泛和安全使用的恰当时机。

一些总结

前几年参与维护 AliNode 的过程中,处理了多起热更新引起的内存泄露问题,恰好借着编写本文的机会对以前的种种案例进行了回顾。

目前实现热更新的模块其实都可以归结到 “黑魔法” 一类中,与 “黑科技” 相比,“黑魔法” 是一把双刃剑,使用之前还需要谨慎切勿伤到自己。

Node 社群


我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。

如果你觉得这篇内容对你有帮助,我想请你帮我2个小忙:

1. 点个「在看」,让更多人也能看到这篇文章2. 订阅官方博客 www.inode.club 让我们一起成长

点赞和在看就是最大的支持❤️
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

浅谈 Node.js 热更新 的相关文章

随机推荐

  • (2019.8.20半解决)Solving environment: failed with initial frozen solve. Retrying with flexible solve.Co

    用conda命令在linux安装python库出现上述错误 这里提到了这个问题 有人建议更新conda 我更新后无效 不过 conda不行 但是pip可以安装 问题先这样 后续有时间再仔细研究
  • ThreadLocal和ThreadLocalMap

    1 ThreadLocal是什么 是用来存放我们需要能够线程隔离的变量的 那就是线程本地变量 也就是说 当我们把变量保存在ThreadLocal当中时 就能够实现这个变量的线程隔离了 entry中的key使用了弱引用 static clas
  • 系统之家装机大师如何制作U盘启动盘?

    U盘的用途非常广泛 现在很多人都会使用U盘重装系统 那就需要先把U盘制作成U盘启动盘来重装系统 那要如何制作U盘启动盘呢 下面小编就教教大家使用系统之家装机大师制作U盘启动盘的方法 系统之家一键重装系统工具下载 系统之家装机大师官方版下载1
  • 【Redis学习笔记】2:认识Redisson及其分布式锁RLock.lock()

    Redisson和Jedis类似 都是用Java实现的操作Redis的客户端 但是使用场景不同 Redisson更多用在分布式场景下 功能可以看wiki Jedis更多用在单机场景下 1 Java接入Redisson 以Spring Boo
  • SpringMVC——静态资源访问之

    web xml 配置
  • 关于不设置div高度时,背景图片或背景颜色不显示的问题

    在写一个网页时 遇到div高度不定时 即高度设为auto 或者不设置时 加入的的背景图片不能显示的问题 在查阅相关资料后发现几种解决方法 出现这种情况有两个前提 1 外部的div没有设置高度 2 内部div浮动 解决方法 1 最直接的方法
  • 多元时间序列

    目录 多元时间序列 RBF径向基神经网络多变量时间序列预测 Matlab完整程序 预测结果 基本介绍 程序设计 参考资料 多元时间序列 RBF径向基神经网络多变量时间序列预测 Matlab完整程序 预测结果 基本介绍 多元时间序列 Matl
  • docker修改镜像的存储位置_Docker中修改镜像、容器的存放目录

    系统是CentOS7 DOCKER 1 13版本 之后版本未测试过 以上步骤在该操作系统和Docker版本下已经测试过 解决方案 在默认情况下 Docker镜像和容器的默认存放位置为 var lib docker 一般根下分区我们不会给太大
  • 爬虫实战

    爬51job url里的内容 项目的开发工具选择Requests模块和BeautifulSoup模块实现爬虫开发与数据清洗 数据存储选择Sqlalchemy框架 数据库选择MySQL 获取城市编号 1 观察搜索页的URL地址 控制变量 观察
  • docker数据管理---数据卷,数据卷容器

    在Docker中 数据卷 data volumes 和数据卷容器 data volume containers 是用于在容器之间共享和持久化数据的两种不同的机制 一 数据卷 数据卷是一个特殊的目录或目录 可以绕过容器文件系统的常规层 直接在
  • 输入一行字符,统计其中各种字符个数(C语言)

    输入一行字符 统计其中各种字符个数 编译环境VS2017 其中输出英文字母 数字 空格以及其他字符的个数 需要注意的是fgets 函数的使用 会在字符串末尾 0前 读入我们在键盘上敲的回车即换行符 n include
  • 自制Web桌面日历

    想要一个带农历的桌面日历 google了半天也没有找到中意的 但是经常浏览网站的时候发现很多不错的日历 直接保存到我的桌面上不就完了吗 下面是具体操作 将中意的日历页面 查看源文件 看是否可以看到script文件 如果可以 就保存到本地 用
  • 【雕爷学编程】Arduino动手做(202)---热释电效应、热释电元件与HC-SR505运动传感器模块

    37款传感器与模块的提法 在网络上广泛流传 其实Arduino能够兼容的传感器模块肯定是不止37种的 鉴于本人手头积累了一些传感器和执行器模块 依照实践出真知 一定要动手做 的理念 以学习和交流为目的 这里准备逐一动手试试多做实验 不管成功
  • vba中find方法查找1

  • QML ListView实现树形效果

    转自 http blog huati365 com 5jELjzLwnx3YGw import QtQuick 2 11 import QtQuick Controls 2 2 import QtQuick Controls Materia
  • Android抓包工具——Fiddler

    前言 在平时和其他大佬交流时 总会出现这么些话 抓个包看看就知道哪出问题了 抓流量啊 payload都在里面 这数据流怎么这么奇怪 这里出现的名词 其实都是差不多的意思啊 这都跟抓包这个词有关 说到抓包呢我们今天就先来了解一下抓包的一些基础
  • MySQL组成

    MySQL 的组成分为两部分 服务器端 服务的提供 相当于卖家 客户端 服务的使 相当于买家 消费者 服务器端只能有一个 而客户端可以有多个 安装了 MySQL 说明我们既是服务器端又是客户端 服务器端的服务体现就是 客户端是使 MySQL
  • 微软亚洲研究院实习生面试

    上周说到微软亚洲研究院 MSRA 一下就简称MSRA吧 网络面试我这边因为连不上而要重新安排面试 周五HR就给我电话约好今天中午1 00 3 00重新面试 于是提前做好各种准备 找了个拉ADSL的宿舍来上网 然后用了人家带摄像头的笔记本 还
  • B站创建视频分集播放列表

    上传视频在B站上创建视频分集列表方法 上传时创建分集列表 1 打开B站 2 登录B站后 点击投稿上传视频 3 上传视频或把视频直接拖拽到页面里 4 点击上传第一个视频后页面下会出现一个 号的按钮 点击 继续上传 上传视频就会出现两个正在上传
  • 浅谈 Node.js 热更新

    大厂技术 高级前端 Node进阶 点击上方 程序员成长指北 关注公众号 回复1 加入高级Node交流群 记得在 15 16 年那会 Node js 刚起步的时候 我在去前东家的入职面试也被问到了要如何实现 Node js 服务的热更新 其实