精华 需要添加一下 Node.js 模块热替换方面的机制
发布于 9 年前 作者 jiyinyiyong 15990 次浏览 最后一次编辑是 8 年前 来自 分享

目前不能投入太多时间, 简单说一下 前面看了 Webpack 在前端做热替换的方案, 而且在前端的效果也比较明显的 后端的话, 为了做到修改代码不会重启 WebSocket, 这个功能最近非常想要 Webpack 用的模块化方案基于 CommonJS, 大致的思路是构建模块的 tree 然后 tree 的节点可以冒泡, 跟 DOM 类似, 然后每个模块的 module.hot 上加入了一些代码 这个代码可以用来检测子模块的更新事件, 决定是否热替换代码 比如这样: http://segmentfault.com/a/1190000003879041

var requestHandler = require("./handler.js");
var server = require("http").createServer();
server.on("request", requestHandler);
server.listen(8080);

// check if HMR is enabled
if(module.hot) {
    // accept update of dependency
    module.hot.accept("./handler.js", function() {
        // replace request handler of server
        server.removeListener("request", requestHandler);
        requestHandler = require("./handler.js");
        server.on("request", requestHandler);
    });
}

在 Node 方面, 现在看的的一个是百度有一篇文章, 可以手动替换一下代码, 短期可以尝试一下: http://fex.baidu.com/blog/2015/05/nodejs-hot-swapping/ 另外找到个 hotswap 模块, 还不清楚具体机制是怎样的, 也打算尝试一下: https://github.com/rlidwka/node-hotswap 单纯从原理上讲, 在 Node 上实现一套热替换的难度并不会比 Webpack 大, 我就奇怪为什么没人做 也许对些 HTTP 无状态请求的同学来说整个服务器重启无所谓吧, node-dev nodemon supervisor 还够用 但是考虑一下, 修改代码, WebSocket 不受影响, 后端的代码直接更新了, 新的请求直接用新的代码 打算花点时间研究一下

28 回复

这个对于nodejs提供不中断服务很有用!

Webpack 作者回复我了, 直接用 Webpack 热替换后端, 晚上看下 https://github.com/webpack/docs/issues/45

内存泄漏!内存泄漏!内存泄漏!

对于无状态的http请求来说,确实没有太大价值。对于长连接的socket请求来说,做到连接和逻辑完全分离,也就变成无状态请求了,也变得没多大价值,反观热部署模块或者热部署服务,要么手动触发要么定时器逻辑开销比较大,所以github上才没有相应的包吧

@JacksonTian 文章里提的代码解决不掉类似的问题吗?

@haozxuan 在部署方面不了解, 大概确实效果不好吧. 我主要在意的是开发过程, 存在大量推送的消息, 而且内存当中也保存着数据状态, 这部分需求的比较明显. …听楼上这么说, 后端开发人员是铁了心要把状态存储放到数据库之类的外部服务区做了?

把 Webpack 的热替换方案的例子跑通了, 写了笔记: http://segmentfault.com/a/1190000003888845

如果要替换 setInterval 为 1000呢,在你的例子中

@Hi-Rube 这个… 属于状态了, 要在更新的回调里加上 clearIntervalsetInterval.

@jiyinyiyong 如果一个模块里建立了很多监听呢,不是很麻烦嘛替换的时候 (^O^)/

@Hi-Rube 是的… 状态这种东西, 是热替换不能控制的. 不能指望热替换代码能做到那么多. 成熟的例子, 比如 Erlang 平台的代码热替换, 因为 Erlang 的状态是控制住的, 所以才简单.

@jiyinyiyong 这样的话,这种方案能够用在哪些方面呢

@Hi-Rube 对我来说主要是 WebSocket 链接下边的一些处理具体逻辑的纯函数 这些逻辑代码不用重启服务器, 调试起来会更方便一些.

如果写模块的人在模块文件中定义了全局变量,就泄漏了。浏览器端刷新一下就解决,作为服务器端运行的node进程,这种方案只能在开发环境玩玩。别搞到生产环境。 自豪地采用 CNodeJS ionic

主要就是开发当中用的, 开发时候频繁修改文件, 热替换才这比较实用. 生产环境热替换是另一个话题了, 不看好 Node.

我个人认为如果仅仅是为了开发过程不用手动重启,直接fs.watch开发目录,发现文件更改,直接杀掉进程,让进程重启就行了,重启一切都是最新的,就没有这些头疼问题了,但这仅仅对于没有状态的网页应用,对游戏类的,那就要搞一套规范出来,而且大家都要严格遵守,反正很麻烦,不要羡慕erlang,人家erlang变量都没有,javascript这种随意的语言就算了。灵活是巨大的优点同时也是巨大的缺点。

只是为了连接不中断的话,建议使用socket.io

来自炫酷的 CNodeMD

@klesh socket.io 有热替换吗, 不是跑题了吧

