资讯专栏INFORMATION COLUMN

Node.js 热更新(一)

LancerComet / 2268人阅读

摘要:直到最近在使用微信机器人时,遇到了强烈的需求。增删文件后热更新上面的代码已经不小心实现了增加文件后热更新,因为表示检测的更新,如果增加一个,那么就变成,于是新模块不等于老模块不存在,从而使用注册事件监听器。

背景

刚思考这个话题的时候,首先想到的是 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 来实现的话,需要完成几件事情:

把事件处理器的代码模块化,便于 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 对象()
            // 如果模块内容不一样,则表示要作热更新处理了
            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 进阶

上面一种思路存在一些问题

业务代码的格式限制太死,不够灵活

在生产阶段也耦合了 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 玩了一轮,看来择日还得再写一篇(二)了

问题

热更新的主要目的是为了提高开发效率,并不是为了在生产上玩热更新,毕竟还有很多潜在问题,例如,模块中涉及全局状态或者单例资源,通过热更新可能会引起混乱......

参考

Webpack 做 Node.js 代码热替换, 第一步 - 题叶 - SegmentFault

Backend Apps with Webpack (Part I)

Backend Apps with Webpack: Driving with Gulp (Part II)

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/82416.html

相关文章

  • Node.js 前后端分离开发新思路

    摘要:从事开发的程序员,对于前后端分离模式多半不陌生,这也是目前主流的开发模式,具体关于前后端分离的模式可以参看文章你不得不了解的前后端分离原理,在这里写者不进行说明。原理图如下,前后端在一个进程同一个端口中,通过热替换更新的,而不是全量重启。 从事 Web 开发的程序员,对于前后端分离模式多半不陌生,这也是目前主流的 Web 开发模式,具体关于前后端分离的模式可以参看文章《你不得不了解的前...

    Lionad-Morotar 评论0 收藏0
  • 如何打造个令人愉悦的前端开发环境(四)

    摘要:在前后端分离的前端项目开发中经常用到。是的一个中间件。即是一个重要的功能。配置先来在配置文件中引入添加一个和通信的客户端添加应用入口文件在插件中引入在我们的开发环境中是这样配置的。 原文链接此文是我同事写的,搭建Express结合Webpack。以下是正文,后面我会附上我的解读 Express 结合 Webpack 实现HMR 本篇文件主要讲结合 Webpack 和 Express 实...

    StonePanda 评论0 收藏0
  • 如何打造个令人愉悦的前端开发环境(四)

    摘要:在前后端分离的前端项目开发中经常用到。是的一个中间件。即是一个重要的功能。配置先来在配置文件中引入添加一个和通信的客户端添加应用入口文件在插件中引入在我们的开发环境中是这样配置的。 原文链接此文是我同事写的,搭建Express结合Webpack。以下是正文,后面我会附上我的解读 Express 结合 Webpack 实现HMR 本篇文件主要讲结合 Webpack 和 Express 实...

    animabear 评论0 收藏0
  • webpack优化

    摘要:使用要给项目构建接入动态链接库的思想,需要完成以下事情把网页依赖的基础模块抽离出来,打包到一个个单独的动态链接库中去。接入已经内置了对动态链接库的支持,需要通过个内置的插件接入,它们分别是插件用于打包出一个个单独的动态链接库文件。 webpack优化 查看所有文档页面:全栈开发,获取更多信息。原文链接:webpack优化,原文广告模态框遮挡,阅读体验不好,所以整理成本文,方便查找。 ...

    ChanceWong 评论0 收藏0
  • 9102年:手写个React脚手架 【优化极致版】

    摘要:马上要出了,完全手写一个优化后的脚手架是不可或缺的技能。每个依赖项随即被处理,最后输出到称之为的文件中,我们将在下一章节详细讨论这个过程。的事件流机制保证了插件的有序性,使得整个系统扩展性很好。 webpack马上要出5了,完全手写一个优化后的脚手架是不可或缺的技能。 本文书写时间 2019年5月9日 , webpack版本 4.30.0最新版本 本人所有代码均手写,亲自试验过可...

    Kylin_Mountain 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<