背景
刚思索这个话题的时刻,起首想到的是 Vue 或 React 的组件热更新(基于 Webpack HMR),厥后又想到了 Lua、Erlang 等言语的热更新,不过在现实开辟 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 来完成的话,须要完成几件事变:
把事宜处置惩罚器的代码模块化,便于 webpack 治理。
自动加载一切处置惩罚器模块
某个事宜处置惩罚模块更新后须要拿到老的模块,用来移除老的监听处置惩罚器。
要晓得文件的增添和删除,而且拿到模块内容。
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 来玩是能够的,固然我们作了不少严厉商定,不过不影响这一阶段的思绪。
思绪二:基于 Webpack 进阶
上面一种思绪存在一些题目
营业代码的花样限定太死,不够天真
在临盆阶段也耦合了 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 玩了一轮,看来择日还得再写一篇(二)了
题目
热更新的重要目标是为了进步开辟效力,并不是为了在临盆上玩热更新,毕竟另有许多潜伏题目,比方,模块中触及全局状况或许单例资本,经由过程热更新能够会引起杂沓……