Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

小蝌蚪传记:nodejs线上模块热部署原理与实现——富婆的爱 #10

Open
airuikun opened this issue Jan 13, 2020 · 8 comments

Comments

@airuikun
Copy link
Owner

airuikun commented Jan 13, 2020

晚十二点,床上

富婆:这是给你的500元,辛苦了

小蝌蚪:不辛苦,主人

富婆:我老公要回来了,你走吧

小蝌蚪:是,主人

说完小蝌蚪从三楼别墅跳了下去

。。。

小蝌蚪是一名程序员,也是一个技师

白天敲代码,晚上捏脚

由于常年敲代码,所以指法高超

时常把客户按到上天日龙

。。。

富婆年轻、有钱,但是缺爱

因为嫁给了80岁老公

小蝌蚪听话活好不粘人

深得富婆的欢心

。。。

又一次见面

富婆:好无聊,赶紧讨好我

小蝌蚪:主人,我用手指在您腿上敲一段“Dijkstra最短路径算法”吧

富婆:这招你用过了

小蝌蚪:那我用胸毛在您背上默写《javascript高级程序设计》

富婆:不想

小蝌蚪:那我用舌头给您表演一段口技

富婆:舌头?口技?

小蝌蚪:不要多想,是正规表演

于是小蝌蚪表演了一段舌头碎大石

富婆大喜,掌声经久不衰

。。。

表演完毕,富婆面色泛红

富婆:我喜欢你

小蝌蚪:我也喜欢我自己

富婆:你爱我吗

小蝌蚪:我不能对客户动心

富婆:我要如何才能得到你的心

小蝌蚪:杀了我,把心掏出来,嘻嘻

富婆:嘻嘻

。。。

富婆:对了,下周来参加我的闺蜜派对

小蝌蚪:不带你老公去吗

富婆:他80岁了,带去丢脸

小蝌蚪:那我可要加钱噢

富婆:多少

小蝌蚪:200块钱两小时,物美价廉,服务一流

富婆:deal

。。。

闺蜜派对

晚八点,高级酒店,各种炫富

闺蜜A:我买了个包,才10万,好便宜哦~

闺蜜B:我包了三个小奶狗,让他们打了一晚上斗地主

闺蜜C:我老公养了好多小动物,比如宝马、路虎、捷豹

富婆默默上前:我在北京二环买了套四合院

其他闺蜜瞬间不说话了

。。。

小蝌蚪意识到

这不是一个普通的饭局

而是一场闺蜜之间的装逼盛宴

小蝌蚪:你需要我做什么

富婆:我需要你打败她们的男宠

。。。

酒后三巡

到了炫耀男宠的阶段

每个闺蜜都带来了自己包养的男宠

有肌肉男、高级鸭、男模等等

肌肉男上台就展示八块腹肌和沙包一样大的裤裆

男模直接亮出大长腿和帅到掉渣的脸

高级鸭把舌头伸出来,在台上发出“略略略略略...”的声音

男宠们各有所长

赢得了场下闺蜜们的掌声

。。。

轮到小蝌蚪上台

一身屎绿色格子衬衫

呆若木鸡

主持人问:辣鸡,你要展示什么特长

小蝌蚪:舌头碎大石

主持人:除了这个还有吗

小蝌蚪:胸毛碎大石

主持人暴怒:我怀疑你是来砸场的

小蝌蚪:是的,我就是来砸场的

主持人叫来20个保安

十秒钟后保安全部倒地抽搐

身上被小蝌蚪的胸毛刻满了神秘javascript代码

。。。

主持人:爸爸对不起,刚才是我冒犯了

主持人:爸爸,麻烦跟观众介绍一下自己

小蝌蚪从书包里掏出一千万美金(富婆送)

点燃后从98楼外抛出

燃烧的钱漫天飞舞

小蝌蚪介绍道:“我是颜色不一样的渣男”

台下所有女人狂轰乱叫

因过度兴奋而七窍流血

男宠们看到自己的主子疯狂跪舔小蝌蚪

深感自己被绿了

主持人:爸爸您还有什么温柔点的特长展示给大家吗

小蝌蚪:我要唱一首孙艳枝的《绿光》,送给在座的各位男辣鸡

《绿光》

爱就像一道绿光

洒在你头上

都怪嫂子太漂亮

。。。

不要怪爸爸无情

是你们太辣鸡

绿光呀~绿光

照亮你我他

男宠们:小蝌蚪你太过分了

小蝌蚪:各凭本事做人渣

男宠们:你到底想干什么

小蝌蚪:我想在你们坟头上蹦迪

男宠们心态爆炸,疯狂抓头

从98楼跳了下去

小蝌蚪赢得了比赛

成为了第一名男宠

。。。

酒店阳台,微风拂过

富婆:今晚谢谢你

小蝌蚪:为了钱,应该的

富婆:我喜欢你

小蝌蚪:我也喜欢我自己

富婆:你爱我吗

小蝌蚪:我不能对客户动心

富婆:我要如何才能得到你的心

小蝌蚪:杀了我,把心掏出来,嘻嘻

