node.js如何避免重启程序实例-新闻详情

node.js如何避免重启程序实例


发布时间:2017-04-01责任编辑:朱明 浏览:1720


Node.js编辑完代码后必须重启真是件麻烦事。在不重启情况下热更新Node.js代码,是本文重要讨论的话题。而解决该问题,JavaScript的引用成为了关键。

 

最开始把目光瞄向了require这个模块。想法很简单,因为Node.js中引入一个模块都是通过require这个方法加载的。于是就开始思考require能不能在更新代码之后再次require一下。尝试如下:

a.js

var express = require('express');var b = require('./b.js');
var app = express();
 
app.get('/', function (req, res) {
  b = require('./b.js');
  res.send(b.num);
});
 
app.listen(3000);


b.js

exports.num = 1024;


两个JS文件写好之后,从a.js启动,刷新页面会输出b.js中的1024,然后修改b.js文件中导出的值,例如修改为2048。再次刷新页面依旧是原本的1024。

再次执行一次require并没有刷新代码。require在执行的过程中加载完代码之后会把模块导出的数据放在require.cache中。require.cache是一个{}对象,以模块的绝对路径为key,该模块的详细数据为value。于是便开始做如下尝试:

a.js

var path = require('path');var express = require('express');var b = require('./b.js');
var app = express();
 
app.get('/', function (req, res) {
  if (true) { // 检查文件是否修改
    flush();
  }
  res.send(b.num);
});
function flush() {
  delete require.cache[path.join(__dirname, './b.js')];
  b = require('./b.js');
}
 
app.listen(3000);


再次require之前,将require之上关于该模块的cache清理掉后,用之前的方法再次测试。结果发现,可以成功的刷新b.js的代码,输出新修改的值。

了解到这个点后,就想通过该原理实现一个无重启热更新版本的node-supervisor。在封装模块的过程中,出于情怀的原因,考虑提供一个类似PHP中include的函数来代替require去引入一个模块。实际内部依旧是使用require去加载。以b.js为例,原本的写法改为var b = include(‘./b’),在文件b.js更新之后include内部可以自动刷新,让外面拿到最新的代码。

但是实际的开发过程中,这样很快就碰到了问题。我们希望的代码可能是这样:

web.js

var include = require('./include');var express = require('express');var b = include('./b.js');var app = express();
 
app.get('/', function (req, res) {
  res.send(b.num);
});
 
app.listen(3000);


但按照这个目标封装include的时候,我发现了问题。无论我们在include.js内部中如何实现,都不能像开始那样拿到新的b.num。

对比开始的代码,发现问题出在少了b = xx。也就是说这样写才可以:

web.js

var include = require('./include');var express = require('express');var app = express();
 
app.get('/', function (req, res) {
  var b = include('./b.js');
  res.send(b.num);
});
 
app.listen(3000);


修改成这样,就可以保证每次能可以正确的刷新到最新的代码,并且不用重启实例了。大家有兴趣的可以研究这个include是怎么实现的,本文就不深入讨论了,因为这个技巧使用度不高,写起起来不是很优雅[1],反而这其中有一个更重要的问题————JavaScript的引用。

JavaScript的引用与传统引用的区别

要讨论这个问题,我们首先要了解JavaScript的引用于其他语言中的一个区别,在C++中引用可以直接修改外部的值:

#include <iostream>
using namespace std;
void test(int &p) // 引用传递
{
    p = 2048;
}
int main()
{
    int a = 1024;
    int &p = a; // 设置引用p指向a
 
    test(p); // 调用函数
 
    cout << "p: " << p << endl; // 2048
    cout << "a: " << a << endl; // 2048
    return 0;
}


而在JavaScript中:

var obj = { name: 'Alan' };
function test1(obj) {
  obj = { hello: 'world' }; // 试图修改外部obj
}
 
test1(obj);
console.log(obj); // { name: 'Alan' } // 并没有修改①
function test2(obj) {
  obj.name = 'world'; // 根据该对象修改其上的属性
}
 
test2(obj);
console.log(obj); // { name: 'world' } // 修改成功②


我发现与C++不同,根据上面代码①可知JavaScript中并没有传递一个引用,而是拷贝了一个新的变量,即值传递。根据②可知拷贝的这个变量是一个可以访问到对象属性的“引用”(与传统的C++的引用不同,下文中提到的JavaScript的引用都是这种特别的引用)。这里需要总结一个绕口的结论:Javascript中均是值传递,对象在传递的过程中是拷贝了一份新的引用。

为了理解这个比较拗口的结论,让我们来看一段代码:

var obj = {
  data: {}
};
 
// data 指向 obj.datavar data = obj.data;
console.log(data === obj.data); // true-->data所操作的就是obj.data
data.name = 'Alan';data.test = function () {
  console.log('hi')
};
 
// 通过data可以直接修改到data的值console.log(obj) // { data: { name: 'Alan', test: [Function] } }
data = {
  name: 'Bob',
  add: function (a, b) {
    return a + b;
  }
};
 
// data是一个引用,直接赋值给它,只是让这个变量等于另外一个引用,并不会修改到obj本身console.log(data); // { name: 'Bob', add: [Function] }console.log(obj); // { data: { name: 'Alan', test: [Function] } }
obj.data = {
  name: 'Bob',
  add: function (a, b) {
    return a + b;
  }
};
 
// 而通过obj.data才能真正修改到data本身console.log(obj); // { data: { name: 'Bob', add: [Function] } }


通过这个例子我们可以看到,data虽然像一个引用一样指向了obj.data,并且通过data可以访问到obj.data上的属性。但是由于JavaScript值传递的特性直接修改data = xxx并不会使得obj.data = xxx。

打个比方最初设置var data = obj.data的时候,内存中的情况大概是:

格子

内容

obj.data

内存1

data

内存1

所以通过data.xx可以修改obj.data的内存1。

然后设置data = xxx,由于data是拷贝的一个新的值,只是这个值是一个引用(指向内存1)罢了。让它等于另外一个对象就好比:

格子

内容

obj.data

内存1

data

内存2

让data指向了新的一块内存2。

如果是传统的引用(如上文中提到的C++的引用),那么obj.data本身会变成新的内存2,但JavaScript中均是值传递,对象在传递的过程中拷贝了一份新的引用。所以这个新拷贝的变量被改变并不影响原本的对象。

 


春秋工作室  供稿