Node.js 热更新(一)

背景

刚思索这个话题的时刻,起首想到的是 VueReact 的组件热更新(基于 Webpack HMR),厥后又想到了 LuaErlang 等言语的热更新,不过在现实开辟 Node.js 背景时,运用 remy/nodemon 之类的热重启(侦测代码修正重启顺序)东西也够用,因而 Node.js 的热更新(替代模块,不必重启)的考证就一向放置。

直到最近在运用「微信机器人」)(Node.js) 时,遇到了猛烈的需求。这类机器人顺序就是:启动了一个网页,登录 Web 微信,经由过程抓取辨认页面中的元素取得一些状况信息,如:音讯、挚友请求等等,由于它的启动时候也比较长,假如每次修正营业代码后都要重启,那末守候顺序启动就要斲丧不少时候,致使开辟体验很差,因而实践 Node.js 的热更新就燃眉之急了。

目标

以下是机器人的中心用法:

robot = new Robot()
robot.addEventListener('msg', ...)
robot.removeEventListener('msg', ...)

那末我们的目标:增/删/改 营业逻辑(事宜处置惩罚器)的时刻顺序不必重启,自动热更新营业逻辑代码,从而进步开辟效力。

思绪一:基于 Webpack 考证可行

从 Webpack Wiki hot module replacement · webpack/docs Wiki 相识到,Webpack 能晓得「哪一个模块须要热更新」,并供应一些钩子,别的 webpack 自有一套模块治理,能够治理替代模块,让你接见的是热更新以后的模块。别的,要完成热加载的不仅要满足「再次加载」,还要斟酌怎样清空相干的「耐久资本」。

所以说,假如基于 webpack HMR 来完成的话,须要完成几件事变:

  1. 把事宜处置惩罚器的代码模块化,便于 webpack 治理。

  2. 自动加载一切处置惩罚器模块

  3. 某个事宜处置惩罚模块更新后须要拿到老的模块,用来移除老的监听处置惩罚器。

  4. 要晓得文件的增添和删除,而且拿到模块内容。

1. 营业代码模块化

简朴地把每一个事宜处置惩罚器定义为一个文件 *.biz.js

// msg.biz.js
module.exports = {
    evt: 'msg',
    fn() {
        console.log('msg hanlder....')
    }
};

个中 evt 是事宜名, fn 是处置惩罚器,因而加载一个营业模块后就能够拿到事宜称号和处置惩罚器。
(能够不满足现实请求,先简朴考证热更新是不是可行哈!)

2. 自动加载

我们商定,营业模块 *.biz.js 都放在 /biz 目次下,该目次下的 index.js 会加载一切营业模块,而 main.js 就只需加载 /biz/index.js

src
 |--- /biz
       |--- a.biz.js
       |--- b.biz.js
       |--- index.js
          
 |--- main.js
    

借助 webpack 的 require-context 加载一切 *.biz.js 模块,防止手写 require:

// index.js
// 加载当前目次下一切 `*.biz.js`
const requireContext = require.context('./', true, /\.biz.js/);

// 此时 requireContext.keys() 为 ['./a.biz.js', './b.biz.js']
requireContext.keys().forEach(key => {

    const module = requireContext(key);
    // 相当于 module = require('./biz/a.biz.js')
    
    // 因而拿到事宜名和处置惩罚器,然后举行事宜监听
    // robot.addEventListener(module.evt, module.fn)
    
});

3. 修正后热更新

参考 Wiki 的例子 Example 3,晓得 require.context 怎样运用热更新机制

// index.js
// 启动 webpack HRM 时则 module.hot 为 true
if (module.hot) {
    // 示意该 context 下的模块都要检测更新
    module.hot.accept(requireContext.id, () => {

        const requireContext = require.context('./', true, /\.biz.js/);
        requireContext.keys().forEach(key => {
        
            const newModule = requireContext(key);

            // 前面初次自动加载一切模块后,纪录到 oldModules 对象(<key,module>)
            // 假如模块内容不一样,则示意要作热更新处置惩罚了
            if (oldModules[key] !== newModule) {
                   // ... 对老模块 oldModules[key] 移除事宜监听
                   // ... 对新模块 newModule 注册事宜监听
                    
                    // 同时更新缓存纪录
                oldModules[key] = newModule;
            }
        });
    });
}