@coordcn 所以关键时刻, 就要有个既是动态语言那么灵活, 也有 Erlang 的不可变数据的语言出现 在我看来已经出现了, 而且有多种可能性: http://clojure-china.org/ http://elixir-cn.com/

@jiyinyiyong 不能,只是会自动重连,可能理解错误,我以为你最主要的是想要客户端保持链接。 热替换牵扯复杂,稍有不慎就会内存泄露,我觉得两相其害取其轻者。尽量保持简单明了为好。

@klesh 关于热替换你明确是怎么回事吗? 我在 Issue 里跟 Webpack 作者提了这个事, 他说压根不担心, 不知道你们指的居室怎么回事?

-> 具体怎么回事?..

不会单单指的是变量的内存回收问题吧?

热替换的功能在开发环境下还是非常有必要的(尤其有 WebSocket 之类的功能时候)。ThinkJS 从 1.0 版本就已经实现,现在 2.0 已经非常完善了。下面说下具体的思路。 热替换分成 2 个部分: 1)监听文件修改 2)清除修改文件的缓存和对应的依赖文件

监听文件修改

监听文件修改最常见的方式是用 fs.watch 方法,但这个方法有很多问题,官网文档里也有说明。为此还有一个模块 chokidar 用来监听文件变化的,这个模块在 Gulp/Babel 等很多工具有使用,一般情况下都是没有问题的。但有时候也有问题,因为本质还是主要基于 fs.watch 来实现的。 如果监听的文件不是很多的话,最保险的办法是通过定时器来实现,获取目录下所有文件的修改时间,跟上一个状态做对比,如果不一样的话,表示文件有修改,然后对比出修改的文件。这里要注意 1 个地方,如果使用 Babel 之类的编译器,如果源文件有删除的话,需要把编译后的文件删掉。

清除文件缓存 Node.js 下所有模块的缓存都放在 Module._cache 下 ,模块下可以通过 require.cache 获取。如果这个文件没有被其他文件依赖的话,只要删除掉 require.cache 下对应的数据就可以了。如果是配置之类的文件,并且是在服务启动之前就加载,需要重新加载下。

如果这个模块被其他模块依赖的话,处理就复杂一些,不光要清除这个模块的缓存,还要清除依赖这个模块的模块。

Node.js 在模块依赖的处理有 bug,require.cache 里每个模块虽然都有 children 字段,表示依赖的子模块,但如果 A 模块被 B 模块依赖的话, B 模块的 children 含有 A 模块,这时候如果 C 模块也依赖 A 模块的话,C 模块的 children 里并不包含 A 模块。下面是 Node.js(V4.2.2) 里 module.js 里的实现:

Module._load = function(request, parent, isMain) {
  ...
  var filename = Module._resolveFilename(request, parent);

  var cachedModule = Module._cache[filename];
  //如果这个模块已经有缓存,直接返回
  if (cachedModule) {
    return cachedModule.exports;
  }
  ...
};

上面的代码去除了一些不相关的代码,可以看到如果一个模块已经加载的话,那么直接返回。这也导致了为啥 C 模块的 children 属性里没有包含 A 模块。 改进的话也很简单,目前我已经给官方发了 pull request, 具体见:https://github.com/nodejs/node/pull/3933/files

有了完整的模块依赖关系后,清除缓存就比较简单了,ThinkJS 里实现如下:

  clearFileCache(file){
    let mod = require.cache[file];
    if(!mod){
      return;
    }
    //remove children
    if(mod && mod.children){
      mod.children.length = 0;
    }

    // clear module cache which dependents this module
    for(let fileItem in require.cache){
      if(fileItem === file || fileItem.indexOf(NODE_MODULES) > -1){
        continue;
      }
      let item = require.cache[fileItem];
      if(item && item.children && item.children.indexOf(mod) > -1){
        this.clearFileCache(fileItem);
      }
    }
    //remove require cache
    delete require.cache[file];
  }

ThinkJS 里本身对这些处理更加完善下,可以直接使用 ES6/7 特性开发,然后自动使用 Babel 编译,自动清除文件的缓存。开发过程中完全不用借助第三方模块清除缓存。

@welefen 监听文件我用过是 gaze, 是 C++ 写的大概比 chakidar 性能好一点.

@haozxuan 往往说的无状态还要做到逻辑和数据分离,不能替换逻辑的时候把当前的数据替换掉,做到逻辑数据分离分离到什么粒度还是有难度的

研究不多,只提一个可能被大家忽略的方式

node-inspector可以直接修改正在执行中的代码,包括已经被外部引用的函数(例如已注册的回调函数) 曾经跟进去看过几眼,貌似v8 debug context里面有个LiveEdit工具

V8 的热替换在前端用过几次, 在 Workspace 当中加载文件, 然后修改代码. 用 LESS 当中多一些.

@fengmk2 1.不会泄露,失去引用v8自动回收.主要是块级作用域的变量无法更新

回到顶部