富婆:嘻嘻

。。。

黑帮大佬

屋内,捏脚

富婆:我要死了

小蝌蚪:怎么了

富婆:我老公在外面有女人了

富婆:他是个黑帮大佬,要杀我,跟小三在一起

小蝌蚪轻抚富婆的脸:傻瓜,在这之前,我会杀了他

小蝌蚪和富婆拥吻在了一起

两嘴之间拉出一条亮丝

吻毕,小蝌蚪消失在夜空中

。。。

刺杀大佬

深夜十二点

黑帮大佬酒店房间门外

塞入了一张小卡片

上面写着“包小姐”

和一张小蝌蚪穿泳裤的照片

。。。

黑帮大佬有着诡异的癖好

喜欢胸毛浓密的小奶狗

小蝌蚪满足了他对一切小奶狗的幻想

。。。

房间门果然打开了

小蝌蚪用一千根胸毛扎入了大佬体内

大佬:你。。。你是谁

小蝌蚪:我是你爸爸

大佬:为什么要杀我

小蝌蚪:因为我是你爸爸

大佬:能不能不提爸爸两个字

小蝌蚪:我和你老婆有一腿

黑帮大佬,猝,享年80

。。。

由于刺杀了黑帮大佬

当晚回去的路上

小蝌蚪被1000名杀手全城追杀

用尽了最后一根胸毛

小蝌蚪寡不敌众

倒在了血泊里,奄奄一息

将死之际,富婆从杀手中出现

她微笑的捅了小蝌蚪一刀

把心掏了出来

小蝌蚪惊恐的望着她

富婆:我终于得到了你的心,嘻嘻

(完)

作者:第一名的小蝌蚪

微信公众号:前端屌丝

github: https://github.com/airuikun/blog

《基于nodejs的云端热部署原理与实现》

背景

大家都知道,nodejs启的后端服务,如果有代码变动,要重启进程,代码才能生效。

nodejs的进程在重启的时候,爸爸们去访问服务,就会出现短暂的 502 bad gateway,爸爸们会不高兴

如果你的服务器加上了watch机制

当服务器上的代码频繁发生变动,或者短时间内发生高频变动,那就会一直 502 bad gateway

所谓“重启一时爽,一直重启一直爽”

近段时间在做云编译相关需求的时候,就出现了短时间内线上服务代码高频变动,代码功能模块高频更新,在不能重启服务的情况下,让更新的代码生效的场景。

这就涉及到一个热部署的概念,在不重启服务的情况下,让新部署的代码生效。

接下来我来给爸爸们讲解热部署的原理和实现方案

代码没法实时生效的原因

当我们通过require('xx/xx.js')去加载一个功能模块的时候,node会把require('xx/xx.js')得到的结果缓存在require.cache('xx/xx.js')

当我们多次调用require('xx/xx.js'),node就不再重新加载,而是直接从require.cache('xx/xx.js')读取缓存

所以当爸爸在服务器上修改xx/xx.js这个路径下的文件时,node只会去读取缓存,不会去加载爸爸的最新代码

源码地址和使用

为了实现这个热部署机制,在网上到处查资料,到处踩坑才弄好

以下代码是提炼出来、完整可运行的热部署基础原理代码,大家可以基于这个代码去自行拓展:smart-node-reload

注意最新版本12版本的node运行会报错,官方对require.cache做了调整,已经上报问题给官方,建议使用nodejs版本:v10.5.0

git clone下来以后,无需安装,直接运行

    npm start

这时候就开启了热部署变动监听

如何看到效果呢

爸爸请看/hots/hot.js文件

    const hot = 1
    module.exports = hot

将第一行代码改为const hot = 111

    const hot = 111
    module.exports = hot

这时候就能看到终端里监听到代码变动,然后动态加载你的最新代码并得到执行结果,输出为:

    热部署文件: hot.js ,执行结果: { 'hot.js': 111 }

热部署服务监听到代码变动,并重新加载了代码,爸爸们就可以实时拿到最新代码的执行结果了,整个过程都在线上环境运行,node进程也没有重启

源码解析

loadHandlers主函数

const handlerMap = {};// 缓存
const hotsPath = path.join(__dirname, "hots");

// 加载文件代码 并 监听指定文件夹目录文件内容变动
const loadHandlers = async () => {
  // 遍历出指定文件夹下的所有文件
  const files = await new Promise((resolve, reject) => {
    fs.readdir(hotsPath, (err, files) => {
      if (err) {
        reject(err);
      } else {
        resolve(files);
      }
    });
  });
  // 初始化加载所有文件 把每个文件结果缓存到handlerMap变量当中
  for (let f in files) {
    handlerMap[files[f]] = await loadHandler(path.join(hotsPath, files[f]));
  }

  // 监听指定文件夹的文件内容变动
  await watchHandlers();
};

loadHandlers是整个热部署服务的主函数,我们指定了服务器根目录下的hots文件夹是用来监听变动和热部署的文件夹

fs.readdir扫描hots文件夹下的所有文件,通过loadHandler方法去加载和运行每一个扫描到的文件,将结果缓存到handlerMap

