热点推送NodeJS

我一直在试图找出Node.js上的“Hot Code Push”。 基本上,我的主文件(在键入node app.js时运行)包含一些设置,configuration和初始化。 在那个文件中,我有一个文件观察者,使用chokidar。 当我添加文件时,我只require该文件。 如果一个文件已被更改或更新,我会删除cachingdelete require.cache[path] ,然后重新要求它。 所有这些模块都不会导出任何内容,只能用于单个全局Storm对象。

 Storm.watch = function() { var chokidar, directories, self = this; chokidar = require('chokidar'); directories = ['server/', 'app/server', 'app/server/config', 'public']; clientPath = new RegExp(_.regexpEscape(path.join('app', 'client'))); watcher = chokidar.watch(directories, { ignored: function(_path) { if (_path.match(/\./)) { !_path.match(/\.(js|coffee|iced|styl)$/); } else { !_path.match(/(app|config|public)/); } }, persistent: true }); watcher.on('add', function(_path){ self.fileCreated(path.resolve(Storm.root, _path)); //Storm.logger.log(Storm.cliColor.green("File Added: ", _path)); //_console.info("File Updated"); console.log(Storm.css.compile(' {name}: {file}', "" + "name" + "{" + "color: white;" + "font-weight:bold;" + "}" + "hr {" + "background: grey" + "}")({name: "File Added", file: _path.replace(Storm.root, ""), hr: "=================================================="})); }); watcher.on('change', function(_path){ _path = path.resolve(Storm.root, _path); if (fs.existsSync(_path)) { if (_path.match(/\.styl$/)) { self.clientFileUpdated(_path); } else { self.fileUpdated(_path); } } else { self.fileDeleted(_path); } //Storm.logger.log(Storm.cliColor.green("File Changed: ", _path)); console.log(Storm.css.compile(' {name}: {file}', "" + "name" + "{" + "color: yellow;" + "font-weight:bold;" + "}" + "hr {" + "background: grey" + "}")({name: "File Changed", file: _path.replace(Storm.root, ""), hr: "=================================================="})); }); watcher.on('unlink', function(_path){ self.fileDeleted(path.resolve(Storm.root, _path)); //Storm.logger.log(Storm.cliColor.green("File Deleted: ", _path)); console.log(Storm.css.compile(' {name}: {file}', "" + "name" + "{" + "color: red;" + "font-weight:bold;" + "}" + "hr {" + "background: grey" + "}")({name: "File Deleted", file: _path.replace(Storm.root, ""), hr: "=================================================="})); }); watcher.on('error', function(error){ console.log(error); }); }; Storm.watch.prototype.fileCreated = function(_path) { if (_path.match('views')) { return; } try { require.resolve(_path); } catch (error) { require(_path); } }; Storm.watch.prototype.fileDeleted = function(_path) { delete require.cache[require.resolve(_path)]; }; Storm.watch.prototype.fileUpdated = function(_path) { var self = this; pattern = function(string) { return new RegExp(_.regexpEscape(string)); }; if (_path.match(pattern(path.join('app', 'templates')))) { Storm.View.cache = {}; } else if (_path.match(pattern(path.join('app', 'helpers')))) { self.reloadPath(path, function(){ self.reloadPaths(path.join(Storm.root, 'app', 'controllers')); }); } else if (_path.match(pattern(path.join('config', 'assets.coffee')))) { self.reloadPath(_path, function(error, config) { //Storm.config.assets = config || {}; }); } else if (_path.match(/app\/server\/(models|controllers)\/.+\.(?:coffee|js|iced)/)) { var isController, directory, klassName, klass; self.reloadPath(_path, function(error, config) { if (error) { throw new Error(error); } }); Storm.serverRefresh(); isController = RegExp.$1 == 'controllers'; directory = 'app/' + RegExp.$1; klassName = _path.split('/'); klassName = klassName[klassName.length - 1]; klassName = klassName.split('.'); klassName.pop(); klassName = klassName.join('.'); klassName = _.camelize(klassName); if (!klass) { require(_path); } else { console.log(_path); self.reloadPath(_path) } } else if (_path.match(/config\/routes\.(?:coffee|js|iced)/)) { self.reloadPath(_path); } else { this.reloadPath(_path); } }; Storm.watch.prototype.reloadPath = function(_path, cb) { _path = require.resolve(path.resolve(Storm.root, path.relative(Storm.root, _path))); delete require.cache[_path]; delete require.cache[path.resolve(path.join(Storm.root, "server", "application", "server.js"))]; //console.log(require.cache[path.resolve(path.join(Storm.root, "server", "application", "server.js"))]); require("./server.js"); Storm.App.use(Storm.router); process.nextTick(function(){ Storm.serverRefresh(); var result = require(_path); if (cb) { cb(null, result); } }); }; Storm.watch.prototype.reloadPaths = function(directory, cb) { }; 

一些代码是不完整的/不使用,因为我正在尝试很多不同的方法。

什么工作:

对于如下代码:

 function run() { console.log(123); } 

完美的作品。 但是任何asynchronous代码都无法更新。

问题=asynchronous代码

 app.get('/', function(req, res){ // code here.. }); 

如果我在nodejs进程运行的时候更新文件,没有任何反应,尽pipe它通过了文件监视器,caching被删除,然后重新build立。 另一个不起作用的例子是:

 // middleware.js function hello(req, res, next) { // code here... } // another file: app.use(hello); 

由于app.use仍然会使用该方法的旧版本。

题:

我怎么能解决这个问题? 有什么我失踪?

请不要抛出build议,像永远使用第三方模块。 我正在尝试在单个实例中包含function。

编辑:

在学习了meteor的代码库之后(在Node.js或者浏览器里“Hot Code Push”的资源令人吃惊),并且用我自己的实现来修补,我已经成功地做出了一个工作的解决scheme。 https://github.com/TheHydroImpulse/Refresh.js 。 这还处于发展的初期阶段,但现在看起来很稳固。 为了完成,我也将实现浏览器解决scheme。

删除require的caching实际上并不是“卸载”你的旧代码,也不会取消那些代码的function。

以下面的function为例:

 var callbacks=[]; registerCallback = function(cb) { callbacks.push(cb); }; 

现在让我们假设你有一个调用这个全局函数的模块。

 registerCallback(function() { console.log('foo'); }); 

你的应用程序启动后, callbacks将有一个项目。 现在我们将修改模块。

 registerCallback(function() { console.log('bar'); }); 

您的“热补丁”代码将运行,删除require.cache d版本并重新加载模块。

你必须认识到,现在callbacks两个项目。 首先,它具有对foo(它是在应用程序启动时添加的)函数的引用,以及对日志栏(刚刚添加)的函数的引用。

即使您将caching的引用删除到模块的exports也不能实际删除该模块。 就JavaScript运行时而言,只需从多个参数中删除一个参考。 应用程序的任何其他部分仍然可以挂在旧模块中的某个引用上。

这正是你的HTTP应用程序正在发生的事情。 当应用程序第一次启动时,您的模块将匿名callback附加到路由。 当你修改这些模块时,他们将新的callback附加到相同的路由; 旧的callback不会被删除。 我猜你正在使用Express,并按照添加的顺序调用路由处理程序。 因此,新的callback永远不会有机会运行。


说实话,我不会使用这种方法重新加载你的应用程序的修改。 大多数人在干净环境的假设下编写应用程序初始化代码; 你在一个肮脏的环境中运行初始化代码就违反了这个假设 – 也就是说,它已经启动并运行了。

试图清理环境,让你的初始化代码运行几乎肯定比它的价值更麻烦。 当您的底层文件发生变化时,我只需重新启动整个应用程序。

Meteor通过允许模块“注册”自己作为热代码推送过程的一部分来解决这个问题。

他们在他们的reload包中实现这个:

https://github.com/meteor/meteor/blob/master/packages/reload/reload.js#L105-L109

我已经看到在GitHub上的一些插件中使用Meteor.reload API,但是它们也在session包中使用它:

https://github.com/meteor/meteor/blob/master/packages/session/session.js#L103-L115

 if (Meteor._reload) { Meteor._reload.onMigrate('session', function () { return [true, {keys: Session.keys}]; }); (function () { var migrationData = Meteor._reload.migrationData('session'); if (migrationData && migrationData.keys) { Session.keys = migrationData.keys; } })(); } 

所以基本上,当页面/窗口加载的时候,meteor运行一个“迁移”,由包来定义数据/方法等等。 在进行热代码推送时会得到重新计算。

这也是由他们的livedata包使用 (searchreload )。

刷新之间,他们正在使用window.sessionStorage保存“状态”。