到了这一步,修正任何 *.biz.js 的代码都能自动热更新了。

4. 增删文件后热更新

上面的代码已不小心完成了 「增添文件后热更新」,由于 module.hot.accept(requireContext.id 示意检测 ./biz/*.biz.js 的更新,假如增添一个 c.biz.js,那末 requireContext.keys() 就变成 [ ..., './c.biz.js'],因而新模块不等于老模块(不存在),从而运用 c.biz.js 注册事宜监听器。

关于删除文件后的热更新,则在上面代码基础上增添:

    if (module.hot) {
        module.hot.accept(requireContext.id, () => {
            
            // 在从新加载目次下的一切模块前,对老纪录作个副本
            const oldKeysRetain = {};
            Object.keys(oldModules)
                .forEach(k => (oldKeysRetain[k] = true));

            const requireContext = require.context('./', true, /\.biz.js/);
            requireContext.keys().forEach(key => {
            
                  // 假如某模块存在当前目次,则从暂时纪录中抹去
                delete oldKeysRetain[key];
                const newModule = requireContext(key);
                if (oldModules[key] !== newModule) {
                   ...
                }
            });

            // 未抹去的部份,意味着不存在当前目次下了,也就是被删除了
            Object.keys(oldKeysRetain).forEach(key => {
                // ... 对老模块移除事宜监听
                delete oldModules[key];
            });
        });
    }

经由以上四步,算是开端考证了,借助 Webpack 来玩是能够的,固然我们作了不少严厉商定,不过不影响这一阶段的思绪。

完全代码请移步:zhenyong/webpack-hot-nodejs-demo: Webpack HMR demo use in Node.js, showing how to auto add/remove listeners.

思绪二:基于 Webpack 进阶

上面一种思绪存在一些题目

  1. 营业代码的花样限定太死,不够天真

  2. 在临盆阶段也耦合了 webpack

因而我想,商定营业代码花样是为了轻易经由过程模块治理事宜的注册和移除,假如说在不侵入代码,不作任何商定的情况下,也能晓得某个模块注册了哪些事宜,是不是是就不需商定了,彷佛是的:

//## a.biz.js 不商定营业代码花样
robot.addLisenter('msg', ...)


//## 进口.js
robot = new Robot();

_add = robot.addLisenter
robot.addLisenter = () => {
    // 阻拦注册事宜要领
    // 从而纪录下 a.biz 模块都注册了哪些事宜处置惩罚器
}
require('a.biz')
robot.addLisenter = _add

然则题目来了,我们的目标包含「自动加载一切营业模块,增删文件都能热更新」,那末在开辟阶段我们照样借助 webpack 的 require.context 要领,而且商定每一个营业模块的进口文件命名为 *.biz.js,至于内里代码怎样写就随便了,而在临盆阶段能够遍历文件找到一切 *.biz.js 举行加载,不必依靠 webpack。

剩下的大部份思绪跟 #思绪一 相似,代码可参考 zhenyong/webpack-hot-nodejs-demo: Webpack HMR demo use in Node.js, showing how to auto add/remove listeners.

更多思绪

最最先写这篇文章是想深扒一下 Node.js 的模块治理和缓存构造,然后考证一下经由过程消灭模块缓存来做热更新是不是可行,厥后觉得 webpack 给我们作了许多事情,因而就先用 webpack 玩了一轮,看来择日还得再写一篇(二)了

题目

热更新的重要目标是为了进步开辟效力,并不是为了在临盆上玩热更新,毕竟另有许多潜伏题目,比方,模块中触及全局状况或许单例资本,经由过程热更新能够会引起杂沓……

参考

    原文作者:zhenyong
    原文地址: https://segmentfault.com/a/1190000009023924
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