然后用watchHandlers方法开启文件变动监听

watchHandlers监听文件变动

// 监视指定文件夹下的文件变动
const watchHandlers = async () => {
  // 这里建议用chokidar的npm包代替文件夹监听
  fs.watch(hotsPath, { recursive: true }, async (eventType, filename) => {
    // 获取到每个文件的绝对路径 
    // 包一层require.resolve的原因,拼接好路径以后,它会主动去帮你判断这个路径下的文件是否存在
    const targetFile = require.resolve(path.join(hotsPath, filename));
    // 当你适应require加载一个模块后,模块的数据就会缓存到require.cache中,下次再加载相同模块,就会直接走require.cache
    // 所以我们热加载部署,首要做的就是清除require.cache中对应文件的缓存
    const cacheModule = require.cache[targetFile];
    // 去除掉在require.cache缓存中parent对当前模块的引用,否则会引起内存泄露,具体解释可以看下面的文章
	//《记录一次由一行代码引发的“血案”》https://cnodejs.org/topic/5aaba2dc19b2e3db18959e63
	//《一行 delete require.cache 引发的内存泄漏血案》https://zhuanlan.zhihu.com/p/34702356
    if (cacheModule.parent) {    
        cacheModule.parent.children.splice(cacheModule.parent.children.indexOf(cacheModule), 1);
    }
    // 清除指定路径对应模块的require.cache缓存
    require.cache[targetFile] = null;

    // 重新加载发生变动后的模块文件,实现热加载部署效果,并将重新加载后的结果,更新到handlerMap变量当中
    const code = await loadHandler(targetFile)
    handlerMap[filename] = code;
    console.log("热部署文件:", filename, ",执行结果:", handlerMap);
  });
};

watchHandlers函数是用来监听指定文件夹下的文件变动、清理缓存更新缓存用的。

fs.watch原生函数监听hots文件夹下文件变动,当文件发生变动,就算出文件的绝对路径targetFile

require.cache[targetFile]就是requiretargetFile原文件的缓存,清除缓存用require.cache[targetFile] = null;

坑爹的地方来了,仅仅只是将缓存置为null,会发生内存泄露,我们还需要清除缓存父级的引用require.cache[targetFile].parent,就是下面这段代码

    if (cacheModule.parent) {    
        cacheModule.parent.children.splice(cacheModule.parent.children.indexOf(cacheModule), 1);
    }

loadHandler加载文件

// 加载指定文件的代码
const loadHandler = filename => {
  return new Promise((resolve, reject) => {
    fs.readFile(filename, (err, data) => {
      if (err) {
        resolve(null);
      } else {
        try {
          // 使用vm模块的Script方法来预编译发生变化后的文件代码,检查语法错误,提前发现是否存在语法错误等报错
          new vm.Script(data);
        } catch (e) {
          // 语法错误,编译失败
          reject(e);
          return;
        }
        // 编译通过后,重新require加载最新的代码
        resolve(require(filename));
      }
    });
  });
};

loadHandler函数的作用是加载指定文件,并校验新文件代码语法等。

通过fs.readFile读取文件内容

用node原生vm模块vm.Script方法去预编译发生变化后的文件代码,检查语法错误,提前发现是否存在语法错误等报错

检验通过后,通过resolve(require(filename))方法重新将文件require加载,并自动加入到require.cache缓存中

结尾:

以上就是热部署的所有内容了,代码地址是:smart-node-reload

这个代码是我经过极简后的代码,方便大家阅读和理解,感兴趣的小伙伴可以通过这个代码去进行深一步拓展

哎。。。。写完这篇文章,已经是凌晨两点了,写文章真不容易,麻烦各位爸爸star、follow、点赞和关注,辛苦爸爸了

作者:第一名的小蝌蚪

微信公众号:前端屌丝

github: https://github.com/airuikun/blog

@airuikun airuikun changed the title 小蝌蚪传记:基于nodejs的云端热部署原理与实现——富婆的爱 小蝌蚪传记:60行代码实现nodejs线上模块热部署——富婆的爱 Jan 14, 2020
@airuikun airuikun changed the title 小蝌蚪传记:60行代码实现nodejs线上模块热部署——富婆的爱 小蝌蚪传记:nodejs线上模块热部署原理与实现——富婆的爱 Jan 14, 2020
@pp512
Copy link

pp512 commented Jan 16, 2020

我只是来看小说的0.0

@SylviaPan
Copy link

来看热模块的我发现了别的东西 奇怪的知识增加了!

@yihan12
Copy link

yihan12 commented Aug 12, 2020

话太骚了。。。。

@qiutian00
Copy link

好闻,嘿嘿

@PLQin
Copy link

PLQin commented Aug 12, 2020

虽然大佬只发布九篇blog , 但看得出来大佬早已经积累了厚厚的文学素养 [狗头]

@ATai2
Copy link

ATai2 commented Feb 24, 2021

骚气

@obj-Rui
Copy link

obj-Rui commented Jul 28, 2021

学到了,嘻嘻

@qiutian00
Copy link

我要看你的小说

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants