NodeJS-秘籍-五-

66 阅读31分钟

NodeJS 秘籍(五)

原文:zh.annas-archive.org/md5/B8CF3F6C144C7F09982676822001945F

译者:飞龙

协议:CC BY-NC-SA 4.0

第九章:编写自己的 Node 模块

在本章中,我们将涵盖:

  • 创建一个测试驱动的模块 API

  • 编写一个功能模块的模拟

  • 从功能到原型的重构

  • 扩展模块的 API

  • 将模块部署到 npm

介绍

自从诞生以来,一个蓬勃发展的模块生态系统一直是 Node 的核心目标之一。该框架倾向于模块化。即使是核心功能(如 HTTP)也是通过模块系统提供的。

创建我们自己的模块几乎和使用核心和第三方模块一样容易。我们只需要了解模块系统的工作原理和一些最佳实践。

一个优秀的模块是执行特定功能的高标准,而优秀的代码是多个开发周期的结果。在本章中,我们将从头开始开发一个模块,从定义其应用程序编程接口(API)开始,逐步创建我们的模块。最后,我们将把它部署到 npm 以造福所有人。

创建一个测试驱动的模块 API

我们将通过松散地遵循测试驱动开发(TDD)模型(参见en.wikipedia.org/wiki/Test-driven_development了解更多信息)来创建我们的模块。JavaScript 是异步的,因此代码可以在多个时间流中执行。这有时可能会构成一个具有挑战性的心智难题。

测试套件在 JavaScript 开发中是一个特别强大的工具。当测试通过时,它提供了一个质量保证过程,并且能够激发模块用户的信心。

此外,我们可以预先定义我们的测试,作为一种在开始开发之前规划预期 API 的方式。

在这个示例中,我们将通过创建一个模块的测试套件来提取 MP3 文件的统计信息。

准备工作

让我们创建一个名为mp3dat的新文件夹,里面有一个名为index.js的文件。然后再创建两个子文件夹:libtest,它们都包含index.js

我们还需要 MP3 文件进行测试。为简单起见,我们的模块只支持关闭错误保护的 MPEG-1 Layer 3 文件。其他类型的 MP3 文件包括 MPEG-2 和 MPEG-2.5。MPEG-1(无错误保护)可能是最常见的类型,但我们的模块可以很容易地进行扩展。我们可以从www.paul.sladen.org/pronunciation/torvalds-says-linux.mp3获取一个 MPEG-1 Layer 3 文件。让我们把这个文件放在我们的新mp3dat/test文件夹中,并将其命名为test.mp3

本章的重点是创建一个完全功能的模块,不需要对 MP3 文件结构有先验知识。在本章中,关于 MP3 的细节可以安全地略过,而有关模块创建的信息则至关重要。然而,我们可以从en.wikipedia.org/wiki/MP3了解更多关于 MP3 文件及其结构的信息。

如何做...

让我们打开test/index.js并设置一些变量,如下所示:

var assert = require('assert');
var mp3dat = require('../index.js');
var testFile = 'test/test.mp3';

assert是一个专门用于构建测试套件的核心 Node 模块。总体思路是我们断言某件事应该是真的(或假的),如果断言是正确的,测试就通过了。mp3dat变量需要我们的主要(当前为空白的)index.js文件,该文件将加载lib/index.js文件,其中包含实际的模块代码。

testFile变量从我们模块的根目录(mp3dat文件夹)的角度指向我们的test.mp3文件。这是因为我们从模块目录的根目录运行测试。

现在我们将决定我们的 API 并编写相应的测试。让我们模仿fs.stat方法来设计我们的模块。我们将使用mp3dat.stat方法获取有关 MP3 文件的数据,该方法将接受两个参数:文件路径和一次收集统计信息后要调用的回调函数。

mp3dat.stat回调将接受两个参数。第一个将是错误对象,如果没有错误,应将其设置为null,第二个将包含我们的stats对象。

stats对象将包含duration, bitrate, filesize, timestamptimesig属性。duration属性将包含一个对象,其中包含hours, minutes, secondsmilliseconds键。

例如,我们的test.mp3文件应该返回类似以下的内容:

{ duration: { hours: 0, minutes: 0, seconds: 5, milliseconds: 186 },
  bitrate: 128000,
  filesize: 82969,
  timestamp: 5186,
  timesig: '00:00:05' }

现在我们已经构想出我们的 API,我们可以将其映射到断言测试中,作为强制执行 API 在整个模块开发过程中的手段。

让我们从mp3datmp3dat.stat开始。

assert(mp3dat, 'mp3dat failed to load');

assert(mp3dat.stat, 'there should be a stat method');

assert(mp3dat.stat instanceof Function, 'stat should be a Function');

要测试mp3dat.stat函数,我们实际上必须调用它,然后在其回调函数中执行进一步的测试。

mp3dat.stat(testFile, function (err, stats) {

  assert.ifError(err);

  //expected properties
  assert(stats.duration, 'should be a truthy duration property');
  assert(stats.bitrate, 'should be a truthy bitrate property');
  assert(stats.filesize, 'should be a truthy filesize property');
  assert(stats.timestamp, 'should be a truthy timestamp property');
  assert(stats.timesig, 'should be a truthy timesig property');

现在我们已经确定了预期的stats属性,我们可以进一步指定这些属性应该是什么样子的,在回调函数中,我们写下以下代码:

  //expected types
  assert.equal(typeof stats.duration, 'object', 'duration should be an object type');
  assert(stats.duration instanceof Object, 'durations should be an instance of Object');
  assert(!isNaN(stats.bitrate), 'bitrate should be a number');
  assert(!isNaN(stats.filesize), 'filesize should be a number');
  assert(!isNaN(stats.timestamp), 'timestamp should be a number');

  assert(stats.timesig.match(/^\d+:\d+:\d+$/), 'timesig should be in HH:MM:SS format');

  //expected duration properties
  assert.notStrictEqual(stats.duration.hours, undefined,  'should be a duration.hours property');
  assert.notStrictEqual(stats.duration.minutes, undefined, 'should be a duration.minutes property');
  assert.notStrictEqual(stats.duration.seconds, undefined, 'should be a duration.seconds property');
  assert.notStrictEqual(stats.duration.milliseconds, undefined, 'should be a duration.milliseconds property');

  //expected duration types
  assert(!isNaN(stats.duration.hours), 'duration.hours should be a number');
  assert(!isNaN(stats.duration.minutes), 'duration.minutes should be a number');
  assert(!isNaN(stats.duration.seconds), 'duration.seconds should be a number');
  assert(!isNaN(stats.duration.milliseconds), 'duration.milliseconds should be a number');

  //expected duration properties constraints
  assert(stats.duration.minutes < 60, 'duration.minutes should be no greater than 59');
  assert(stats.duration.seconds < 60, 'duration.seconds should be no greater than 59');
  assert(stats.duration.milliseconds < 1000, 'duration.seconds should be no greater than 999');

  console.log('All tests passed');  //if we've gotten this far we are done.
});

现在让我们运行我们的测试。从mp3dat文件夹中,我们说:

node test 

这应该返回包含以下内容的文本:

AssertionError: there should be a stat method

这完全正确,我们还没有编写stat方法。

它是如何工作的...

当运行测试时,assert模块将抛出AssertionError,以让开发人员知道他们的代码目前与他们对所需 API 的预定义断言不一致。

在我们的单元测试文件(test/index.js)中,我们主要使用简单的assert函数(assert.ok的别名)。assert要求传递给它的第一个参数为真值。否则,它会抛出AssertionError,其中第二个提供的参数用于错误消息(assert.ok的相反是assert.fail,它期望一个假值)。

我们的测试失败了:

assert(mp3dat.stat, 'there should be a stat method');

这是因为mp3dat.statundefined(一个假值)。

assert的第一个参数可以是一个表达式。例如,我们使用stats.duration.minutes < 60来为duration.minutes属性设置约束,并在timesig上使用match方法来验证正确的时间模式 HH:MM:SS。

我们还使用assert.equalassert.notStrictEqualassert.equal是一个应用类型强制相等的测试(例如,等同于==),assert.strictEqual要求值和类型匹配,assert.notEqualassert.notStrictEqual是相应的对立面。

我们使用assert.notStrictEqual来确保duration对象的子属性(hours, minutes等)的存在。

还有更多...

有许多测试框架提供额外的描述性语法、增强功能、异步测试能力等。让我们尝试一个。

使用 should.js 进行单元测试

第三方的should模块很好地放在核心assert模块之上,为我们的测试添加了一些语法糖,以简化和增强描述能力。让我们安装它。

npm install should 

现在我们可以使用should来重写我们的测试,如下面的代码所示:

var should = require('should');
var mp3dat = require('../index.js');
var testFile = 'test/test.mp3';

should.exist(mp3dat);
mp3dat.should.have.property('stat');
mp3dat.stat.should.be.an.instanceof(Function);

mp3dat.stat(testFile, function (err, stats) {
  should.ifError(err);

  //expected properties
  stats.should.have.property('duration');
  stats.should.have.property('bitrate');
  stats.should.have.property('filesize');    
  stats.should.have.property('timestamp');
  stats.should.have.property('timesig');  

  //expected types
  stats.duration.should.be.an.instanceof(Object);
  stats.bitrate.should.be.a('number');
  stats.filesize.should.be.a('number');
  stats.timestamp.should.be.a('number');  

  stats.timesig.should.match(/^\d+:\d+:\d+$/);

  //expected duration properties
  stats.duration.should.have.keys('hours', 'minutes', 'seconds', 'milliseconds');

  //expected duration types and constraints
  stats.duration.hours.should.be.a('number');
  stats.duration.minutes.should.be.a('number').and.be.below(60);
  stats.duration.seconds.should.be.a('number').and.be.below(60);
  stats.duration.milliseconds.should.be.a('number').and.be.below(1000);  

  console.log('All tests passed');

});

should允许我们编写更简洁和描述性的测试。它的语法是自然和不言自明的。我们可以在其 Github 页面上阅读各种should方法:www.github.com/visionmedia/should.js.

另请参阅

  • 在本章讨论的编写功能模块原型

  • 在本章讨论的模块 API 扩展

  • 在本章讨论的将模块部署到 npm

编写一个功能模块原型

现在我们已经编写了我们的测试(参见前面的配方),我们准备创建我们的模块(顺便说一句,从现在开始,我们将使用我们的单元测试的should版本,而不是assert)。

在这个配方中,我们将以简单的功能风格编写我们的模块,以证明概念。在下一个配方中,我们将把我们的代码重构成一个更常见的以可重用性和可扩展性为中心的模块化格式。

准备工作

让我们打开我们的主要index.js文件,并通过module.exports将其链接到lib目录。

module.exports = require('./lib');

这使我们可以将模块代码的核心整齐地放在lib目录中。

如何做...

我们将打开lib/index.js,并开始引入fs模块,用于读取 MP3 文件,并设置一个bitrates映射,将十六进制表示的值与 MPEG-1 规范定义的比特率值进行交叉引用。

var fs = require('fs');

//half-byte (4bit) hex values to interpreted bitrates (bps)
//only MPEG-1 bitrates supported
var bitrates = { 1 : 32000, 2 : 40000, 3 : 48000, 4 : 56000, 5 : 64000,
  6 : 80000, 7 : 96000, 8 : 112000, 9 : 128000, A : 160000, B : 192000,
  C : 224000, D : 256000, E : 320000 };

现在我们将定义两个函数,findBitRate用于定位和转换半字节比特率,buildStats用于将所有收集到的信息压缩成先前确定的最终stats对象。

function buildStats(bitrate, size, cb) {
  var magnitudes = [ 'hours', 'minutes', 'seconds', 'milliseconds'],
    duration = {}, stats,
    hours = (size / (bitrate / 8) / 3600);

  (function timeProcessor(time, counter) {
      var timeArray = [], factor = (counter < 3) ? 60 : 1000 ;
      if (counter) {        
        timeArray = (factor * +('0.' + time)).toString().split('.');
      }

      if (counter < magnitudes.length - 1) {
        duration[magnitudes[counter]] = timeArray[0] || Math.floor(time);
        duration[magnitudes[counter]] = +duration[magnitudes[counter]];
        counter += 1;
        timeProcessor(timeArray[1] || time.toString().split('.')[1], counter);
        return;
      }
        //round off the final magnitude
        duration[magnitudes[counter]] = Math.round(timeArray.join('.'));
  }(hours, 0));

  stats = {
    duration: duration,
    bitrate: bitrate,
    filesize: size,
    timestamp: Math.round(hours * 3600000),
    timesig: ''
  };

  function pad(n){return n < 10 ? '0'+n : n}  
   magnitudes.forEach(function (mag, i) {
   if (i < 3) {
    stats.timesig += pad(duration[mag]) + ((i < 2) ? ':' : '');
   }
  });

  cb(null, stats);
}

buildStats接受bitrate, sizecb参数。它使用bitratesize来计算音轨中的秒数,然后使用这些信息生成stats对象,并通过cb函数传递。

为了将bitrate传递给buildStats,让我们按照以下代码编写findBitRate函数:

function findBitRate(f, cb) {
   fs.createReadStream(f)
    .on('data', function (data) {
      var i;
      for (i = 0; i < data.length; i += 2) {
        if (data.readUInt16LE(i) === 64511) {
          this.destroy();
          cb(null, bitrates[data.toString('hex', i + 2, i + 3)[0]]);
          break;
        };
    }
  }).on('end', function () {
    cb(new Error('could not find bitrate, is this definitely an MPEG-1 MP3?'));
  });   
}

最后,我们公开了一个stat方法,它利用我们的函数来生成stats对象:

exports.stat = function (f, cb) {
  fs.stat(f, function (err, fstats) {
    findBitRate(f, function (err, bitrate) {
      if (err) { cb(err); return; }
      buildStats(bitrate, fstats.size, cb);    
    });
  });
}

现在让我们运行我们从上一个示例中的(should)测试:

node test 

它应该输出以下内容:

All tests passed

它是如何工作的...

exports对象是 Node 平台的核心部分。它是require的另一半。当我们需要一个模块时,添加到exports的任何属性都通过require暴露出来。因此,当我们这样做时:

var mp3dat = require('mp3dat');

我们可以通过mp3dat.stat访问exports.stat,甚至可以通过require('mp3dat').stat访问(假设我们已经将mp3dat安装为一个模块,参见将模块部署到 npm)。

如果我们想为整个模块公开一个函数,我们使用module.exports,就像我们在本示例的准备就绪部分中设置的顶级index.js文件一样。

我们的stat方法首先调用fs.stat,并传递用户提供的文件名(f)。我们使用提供的fstats对象来检索文件的大小,然后将其传递给buildStats。也就是说,在我们调用findBitRate来检索 MP3 的bitrate之后,我们也将其传递给buildStats

buildStats回调直接通过我们的stat方法的回调传递,用户回调的执行起源于buildStats

findBitRate创建了用户提供文件(f)的readStream,并循环遍历每个发出的data块,每次两个字节,从而减少搜索时间。我们之所以能这样做,是因为我们正在寻找两个字节的同步字,它们总是在可以被二整除的位置。在十六进制中,同步字是FFFB,作为 16 字节的小端无符号整数是64511(这仅适用于没有错误保护的 MPEG-1 MP3 文件)。

MP3 同步字后面的四个比特(半字节)包含比特率值。因此,我们通过Buffer.toString方法将其传递,要求十六进制输出,然后与我们的bitrates对象映射进行匹配。在我们的test.mp3文件的情况下,半字节的十六进制值为9,表示每秒128000比特的比特率。

一旦我们找到比特率,我们执行回调并调用this.destroy,这会突然终止我们的readStream,防止end事件被触发。只有在没有发现比特率的情况下,end事件才会发生,此时我们通过回调发送错误。

buildStats接收bitrate并将其除以8得到每秒的字节数(8 位为 1 字节)。将 MP3 的总字节数除以每秒的字节数得到秒数。然后我们再除以 3,600 得到hours变量,然后将其传递到嵌入的timeProcessor函数中。timeProcessor简单地通过magnitudes数组(小时,分钟,秒,毫秒)进行递归,直到seconds被准确转换并分配到每个数量级,从而得到我们的duration对象。然后,我们使用计算出的持续时间(以任何形式)来构建我们的timestamptimesig属性。

还有更多...

模块的使用示例可以成为最终用户的重要资源。让我们为我们的新模块编写一个示例。

编写模块使用示例

我们将在mp3dat文件夹中创建一个examples文件夹,并创建一个名为basic.js的文件(用于基本用法示例),将以下内容写入其中:

var mp3dat = require('../index.js');

mp3dat.stat('../test/test.mp3', function (err, stats) {
  console.log(stats);
});

这应该导致控制台输出以下内容:

{ duration: { hours: 0, minutes: 0, seconds: 5, milliseconds: 186 },
  bitrate: 128000,
  filesize: 82969,
  timestamp: 5186,
  timesig: '00:00:05' }

另请参阅

  • 在本章讨论的创建测试驱动模块 API

  • 从功能到原型的重构在本章中讨论

  • 将模块部署到 npm在本章中讨论

从功能到原型的重构

在上一篇文章中创建的功能模拟可以帮助我们对概念有所了解,并且对于范围较小、简单的模块可能是完全足够的。

然而,原型模式(以及其他模式)通常被模块创建者使用,经常用于 Node 的核心模块,并且是原生 JavaScript 方法和对象的基础。

原型继承稍微更节省内存。原型上的方法在调用之前不会被实例化,并且它们会被重复使用,而不是在每次调用时重新创建。

另一方面,它可能比我们上一个配方的过程式风格稍慢,因为 JavaScript 引擎需要额外的开销来遍历原型链。尽管如此,将模块视为用户可以创建实例的实体,并以(例如,基于原型的方法)实现它们可能更合适。首先,这样可以更容易地通过克隆和原型修改进行编程扩展。这为最终用户提供了极大的灵活性,同时模块代码的核心完整性保持不变。

在这个配方中,我们将根据原型模式重写上一个任务中的代码。

准备工作

让我们开始编辑mp3dat/lib中的index.js

如何做...

首先,我们需要创建一个构造函数(使用new调用的函数),我们将其命名为Mp3dat

var fs = require('fs');

function Mp3dat(f, size) {
  if (!(this instanceof Mp3dat)) {
    return new Mp3dat(f, size);
  }
  this.stats = {duration:{}};
}

我们还像上一个任务一样需要fs模块。

让我们向构造函数的原型添加一些对象和方法:

Mp3dat.prototype._bitrates = { 1 : 32000, 2 : 40000, 3 : 48000, 4 : 56000, 5 : 64000, 6 : 80000, 7 : 96000, 8 : 112000, 9 : 128000, A : 160000, B : 192000, C : 224000, D : 256000, E : 320000 };

Mp3dat.prototype._magnitudes = [ 'hours', 'minutes', 'seconds', 'milliseconds'];

Mp3dat.prototype._pad = function (n) { return n < 10 ? '0' + n : n; }  

Mp3dat.prototype._timesig = function () {
  var ts = '', self = this;;
  self._magnitudes.forEach(function (mag, i) {
   if (i < 3) {
    ts += self._pad(self.stats.duration[mag]) + ((i < 2) ? ':' : '');
   }
  });
  return ts;
}

我们的新Mp3dat属性中的三个(_magnitudes、_pad_timesig)在buildStats函数中以某种形式被包含。我们在它们的名称前加上下划线(_)来表示它们是私有的。这只是一个约定,JavaScript 实际上并没有将它们私有化。

现在我们将上一个配方的findBitRate函数转换如下:

Mp3dat.prototype._findBitRate = function(cb) {
  var self = this;
   fs.createReadStream(self.f)
    .on('data', function (data) {
      var i = 0;
       for (i; i < data.length; i += 2) {
        if (data.readUInt16LE(i) === 64511) {
          self.bitrate = self._bitrates[data.toString('hex', i + 2, i + 3)[0]];
          this.destroy();
          cb(null);
          break;
        };
    }
  }).on('end', function () {
    cb(new Error('could not find bitrate, is this definitely an MPEG-1 MP3?'));
  });
}

唯一的区别是我们从对象(self.f)中加载文件名,而不是通过第一个参数,我们将bitrate加载到对象上,而不是通过cb的第二个参数发送它。

现在,为了将buildStats转换为原型模式,我们编写以下代码:

Mp3dat.prototype._buildStats = function (cb) {
  var self = this,
  hours = (self.size / (self.bitrate / 8) / 3600);

  self._timeProcessor(hours, function (duration) {
    self.stats = {
      duration: duration,
      bitrate: self.bitrate,
      filesize: self.size,
      timestamp: Math.round(hours * 3600000),
      timesig: self._timesig(duration, self.magnitudes)
    };
    cb(null, self.stats);

  });
}

我们的_buildStats原型方法比上一个任务中的buildStats方法要小得多。我们不仅提取了它的内部magnitudes数组、pad实用函数和时间签名功能(将其包装成自己的_timesig方法),还将内部递归的timeProcessor函数外包到了一个原型方法中。

Mp3dat.prototype._timeProcessor = function (time, counter, cb) {
  var self = this, timeArray = [], factor = (counter < 3) ? 60 : 1000,
    magnitudes = self._magnitudes, duration = self.stats.duration;

  if (counter instanceof Function) {
    cb = counter;
    counter = 0;
  }

  if (counter) {        
    timeArray = (factor * +('0.' + time)).toString().split('.');
  }
  if (counter < magnitudes.length - 1) {
    duration[magnitudes[counter]] = timeArray[0] || Math.floor(time);
    duration[magnitudes[counter]] = +duration[magnitudes[counter]];
    counter += 1;
    self._timeProcessor.call(self, timeArray[1] || time.toString().split('.')[1], counter, cb);
    return;
  }
    //round off the final magnitude (milliseconds)
    duration[magnitudes[counter]] = Math.round(timeArray.join('.'));
    cb(duration);
}

最后,我们编写stat方法(没有下划线前缀,因为它是供公共使用的),并导出Mp3dat对象。

Mp3dat.prototype.stat = function (f, cb) {
  var self = this;
  fs.stat(f, function (err, fstats) {
    self.size = fstats.size;
    self.f = f;
    self._findBitRate(function (err, bitrate) {
      if (err) { cb(err); return; }
      self._buildStats(cb);
    });    
  });
}

module.exports = Mp3dat();

我们可以通过运行我们在第一个配方中构建的测试来确保一切都正确。在mp3dat文件夹的命令行中,我们说:

node test 

应该输出:

All tests passed

它是如何工作的...

在上一个配方中,我们有一个exports.stat函数,它调用findBitRatebuildStats函数来获取stats对象。在我们重构的模块中,我们将stat方法添加到原型上,并通过module.exports导出整个Mp3dat构造函数。

我们不必使用newMp3dat传递给module.exports。我们的函数在直接调用时生成新的实例,代码如下:

  if (!(this instanceof Mp3dat)) {
    return new Mp3dat();
  }

这真的是一个保险策略。使用new初始化构造函数更有效(尽管在边缘上)。

我们重构的代码中的stat方法与先前任务中的exports.stat函数不同。它不是将文件名和指定 MP3 的大小作为参数传递给findBitRatebuildStats,而是通过this将它们分配给父对象(将其分配给self以避免this的新回调范围重新分配)。

然后调用_findBitRate_buildStats方法,最终生成stats对象并将其传递回用户的回调。

在我们的test.mp3文件上运行mp3dat.stats之后,我们重构的mp3dat模块对象将包含以下内容:

{ stats:
   { duration: { hours: 0, minutes: 0, seconds: 5, milliseconds: 186 },
     bitrate: 128000,
     filesize: 82969,
     timestamp: 5186,
     timesig: '00:00:05' },
  size: 82969,
  f: 'test/test.mp3',
  bitrate: 128000 }

然而,在前一个示例中,返回的对象将简单地如下所示:

{ stat: [Function] }

功能风格揭示了 API。我们重构的代码允许用户以多种方式与信息进行交互(通过statsmp3dat对象)。我们还可以扩展我们的模块,并在stats对象之外的其他时间填充mp3dat

还有更多...

我们可以构建我们的模块,使其更容易使用。

将 stat 函数添加到初始化的 mp3dat 对象中

如果我们想直接将我们的stat函数暴露给mp3dat对象,从而允许我们直接查看 API(例如,使用console.log),我们可以通过删除Mp3dat.prototype.stat并修改Mp3dat来添加它如下:

function Mp3dat() {
  var self = this;
  if (!(this instanceof Mp3dat)) {
    return new Mp3dat();
  }
  self.stat = function (f, cb) {
    fs.stat(f, function (err, fstats) {
      self.size = fstats.size;
      self.f = f;
      self._findBitRate(function (err, bitrate) {
        if (err) { cb(err); return; }
        self._buildStats(cb);
      });    
    });
  }  
  self.stats = {duration:{}};
}

然后我们的最终对象变成:

{ stat: [Function],
  stats:
   { duration: { hours: 0, minutes: 0, seconds: 5, milliseconds: 186 },
     bitrate: 128000,
     filesize: 82969,
     timestamp: 5186,
     timesig: '00:00:05' },
  size: 82969,
  f: 'test/test.mp3',
  bitrate: 128000 }

或者,如果我们不关心将stats对象和其他Mp3dat属性推送到模块用户,我们可以将一切保持原样,只需更改以下代码:

module.exports = Mp3dat()

到:

exports.stat = function (f, cb) {
  var m = Mp3dat();
  return Mp3dat.prototype.stat.call(m, f, cb);
}

这使用call方法将Mp3dat范围应用于stat方法(允许我们依赖stat方法),并将返回一个具有以下内容的对象:

{ stat: [Function] }

就像我们的模块的第一次写作一样,只是我们仍然保留了原型模式。这种第二种方法稍微更有效。

允许多个实例

我们的模块是一个单例,因为它返回已初始化的Mp3dat对象。这意味着无论我们需要多少次并将其分配给变量,模块用户始终将引用相同的对象,即使Mp3dat在父脚本加载的不同子模块中被需要。

这意味着如果我们尝试同时运行两个mp3dat.stat方法,将会发生糟糕的事情。在需要多次引用我们的模块的情况下,持有相同对象的两个变量最终可能会互相覆盖属性,导致不可预测(和令人沮丧)的代码。最有可能的结果是readStreams会发生冲突。

克服这一点的一种方法是修改以下内容:

module.exports = Mp3dat()

到:

module.exports = Mp3dat

然后使用以下代码加载两个实例:

var Mp3dat = require('../index.js'),
	mp3dat = Mp3dat(),
      mp3dat2 = Mp3dat();

如果我们想要提供单例和多个实例,我们可以在构造函数的原型中添加一个spawnInstance方法:

Mp3dat.prototype.spawnInstance = function () {
  return Mp3dat();
}

module.exports = Mp3dat();

然后我们可以做如下事情:

var mp3dat = require('../index.js'),
   mp3dat2 = mp3dat.spawnInstance();

mp3datmp3dat2都将是单独的Mp3dat实例,而在以下情况下:

var mp3dat = require('../index.js'),
   mp3dat2 = require('../index.js');

两者将是相同的实例。

另请参阅

  • 在本章中讨论的编写功能模块模拟

  • 在本章中讨论的扩展模块的 API

  • 在本章中讨论的将模块部署到 npm

扩展模块的 API

我们可以以许多方式扩展我们的模块,例如,我们可以使其支持更多的 MP3 类型,但这只是例行工作。只需找出不同的同步字和不同类型的 MP3 的比特率,然后将它们添加到相关位置。

对于更有趣的冒险,我们可以扩展 API,为我们的模块用户创建更多选项。

由于我们使用流来读取我们的 MP3 文件,我们可以允许用户传入文件名或 MP3 数据流,提供简单(使用简单文件名)和灵活(使用流)两种方式。这样我们就可以启动下载流、STDIN 流,或者实际上任何 MP3 数据流。

准备工作

我们将从前一篇食谱的允许多个实例部分的*还有更多..*中继续使用我们的模块。

如何做...

首先,我们将为我们的新 API 添加一些更多的测试。在tests/index.js中,我们将从mp3dat.stat调用中提取回调函数到全局范围,并将其命名为cb:

function cb (err, stats) {
  should.ifError(err);

  //expected properties
  stats.should.have.property('duration');

  //...all the other unit tests here

  console.log('passed');

};

现在我们将调用stat以及一个我们将要编写并命名为statStream的方法:

mp3dat.statStream({stream: fs.createReadStream(testFile),
  size: fs.statSync(testFile).size}, cb);

mp3dat2.stat(testFile, cb);

注意我们使用了两个Mp3dat实例(mp3datmp3dat2)。所以我们可以同时运行statstatStream测试。由于我们正在创建一个readStream,我们需要在我们的[tests/index.js]文件的顶部引入fs

var should = require('should');
var fs = require('fs');
var mp3dat = require('../index.js'),
  mp3dat2 = mp3dat.spawnInstance();

我们还将为statStream方法添加一些顶层的should测试,如下所示:

should.exist(mp3dat);
mp3dat.should.have.property('stat');
mp3dat.stat.should.be.an.instanceof(Function);
mp3dat.should.have.property('statStream');
mp3dat.statStream.should.be.an.instanceof(Function);

现在来满足我们测试的期望。

lib/index.js中,我们向Mp3dat的原型添加了一个新的方法。它不再接受第一个参数的文件名,而是接受一个对象(我们将其称为opts),该对象必须包含streamsize属性:

Mp3dat.prototype.statStream = function (opts, cb) {
  var self = this,
    errTxt = 'First arg must be options object with stream and size',
    validOpts = ({}).toString.call(opts) === '[object Object]'
      && opts.stream
      && opts.size
      && 'pause' in opts.stream
      && !isNaN(+opts.size);
   lib
  if (!validOpts) {
    cb(new Error(errTxt));
    return;
  }

  self.size = opts.size;
  self.f = opts.stream.path;

  self.stream = opts.stream;

  self._findBitRate(function (err, bitrate) {
    if (err) { cb(err); return; }
    self._buildStats(cb);
  });    

}

最后,对_findBitRate进行一些修改,我们就完成了。

Mp3dat.prototype._findBitRate = function(cb) {
  var self = this,
    stream = self.stream || fs.createReadStream(self.f);
  stream
    .on('data', function (data) {
      var i = 0;
       for (i; i < data.length; i += 2) {
        if (data.readUInt16LE(i) === 64511) {
          self.bitrate = self._bitrates[data.toString('hex', i + 2, i + 3)[0]];
          this.destroy();
          cb(null);
          break;
        };
//rest of the _findBitRate function...

我们有条件地挂接到传入的流,或者我们从给定的文件名创建一个流。

让我们运行我们的测试(从mp3dat文件夹):

node tests 

结果应该是:

passed
passed

一个用于stat,一个用于statStream

它是如何工作的...

我们已经在使用流来检索我们的数据。我们只需通过修改_findBitRate来向用户公开这个接口,使其可以从文件名生成自己的流,或者如果流存在于父构造函数的属性中(self.stream),它只需将该流插入到已经存在的流程中。

然后,我们通过定义一个新的 API 方法statStream来使这个功能对模块用户可用。我们首先通过为其制定测试来概念化它,然后通过Mp3dat.prototype来定义它。

statStream方法类似于stat方法(实际上,我们可以将它们合并,参见还有更多..)。除了检查输入的有效性之外,它只是向Mp3dat实例添加了一个属性:stream属性,它取自opts.stream。为了方便起见,我们将opts.stream.pathself.f进行交叉引用(这取决于流的类型,这可能有或者没有)。这基本上是多余的,但对于用户的调试目的可能是有用的。

statStream的顶部,我们有validOpts变量,其中有一系列由&&条件连接的表达式。这是一堆if语句的简写。如果这些表达式中的任何一个测试失败,opts对象就无效。一个有趣的表达式是opts.stream中的'pause',它测试opts.stream是否绝对是一个流或者是从流继承而来的(所有流都有一个pause方法,in检查整个原型链中的属性)。在validOpts测试中另一个值得注意的表达式是!isNaN(+opts.size),它检查opts.size是否是一个有效的数字。前面的+将其转换为Number类型,!isNaN检查它是否不是"not a number"(JavaScript 中没有isNumber,所以我们使用!isNaN)。

还有更多...

现在我们有了这个新方法。让我们写一些更多的例子。我们还将看到如何将statStreamstat合并在一起,并通过使其发出事件来进一步增强我们的模块。

制作标准输入流示例

为了演示与其他流的使用,我们可以编写一个使用process.stdin流的示例,如下所示:

//to use try :
// cat ../test/test.mp3 | node stdin_stream.js 82969
// the argument (82969) is the size in bytes of the mp3

if (!process.argv[2]) {
  process.stderr.write('\nNeed mp3 size in bytes\n\n');
  process.exit();
}

var mp3dat = require('../');
process.stdin.resume();
mp3dat.statStream({stream : process.stdin, size: process.argv[2]}, function (err, stats) {
  if (err) { console.log(err); }
  console.log(stats);
});

我们在示例中包含了注释,以确保我们的用户了解如何使用它。我们在这里所做的就是接收process.stdin流和文件大小,然后将它们传递给我们的statStream方法。

制作 PUT 上传流示例

在第二章的处理文件上传食谱中,探索 HTTP 对象,我们在那个食谱的*还有更多..*部分创建了一个 PUT 上传实现。

我们将从该示例中获取put_upload_form.html文件,并在mp3dat/examples文件夹中创建一个名为HTTP_PUT_stream.js的新文件。

var mp3dat = require('../../mp3dat');
var http = require('http');
var fs = require('fs');
var form = fs.readFileSync('put_upload_form.html');
http.createServer(function (req, res) {
  if (req.method === "PUT") {
    mp3dat.statStream({stream: req, size:req.headers['content-length']}, function (err, stats) {
      if (err) { console.log(err); return; }
      console.log(stats);
    });

  }
  if (req.method === "GET") {
    res.writeHead(200, {'Content-Type': 'text/html'});
    res.end(form);
  }
}).listen(8080);

在这里,我们创建了一个服务器,用于提供put_upload_form.html文件。HTML 文件允许我们指定要上传的文件(必须是有效的 MP3 文件),然后将其发送到服务器。

在我们的服务器中,我们将req(一个流)传递给stream属性和req.headers['content-length'],这样我们就可以得到 MP3 的大小(以浏览器通过Content-Length头部指定的字节为单位)。

然后我们通过在控制台上记录stats来完成(我们还可以通过以 JSON 形式将stats发送回浏览器来扩展此示例)。

合并 stat 和 statStream

statstatStream之间有很多相似的代码。通过一些重构,我们可以将它们合并成一个方法,允许用户将包含文件名的字符串或包含流和大小属性的对象直接传递给stat方法。

首先,我们需要更新我们的测试和示例。在test/index.js中,我们应该删除以下代码:

mp3dat.should.have.property('statStream');
mp3dat.statStream.should.be.an.instanceof(Function);

由于我们正在将statStream合并到stat中,我们对statstatStream的两次调用应该变成:

mp3dat.stat({stream: fs.createReadStream(testFile),
    size: fs.statSync(testFile).size}, cb);
mp3dat2.stat(testFile, cb);

examples/stdin_stream.js中的statStream行应该变成:

mp3dat.stat({stream : process.stdin, size: process.argv[2]}

HTTP_PUT_stream.js中应该是:

mp3dat.stat({stream: req, size: req.headers['content-length']}

lib/index.js中,我们删除streamStat方法,插入_compile方法:

Mp3dat.prototype._compile =  function (err, fstatsOpts, cb) {
  var self = this;
  self.size = fstatsOpts.size;
  self.stream = fstatsOpts.stream;
    self._findBitRate(function (err, bitrate) {
    if (err) { cb(err); return; }
    self._buildStats(cb);
  });    
}

最后,我们修改我们的Mp3dat.prototype.stat方法如下:

Mp3dat.prototype.stat = function (f, cb) {
  var self = this, isOptsObj = ({}).toString.call(f) === '[object Object]';

  if (isOptsObj) {
    var opts = f, validOpts = opts.stream && opts.size
      && 'pause' in opts.stream && !isNaN(+opts.size);
    errTxt = 'First arg must be options object with stream and size'

    if (!validOpts) { cb(new Error(errTxt)); return; }

    self.f = opts.stream.path;
    self._compile(null, opts, cb);
    return;
  }

  self.f = f;
  fs.stat(f, function (err, fstats) {
    self._compile.call(self, err, fstats, cb);
  });
}

实际生成stats的代码已放入_compile方法中。如果第一个参数是一个对象,我们假设是一个流,stats就承担了以前的statStream的角色,调用_compile并从函数中提前返回。如果不是,我们假设是一个文件名,并使用 JavaScript 的call方法在fs.stat回调中调用_compile,确保我们的this/self变量通过_compile方法传递。

集成 EventEmitter

在本书中,我们通常通过回调参数或通过侦听事件来从模块中接收数据。我们可以进一步扩展我们的modules接口,允许用户通过使 Node 的EventEmitter采用我们的Mp3dat构造函数来侦听事件。

我们需要引入eventsutil模块,然后通过将Mp3datthis对象分配给它来将Mp3datEventEmitter连接起来,然后使用util.inherits给它Mp3dat EventEmitter的超级能力:

var fs = require('fs'),
  EventEmitter = require('events').EventEmitter,
  util = require('util');

function Mp3dat() {
  if (!(this instanceof Mp3dat)) {
    return new Mp3dat();
  }  
  EventEmitter.call(this);
  this.stats = {duration:{}};
}

util.inherits(Mp3dat, EventEmitter);

现在我们只需遍历Mp3dat的现有方法,并在相关位置插入emit事件。一旦找到bitrate,我们就可以像下面这样emit它:

Mp3dat.prototype._findBitRate = function(cb) {
//beginning of _findBitRate method
       for (i; i < data.length; i += 2) {
        if (data.readUInt16LE(i) === 64511) {
          self.bitrate = self._bitrates[data.toString('hex', i + 2, i + 3)[0]];
          this.destroy();
          self.emit('bitrate', self.bitrate);
          cb(null);
          break;
        };
 //rest of _findBitRate method

在我们回调错误的地方,我们也可以像下面的代码一样发出错误:

//last part of _findBitRate method
  }).on('end', function () {
    var err = new Error('could not find bitrate, is this definately an MPEG-1 MP3?');
    self.emit('error', err);
    cb(err);
  });

然后是时间签名:

Mp3dat.prototype._timesig = function () {
 //_timesig function code....
  self.emit('timesig', ts);
  return ts;
}

当然,stats对象:

Mp3dat.prototype._buildStats = function (cb) {
//_buildStats code
  self._timeProcessor(hours, function (duration) {
   //_timeProcessor code
    self.emit('stats', self.stats);
    if (cb) { cb(null, self.stats); }    
  });
}

我们还在_buildStats中添加了if (cb),因为如果用户选择侦听事件,则回调可能不再必要。

如果模块用户动态生成Mp3dat实例,他们可能希望有一种方法来连接到生成的实例事件:

Mp3dat.prototype.spawnInstance = function () {
  var m = Mp3dat();
  this.emit('spawn', m);
  return m;
}

最后,为了允许链接,我们还可以从两个地方的stat函数返回Mp3dat实例。首先在isOptsObj块内如下:

Mp3dat.prototype.stat = function (f, cb) {
//stat code
  if (isOptsObj) {
    //other code here
    self._compile(null, opts, cb);
    return self;
  }

然后在函数的最后,如下所示的代码:

  //prior stat code
  self.f = f;
  fs.stat(f, function (err, fstats) {
    self._compile.call(self, err, fstats, cb);
  });
  return self;
}

这是因为我们根据检测到的输入(文件名或流)从函数中提前返回,所以我们必须从两个地方返回self

现在我们可以为我们的新用户界面编写一个示例。让我们在mp3dat/examples中创建一个名为event_emissions.js的新文件。

var mp3dat = require('../index');

mp3dat
  .stat('../test/test.mp3')
  .on('bitrate', function (bitrate) {
    console.log('Got bitrate:', bitrate);
  })
  .on('timesig', function (timesig) {
     console.log('Got timesig:', timesig);
  })
  .on('stats', function (stats) {
     console.log('Got stats:', stats);
     mp3dat.spawnInstance();
  })
  .on('error', function (err) {
     console.log('Error:', err);
  })
  .on('spawn', function (mp3dat2) {
    console.log('Second mp3dat', mp3dat2);
  });

另请参阅

  • 在本章中讨论创建一个测试驱动的模块 API

  • 处理文件上传在第二章中讨论,探索 HTTP 对象

  • 在本章中讨论将模块部署到 npm

将模块部署到 npm

现在我们已经创建了一个模块,我们可以使用相同的集成工具与世界其他地方分享它:npm

做好准备

在我们可以部署到 npm 之前,我们需要创建一个package.json文件,所以让我们为我们的模块做这个。在mp3dat中,我们将创建package.json并添加以下代码:

{
  "author": "David Mark Clements <contact@davidmarkclements.com> (http://davidmarkclements.com)",
  "name": "mp3dat",
  "description": "A simple MP3 parser that returns stat infos in a similar style to fs.stat for MP3 files or streams. (MPEG-1 compatible only)",
  "version": "0.0.1",
  "homepage": "http://nodecookbook.com/mp3dat",
  "repository": {
    "type": "git",
    "url": "git://github.com/davidmarkclements/mp3dat.git"
  },
  "main": "./lib/index.js",
  "scripts": {
    "test": "node test"
  },
  "engines": {
    "node": "~0.6.13"
  },
  "dependencies": {},
  "devDependencies": {}
}

当然,我们可以插入我们自己的名称和包的名称。创建package.json文件的另一种方法是使用npm init,它通过命令行询问一系列问题,然后生成package.json文件。

我们可以在package.json中指定一个存储库。使用在线存储库(如 GitHub)来管理版本控制、共享代码并允许其他人在您的代码上工作是一个好主意。请参阅help.github.com/开始使用。

main属性很重要。它定义了我们模块的入口点,在我们的情况下是./lib/index.js(尽管我们也可以指定./index.js来加载./lib/index.js)。通过将scripts.test定义为node test,我们现在可以运行npm test(或者一旦通过npm安装了mp3dat,就可以运行npm mp3dat test)来执行我们的单元测试。

我们将按照上一个步骤中的方式将我们的模块部署到 npm,其中statstatStream都合并到了stat中,并且我们已经集成了EventEmitter

如何做...

要部署到npm,我们必须拥有开发者账户。我们通过执行以下操作来实现这一点:

npm adduser 

填写我们想要的用户名、密码和联系邮箱。就这样,我们现在注册了。

在继续发布我们的模块之前,我们会想要测试npm是否会在我们的系统上安装它。在mp3dat中,我们执行以下操作:

sudo npm install -g 

然后,如果我们从命令行运行node,我们应该能够:

require('mp3dat') 

如果没有收到错误消息。如果成功了,我们可以继续发布我们的模块!在mp3dat中,我们说以下内容:

npm publish 

现在,如果我们转到一个完全不同的文件夹(比如我们的主文件夹)并输入以下内容:

npm uninstall mp3dat
npm install mp3dat 

npm应该从其存储库中安装我们的包。

我们可以用以下命令来双重检查它是否存在:

npm search mp3dat 

或者,如果这花费的时间太长,我们可以在浏览器中转到search.npmjs.org/。我们的模块可能会出现在主页上(其中包含最近发布的模块)。或者我们可以点击search.npmjs.org/#/mp3dat直接转到我们模块的 npm 注册页面。

它是如何工作的...

npm是一个用 Node 编写的命令行脚本,为开发和发布模块提供了一些出色的工具。这些工具确实做到了它们所说的,adduser添加用户,install安装,publish发布。这真的非常优雅。

在服务器端,npm注册表由一个 CouchDB 数据库支持,该数据库保存了每个包的类似 JSON 的数据。甚至有一个我们可以连接到的 CouchDB _changes字段。在命令行上,我们可以这样做:

curl http://isaacs.couchone.com/registry/_changes?feed=continuous&include_docs=true 

观察模块在实时中添加和修改。如果没有发生任何事情,我们可以打开另一个终端并输入以下命令:

mp3dat unpublish
mp3dat publish 

这将导致 CouchDB 更改反馈更新。

还有更多...

npm有一些非常好的功能,让我们来看看其中的一些。

npm link

npm link命令对于模块作者来说可能很有用。

在开发过程中,如果我们想要将mp3dat作为全局模块进行引用,例如require('mp3dat'),每次进行更改时,我们可以通过运行以下命令来更新全局包:

sudo npm install . -g 

然而,当我们运行以下命令时,npm link提供了一个更简单的解决方案:

sudo npm link 

在我们的mp3dat文件夹中,从我们的全局node_modules文件夹到我们的工作目录创建了一个符号链接。这会导致 Node 将mp3dat视为已安装的全局模块,但我们对开发副本所做的任何更改都将在全局范围内反映出来。当我们完成开发并希望在系统上冻结模块时,我们只需取消链接,如下所示:

sudo npm unlink -g mp3dat 

.npmignore 和 npm 版本

我们的example文件可能在 GitHub 上很方便,但我们可能会决定它们在npm中没有什么好处。我们可以使用.npmignore文件来阻止某些文件被发布到npm包中。让我们在mp3dat文件夹中创建.npmignore,并放入:

examples/

现在,当我们重新发布到npm注册表时,我们的新包将不包括examples文件夹。在我们可以发布之前,我们要么取消发布,要么更改我们包的版本(或者我们可以使用--force参数)。让我们改变版本,然后再次发布:

npm version 0.0.2 --message "added .npmignore"
npm publish 

更改版本还将改变我们的package.json文件到新的版本号。

参见

  • 编写一个功能模块的模拟在本章讨论

  • 从功能到原型的重构在本章讨论

  • 在本章讨论的扩展模块的 API

  • 使用 Cradle 访问 CouchDB 更改流在第四章中讨论,与数据库交互

第十章:让它上线

在本章中,我们将涵盖:

  • 部署到服务器环境

  • 自动崩溃恢复

  • 持续部署

  • 使用平台即服务提供商进行托管

介绍

Node 是构建和提供在线服务的绝佳平台选择。无论是简单的、精简的网站,还是高度灵活的 Web 应用程序,或者超越 HTTP 的服务,我们都必须在某个时候部署我们的创作。

本章重点介绍了将我们的 Node 应用程序上线所需的步骤。

部署到服务器环境

虚拟专用服务器(VPS)、专用服务器或基础设施即服务(例如,亚马逊 EC2 或 Rackspace 等)以及拥有我们自己的服务器机器都有一个共同点:对服务器环境的完全控制。

然而,伴随着巨大的权力而来的是巨大的责任,我们需要意识到一些挑战。本配方将演示如何克服这些挑战,安全地在端口80上初始化一个 Node Web 应用程序。

准备就绪

当然,我们需要一个远程服务器环境(或我们自己的设置)。研究找到最适合我们需求的最佳套餐非常重要。

专用服务器可能很昂贵。硬件与软件的比例是一比一,我们实际上是在租用一台机器。

VPS 可能更便宜,因为它们共享单台机器(或集群)的资源,因此我们只租用托管操作系统实例所需的资源。然而,如果我们开始使用超出分配的资源,我们可能会受到处罚(停机时间,额外费用),因为过度使用可能会影响其他 VPS 用户。

IaaS 可能相对便宜,特别是在涉及扩展时(当我们需要更多资源时),尽管 IaaS 往往包含按使用量计费的元素,这意味着成本不是固定的,可能需要额外的监控。

我们的配方假设使用运行sshd(SSH 服务)的 Unix/Linux 服务器。此外,我们应该有一个指向我们服务器的域名。在这个配方中,我们将假设域名为nodecookbook.com。最后,我们必须在远程服务器上安装 Node。如果出现困难,我们可以使用www.github.com/joyent/node/wiki/Installation上提供的说明,或者通过包管理器安装,我们可以使用www.github.com/joyent/node/wiki/Installing-Node.js-via-package-manager上的说明。

我们将从第六章使用 Express 加速开发的倒数第二个配方中部署login应用程序,所以我们需要这个。

如何做...

为了准备我们的应用程序传输到远程服务器,我们将删除node_modules文件夹(我们可以在服务器上重建它):

rm -fr login/node_modules 

然后我们通过执行以下命令压缩login目录:

npm pack login 

这将生成一个压缩的存档,名称与package.json文件中给出的应用程序名称和版本相同,对于未经修改的 Express 生成的package.json文件,将生成文件名application-name-0.0.1.tgz

无论npm pack称其为什么,让我们将其重命名为login.tgz

mv application-name-0.0.1.tgz login.tgz #Linux/Mac OS X
rename application-name-0.0.1.tgz login.tgz ::Windows. 

接下来,我们将login.tgz上传到我们的服务器。例如,我们可以使用 SFTP:

sftp root@nodecookbook.com 

一旦通过 SFTP 登录,我们可以发出以下命令:

cd /var/www
put login.tgz 

将上传到/var/www目录并不是必需的,这只是放置网站的一个自然位置。

这假设我们已经通过 SFTP 从包含login.tgz的目录 SFTP 到我们的服务器。

接下来,我们通过 SSH 登录到服务器:

ssh -l root nodecookbook.com 

提示

如果我们使用 Windows 桌面,我们可以使用 putty 进行 SFTP 和 SSH 登录到我们的服务器:www.chiark.greenend.org.uk/~sgtatham/putty/

一旦登录到远程服务器,我们就导航到/var/www并解压login.tar.gz

tar -xvf login.tar.gz 

login.tar.gz解压缩时,它会在服务器上重新创建我们的login文件夹。

要重建node_modules文件夹,我们进入login文件夹并使用npm重新生成依赖项。

cd login
npm -d install

大多数服务器都有基于 shell 的编辑器,如nano,vimemacs。我们可以使用这些编辑器之一来更改app.js中的一行(或者通过 SFTP 传输修改后的app.js):

app.listen(80, function () { process.setuid('www-data'); });

我们现在正在监听标准 HTTP 端口,这意味着我们可以访问我们的应用程序而无需在其 Web 地址后加上端口号。但是,由于我们将以root身份启动应用程序(为了绑定到端口80是必要的),我们还将回调传递给listen方法,该方法将应用程序的访问权限从root更改为www-data

在某些情况下,根据文件权限,从我们的应用程序读取或写入文件可能不再起作用。我们可以通过更改所有权来解决这个问题:

chown -R www-data login 

最后,我们可以用以下方式启动我们的应用程序:

cd login
nohup node app.js & 

我们可以确保我们的应用程序正在作为www-data运行:

ps -ef | grep node 

它是如何工作的...

我们修改了app.listen以绑定到端口80,并添加了一个回调函数,该函数将用户 ID 从root重置为www-data

listen添加回调不仅限于 Express,它在使用普通的httpServer实例时也是一样的。

root身份运行 Web 服务器是不好的做法。如果我们的应用程序被攻击者入侵,他们将通过我们应用程序的特权状态获得对我们系统的root访问权限。

为了降级我们的应用程序,我们调用process.setuid并传入www-data. process.setuid。这要么是用户的名称,要么是用户的 UID。通过传递一个名称,我们导致process.setuid阻塞事件循环(基本上冻结操作),同时它交叉引用用户字符串到其 UID。这消除了应用程序绑定到端口80并作为root运行的潜在时间。实质上,将字符串传递给process.setuid而不是底层 UID 意味着在应用程序不再是root之前什么都不会发生。

我们使用nohup调用我们的进程,然后跟上&。这意味着我们可以自由结束我们的 SSH 会话,而不会导致我们的应用程序随着会话终止而终止。

安德符号将我们的进程转换为后台任务,因此我们可以在其运行时做其他事情(比如退出)。nohup意味着忽略挂断信号(HUP)。每当 SSH 会话终止时,HUP 被发送到通过 SSH 启动的任何运行进程。基本上,使用nohup允许我们的 Web 应用程序在 SSH 会话结束后继续存在。

还有更多...

有其他方法可以独立于我们的会话启动我们的应用程序,并绑定到端口80而不以root身份运行应用程序。此外,我们还可以运行多个应用程序并使用http-proxy将它们代理到端口80

使用screen而不是nohup

实现与使用nohup独立于我们的 SSH 会话的另一种方法是使用screen。我们将使用它如下:

screen -S myAppName 

这将给我们一个虚拟终端,我们可以说:

cd login
node app.js 

然后我们可以通过按下Ctrl + A,然后按D来离开虚拟终端。我们将返回到我们最初的终端。虚拟终端将在我们注销 SSH 后继续运行。我们随时可以重新登录 SSH 并说:

screen -r myAppName 

在那里我们可以看到任何控制台输出并停止(Ctrl + C)和启动应用程序。

使用特权端口的 authbind

在这个例子中,我们应该以非 root 用户的身份 SSH 到我们的服务器:

ssh -l dave nodecookbook.com 

绑定到端口80的另一种方法是使用authbind,可以通过我们服务器的软件包管理器安装。例如,如果我们的软件包管理器是apt-get,我们可以说:

sudo apt-get install authbind 

authbind通过抢占端口绑定的操作系统策略并在执行时利用一个名为LD_PRELOAD的环境变量来工作。因此,它永远不需要以root权限运行。

为了使它为我们工作,我们必须进行一些简单的配置工作,如下所示:

sudo touch /etc/authbind/byport 80
sudo chown dave /etc/authbind/byport 80
sudo chmod 500 /etc/authbind/byport 80 

这告诉authbind允许用户dave将进程绑定到端口80

我们不再需要更改进程 UID,因此我们编辑app.js的倒数第二行为:

app.listen(80);

我们还应该按以下方式更改login文件夹的所有权:

chown -R dave login 

现在我们可以在完全不触及root访问的情况下启动我们的服务器:

nohup authbind node app.js & 

authbind可以使我们的应用立即运行,无需任何修改。但是,它目前缺乏 IPv6 支持,因此尚不具备未来的兼容性。

从端口 80 托管多个进程

如何使用默认 HTTP 端口提供多个进程?

我们可以使用第三方http-proxy模块来实现这一点。

npm install http-proxy 

假设我们有两个应用程序,一个(我们的login应用程序)托管在login.nodecookbook.com,另一个(本书第一个示例中的server.js文件)简单地托管在nodecookbook.com。这两个域指向同一个 IP。

server.js将监听端口8080,我们将修改login/app.js以再次监听端口3000,如下面的代码所示:

app.listen(3000, '127.0.0.1');

我们还添加了第二个参数,定义绑定到哪个地址(而不是任何地址)。这可以防止我们的服务器通过端口被访问。

让我们在一个新文件夹中创建一个文件,称之为proxy.js,并写入以下内容:

require('http-proxy')
  .createServer({router : {
    'login.nodecookbook.com': 'localhost:3000',
    'nodecookbook.com': 'localhost:8080'
  }}).listen(80, function () {
    process.setuid('www-data');
  });

createServer传递的对象包含一个路由器属性,该属性本身是一个对象,指示http-proxy根据其端口将特定域上的传入流量路由到正确的本地托管进程。

最后,我们绑定到端口80,并从root降级到www-data

要初始化,我们必须执行:

nohup node login/app.js &
nohup node server.js &
nohup node proxy.js & 

由于我们将代理服务器绑定到端口80,这些命令必须以root身份运行。如果我们正在使用非 root 帐户操作 SSH,我们只需在这三个命令前加上sudo

另请参阅

  • 本章讨论的自动崩溃恢复

  • 本章讨论的持续部署

  • 本章讨论的作为服务提供商的平台托管

自动崩溃恢复

当我们创建一个站点时,服务器和站点逻辑都绑定在一个进程中。而在其他平台上,服务器代码已经就位。如果我们的站点代码有错误,服务器很不可能崩溃,因此在许多情况下,即使其中一部分出现问题,站点也可以保持活动状态。

对于基于 Node 的网站,一个小错误可能会导致整个进程崩溃,而这个错误可能只会在很长时间内触发一次。

作为一个假设的例子,错误可能与 POST 请求的字符编码有关。当像 Felix Geisendörfer 这样的人完成并提交表单时,突然间我们整个服务器崩溃了,因为它无法处理变音符号。

在这个示例中,我们将使用 Upstart,这是一个可用于 Linux 服务器的事件驱动的 init 服务,它不是基于 Node,但仍然是一个非常方便的助手。

准备工作

我们需要在服务器上安装 Upstart。upstart.ubuntu.com包含有关如何下载和安装的说明。如果我们已经在使用 Ubuntu 或 Fedora 远程服务器,则 Upstart 将已经集成。

如何做...

让我们创建一个新的服务器,当我们通过 HTTP 访问它时故意崩溃:

var http = require('http');
http.createServer(function (req, res) {
  res.end("Oh oh! Looks like I'm going to crash...");
  throw crashAhoy;
}).listen(8080);

在第一页加载后,服务器将崩溃,站点将下线。

让我们将这段代码称为server.js,将其放在远程服务器上的/var/www/crashingserver

现在我们创建我们的 Upstart 配置文件,将其保存在服务器上的/etc/init/crashingserver.conf

start on started network-services

respawn
respawn limit 100 5

setuid www-data

exec /usr/bin/node /var/www/crashingserver/server.js >> \ /var/log/crashingserver.log 2>&1 

post-start exec echo "Server was (re)started on $(date)" | mail -s "Crashing Server (re)starting" dave@nodecookbook.com

最后,我们初始化我们的服务器如下:

start crashingserver 

当我们访问http://nodecookbook.com:8080并刷新页面时,我们的网站仍然可以访问。快速查看/var/log/crashingserver.log,我们可以发现服务器确实崩溃了。我们还可以检查我们的收件箱以查找服务器重新启动通知。

它是如何工作的...

Upstart 服务的名称取自特定的 Upstart 配置文件名。我们使用start crashingserver来启动/etc/init/crashingserver.conf Upstart 服务。

配置的第一行确保我们的 Web 服务器在远程服务器的操作系统重新启动时自动恢复(例如,由于停电或需要重新启动等)。

respawn被声明两次,一次用于打开重生,然后设置一个“重生限制 - 每 5 秒最多 100 次重启”。限制必须根据我们自己的情况进行设置。如果网站流量较低,这个数字可能会调整为在 8 秒内重启 10 次。

我们希望尽可能保持在线,但如果问题持续存在,我们可以将其视为一个警示,表明错误对用户体验或系统资源产生了不利影响。

下一行将我们的服务器初始化为www-data用户,并将输出发送到/var/log/crashingserver.log

最后一行在我们的服务器启动或重新启动后立即发送电子邮件。这样我们就可以得知可能需要解决服务器问题。

还有更多...

让我们实现另一个 Upstart 脚本,如果服务器崩溃超出其“重生限制”,我们还将看另一种方法来保持我们的服务器在线。

检测重生限制违规

如果我们的服务器超出了“重生限制”,很可能存在严重问题,应尽快解决。我们需要立即了解情况。为了在 Upstart 中实现这一点,我们可以创建另一个 Upstart 配置文件,监视crashingserver守护程序,如果“重生限制”被违反,则发送电子邮件。

task

start on stopped crashingserver PROCESS=respawn

script
  if [ "$JOB" != ''  ]
    then echo "Server "$JOB" has crashed on $(date)" | mail -s \
    $JOB" site down!!" dave@nodecookbook.com
  fi
end script

让我们把它保存到/etc/init/sitedownmon.conf

然后我们做:

start crashingserver
start sitedownmon 

我们将这个 Upstart 进程定义为一个任务(它只有一件事要做,之后就退出了)。我们不希望在我们的服务器崩溃后它继续存在。

crashingserver守护程序在重生期间停止时执行该任务(例如,当“重生限制”被打破时)。

我们的脚本段(指令)包含一个小的 bash 脚本,用于检查JOB环境变量的存在(在我们的情况下,它将设置为crashingserver),然后相应地发送电子邮件。如果我们不检查它的存在,当它首次启动并发送一个带有空JOB变量的电子邮件时,sitedownmon似乎会触发错误的警报。

稍后我们可以通过在每个服务器的sitedownmon.conf中添加一行来扩展此脚本:

start on stopped anotherserver PROCESS=respawn

使用 forever 保持在线

有一个更简单的基于 Node 的替代方案叫做forever:

npm -g install forever 

如果我们只是用以下方式启动我们的服务器:

forever server.js 

然后访问我们的网站,一些终端输出将包含以下内容:

warn: Forever detected script exited with code: 1
warn: Forever restarting script for 1 time

但我们仍然可以访问我们的网站(尽管它已经崩溃并重新启动)。

要在远程服务器上部署我们的网站,我们通过 SSH 登录到服务器,安装forever并说:

forever start server.js 

虽然这种技术确实较少复杂,但也较不稳健。Upstart 提供了核心内核功能,因此是系统关键性的。如果 Upstart 失败,内核就会发生恐慌,整个服务器就会重新启动。

然而,在 Nodejitsu 的 PaaS 堆栈上广泛使用forever,其吸引人的简单性可能适用于不太关键的生产环境。

另请参阅

  • 本章讨论了部署到服务器环境

  • 本章讨论了使用平台即服务提供商进行托管

  • 本章讨论了持续部署

持续部署

我们的流程越简化,我们就能更加高效。持续部署是指将小的持续改进提交到生产服务器,以节省时间、高效地进行。

持续部署对团队协作项目尤为重要。与其在代码的不同分支上工作并花费额外的时间、金钱和精力进行集成,不如让每个人都在同一代码库上工作,这样集成就会更加顺畅。

在这个示例中,我们将使用 Git 作为版本控制工具创建部署流程。虽然这可能不是 Node,但它肯定可以提高编码、部署和管理 Node 项目的生产力。

注意

如果我们对 Git 有点陌生,我们可以从 Github 的帮助文档中获得见解,help.github.com

准备就绪

我们需要在服务器和桌面系统上都安装 Git,不同系统的说明可以在这里找到book.git-scm.com/2_installing_git.html。如果我们使用带有apt-get软件包管理器的 Linux,我们可以执行:

sudo apt-get install git git-core 

如果我们是第一次安装 Git,我们将不得不按照以下方式设置个人信息配置设置:

git config --global user.name "Dave Clements"
git config --global user.email "dave@nodecookbook.com" 

我们将使用我们的login应用程序,在第一个教程中我们将其部署到服务器上。因此,让我们 SSH 到服务器并进入/var/www/login目录。

ssh -l root nodecookbook.com -t "cd /var/www/login; bash" 

由于我们将不会以 root 身份运行我们的应用程序,因此我们将简化事情并将login/app.js中的监听端口更改为8000

app.listen(8000);

如何做...

一旦我们登录到服务器并在login文件夹中安装了 Git(请参阅准备工作),我们说:

git init
git add *
git commit -m "initial commit" 

接下来,我们创建一个裸存储库(它记录了所有更改,但没有实际的工作文件),我们将向其推送更改。这有助于保持一致。

我们将称这个裸存储库为repo,因为这是我们将推送更改的存储库,并且我们将在login文件夹中创建它:

mkdir repo
echo repo > .gitignore
cd repo
git --bare init 

接下来,我们将裸repo连接到login应用程序存储库,并将所有提交推送到repo

cd ..
git remote add repo ./repo
git push repo master 

现在我们将编写一个 Git 挂钩,指示login存储库从裸repo存储库中拉取任何更改,然后在通过远程 Git 推送更新repo时重新启动我们的login应用程序。

cd repo/hooks
touch post-update
chmod +x post-update
nano post-update 

nano中打开文件后,我们编写以下代码:

#!/bin/sh

cd /root/login
env -i git pull repo master

exec forever restart /root/login/app.js

使用Ctrl + O保存我们的挂钩,然后使用Ctrl + X退出。

如果我们对login存储库进行 Git 提交,这两个存储库可能会不同步。为了解决这个问题,我们为login存储库创建另一个挂钩:

#!/bin/sh
git push repo

我们将其存储在login/.git/hooks/post-commit中,并确保使用chmod +x post-commit使其可执行。

我们将通过 SSH 协议远程向repo进行提交。理想情况下,我们希望为 Git 交互创建一个系统用户。

useradd git
passwd git   #set a password

mkdir /home/git
chown git /home/git 

我们还为git用户创建了主目录,以便forever可以轻松存储日志和 PID 文件。我们需要将git设置为login应用程序的所有者,以便我们可以通过 SSH 使用 Git 来管理它:

cd /var/www
chown -R git login 

最后(对于服务器端设置),我们以git用户身份登录并使用forever启动我们的应用程序。

su git
forever start /var/www/login/app.js 

假设我们的服务器托管在nodecookbook.com,我们现在可以在http://nodecookbook.com:8000访问login应用程序。

回到桌面,我们克隆repo存储库:

git clone ssh://git@nodecookbook.com/var/www/login/repo 

这将给我们一个repo目录,其中包含与我们原始的login文件夹完全匹配的所有生成的文件。然后我们可以进入repo文件夹并更改我们的代码(比如,在app.js中更改端口)。

app.listen(9000);

然后我们提交更改并推送到我们的服务器。

git commit -a -m "changed port"
git push 

在服务器端,我们的应用程序应该已自动重新启动,因此我们的应用程序现在是从http://nodecookbook.com:9000而不是http://nodecookbook.com:8000托管的。

它是如何工作的...

我们创建了两个 Git 存储库。第一个是login应用程序本身。当我们运行git init时,.git目录将添加到login文件夹中。git add *添加文件夹中的所有文件,commit -m "initial commit"将我们的添加放入 Git 的版本控制系统中。因此,现在我们的整个代码库都被 Git 识别。

第二个是repo,它是使用--bare标志创建的。这是一种骨架存储库,提供了所有预期的 Git 功能,但缺少实际文件(它没有工作树)。

虽然使用两个存储库可能看起来过于复杂,但实际上大大简化了事情。由于 Git 不允许将推送到当前签出的分支,因此我们必须创建一个单独的虚拟分支,以便我们可以从主分支签出并进入虚拟分支。这会导致 Git 挂钩和重新启动我们的应用程序出现问题。挂钩尝试启动错误的分支的应用程序。分支也很快会不同步,而挂钩只会火上浇油。

由于repo位于login目录中,我们创建一个.gitignore文件,告诉 Git 忽略这个子目录。尽管loginrepo在同一台服务器上,我们将repo添加为remote存储库。这在存储库之间增加了一些必要的距离,并允许我们稍后使用我们的第一个 Git 钩子使loginrepo拉取更改。从repologin的 Git 推送不会导致login更新其工作目录,而从repologin的拉取确实会启动合并。

在我们的remote add之后,我们从主分支(login)向repo执行初始推送,现在它们在同一张乐谱上演奏。

然后我们创建了我们的钩子。

Git 钩子是可执行文件,驻留在存储库的hook文件夹中。有各种可用的钩子(已经在文件夹中,但后缀为.sample)。我们使用了两个:post-updatepost-commit。一个在更新后执行(例如,一旦更改已被拉取并集成到repo中),另一个在提交后执行。

第一个钩子login/repo/hooks/post-update基本上提供了我们的持续部署功能。它使用cd将其工作目录从repo更改为login,并命令git pullgit pull命令前缀为env -i。这可以防止某些 Git 功能出现问题,否则会执行 Git 命令代表repo,无论我们将我们的钩子脚本发送到什么目录。Git 利用$GIT_DIR环境变量将我们锁定到调用钩子的存储库。env -i通过告诉git pull忽略(-i)所有环境变量来处理这个问题。

更新工作目录后,我们的钩子继续调用forever restart,从而使我们的应用程序重新初始化并应用提交的更改。

我们的第二个钩子只是一个填充物,以确保在直接提交到login存储库时代码库的一致性。直接向login目录提交不会更新工作树,也不会导致我们的应用程序重新启动,但loginrepo之间的代码至少会保持同步。

为了限制损害(如果我们曾经受到攻击),我们为处理 SSH 上的 Git 更新创建了一个特定的账户,为其提供一个主目录,接管login应用程序并执行我们应用程序的主要初始化。

一旦服务器配置完成,一切都很顺利。在将repo存储库克隆到我们的本地开发环境后,我们只需进行更改,添加和提交,然后推送到服务器。

服务器接收我们的推送请求,更新repo,启动post-update钩子,使loginrepo拉取更改,之后post-update钩子使用forever重新启动app.js,因此我们有了一个持续部署工作流程。

我们可以从任意位置克隆任意数量的克隆,因此这种方法非常适合于地理位置独立的团队协作项目,无论规模大小。

还有更多...

我们可以通过在 post-update 钩子中使用npm install来避免上传模块。此外,Git 钩子不一定要用 shell 脚本编写,我们可以用 Node 来编写它们!

构建模块依赖关系的更新

一些 Node 模块是纯 JavaScript 编写的,另一些具有 C 或 C++绑定。具有 C 或 C++绑定的模块必须从源代码构建-这是一个特定于系统的任务。除非我们的实时服务器环境与我们的开发环境完全相同,否则我们不应该简单地将为一个系统构建的代码推送到另一个系统上。

另外,为了节省传输带宽并实现更快的同步,我们可以让我们的 Git 钩子安装所有模块(本地绑定和 JavaScript),并让 Git 完全忽略node_modules文件夹。

因此,在我们的本地存储库中,让我们做以下事情:

echo node_modules >> .gitignore 

然后我们将我们裸远程存储库(login/repo/hooks)中的post-update钩子更改为:

#!/bin/sh

cd /root/login

env -i git pull repo master && npm rebuild && npm install

exec forever restart /root/login/app.js

我们已经在git pull行中添加了&& npm rebuild && npm install(使用&&确保它们受益于env -i命令)。

现在,如果我们向package.json添加了一个模块,并执行了git commit -a,然后执行git push,我们的本地repopackage.json推送到远程repo。这将触发post-update挂钩将更改拉入主login存储库,并随后执行npm rebuild(重新构建任何 C / C++依赖项)和npm install(安装任何新模块)。

编写一个用于集成测试的 Node Git 挂钩

持续部署是持续集成的延伸,通常期望对任何代码更改运行彻底的测试套件以进行质量保证。

我们的login应用(作为一个基本的演示站点)没有测试套件(有关测试套件的信息,请参见第九章中,编写自己的 Node 模块),但我们仍然可以编写一个挂钩,以便在将来为login添加任何测试时执行。

此外,我们可以用 Node 编写它,这样做的额外好处是可以跨平台运行(例如在 Windows 上)。

#!/usr/bin/env node

var npm = require("npm");

npm.load(function (err) {
    if (err) { throw err; }

    npm.commands.test(function (err) {
        if (err) { process.exit(1); }       
    });

});

我们将把这段代码放在服务器上的login/repo/hooks/pre-commit中,并使其可执行(chmod +x pre-commit)。

第一行将node设置为脚本解释器指令(就像#!/bin/sh为 shell 脚本设置sh shell 一样)。现在我们进入了 Node 的领域。

我们使用npm的可编程性,加载我们应用的package.json文件,然后运行测试脚本(如果有指定的话)。

然后,我们将以下内容添加到我们的package.json文件中:

{
    "name": "application-name"
  , "version": "0.0.1"
  , "private": true
  , "dependencies": {
      "express": "2.5.5"
    , "jade": ">= 0.0.1"
  },
   "scripts": {
    "test": "node test"
  },
  "devDependencies": {"npm": "1.1.18"}
}

然后执行以下操作:

npm -d install 

现在,每当我们推送到repo时,只有通过测试的更改才会被提交。只要我们有一个良好编写的测试套件,这是保持良好代码的好方法。

提示

对于我们的scripts.test属性,我们使用了node test(就像在第九章中,编写自己的 Node 模块中一样)。然而,我们还可以使用更高级的测试框架,比如 Mocha visionmedia.github.com/mocha/

注意

这个 Node Git 挂钩是根据 Domenic Denicola 的一个 gist(经过许可)进行调整的,可以在gist.github.com/2238951找到。

另请参阅

  • 本章讨论的部署到服务器环境

  • 本章讨论的自动崩溃恢复

  • 创建一个测试驱动的模块 API,在第九章中讨论,编写自己的 Node 模块

  • 本章讨论的使用平台即服务提供商进行托管

使用平台即服务提供商进行托管

Node 的**平台即服务提供商(PaaS)**包含了前三章讨论的所有概念,并将部署简化为一组非常基本但强大的命令。在部署方面,PaaS 可以让我们的生活变得非常简单。只需一个简单的命令,我们的应用就可以部署,另一个命令可以无缝更新和重新初始化。

在这个例子中,我们将学习如何部署到 Nodejitsu,这是领先的 Node 托管平台提供商之一。

做好准备

首先,我们将安装jitsu,Nodejitsu 的部署和应用管理命令行应用程序。

sudo npm -g install jitsu 

在继续之前,我们必须按照以下步骤注册一个帐户:

jitsu signup 

该应用程序将引导我们完成简单的注册过程,并为我们创建一个帐户,我们必须通过电子邮件确认。

提示

Nodejitsu 并不是唯一的 Node PaaS,还有其他类似的平台,如 no.de、Nodester 和 Cloud Foundry,它们遵循类似的流程。

一旦我们收到了我们的电子邮件,我们就可以使用提供的凭证,例如:

jitsu users confirm daveclements _sCjXz46in-6IBpl 

与第一个示例一样,我们将使用第六章中的初始化和使用会话配方中的login应用程序,使用login应用程序。

如何做...

首先,我们进入login文件夹并对package.json进行一些修改:

{
  "name": "ncb-login",
  "version": "0.0.1",
  "private": true,
  "dependencies": {
    "express": "2.5.5",
    "jade": ">= 0.0.1"
  },
  "subdomain": "login",
  "scripts": {
    "start": "app.js"
  },
  "engines": {
    "node": "0.6.x"
  }
}

现在我们部署!

Jitsu deploy

如果我们在http://login.nodejitsu.com或者http://login.jit.su导航到我们指定的子域,我们将看到我们的login应用程序(如果子域不可用,jitsu将建议替代方案)。

它是如何工作的...

我们对package.json进行了一些修改。我们的应用程序名称是唯一必须直接编辑package.json进行的更改。其他添加可能已经由jitsu可执行文件代表我们完成。设置应用程序的名称很重要,因为在jitsu中,应用程序是通过其名称进行管理的。

如果我们没有将subdomain, scriptsengines属性附加到package.json中,当我们运行jitsu deploy并由jitsu代表我们重新生成package.json时,jitsu将要求我们提供详细信息。

subdomain指定了nodejistu.com的标签前缀,我们从中托管我们的应用程序(例如,login.nodejitsu.com)。scripts,带有start子属性,通知 Nodejitsu 我们的启动脚本,启动应用程序的文件。engines定义了我们的应用程序设计的 Node 的哪些版本。

还有更多...

让我们看看如何通过自定义域名访问我们的 Nodejitsu 应用,并通过jitsu可执行文件为其提供数据库后端。

为 Nodejitsu 应用分配自定义域名

为了为我们的应用程序准备通过自定义域名提供服务,我们对package.json进行了修改,如下所示:

//prior package.json data
 "subdomain": "login",
  "domains": "login.nodecookbook.com",
  "scripts": {
    "start": "app.js"
  },
//rest of package.json data

然后我们使用jitsu推送我们的更改,如下所示:

jitsu apps update ncb-login 

现在应用程序已准备好通过login.nodecookbook.com接收流量,但在流量到达之前,我们必须将我们的域的 A 记录与 Nodejitsu 的 A 记录匹配。

我们可以使用dig(或类似的命令行应用程序)获取当前的 Nodejitsu A 记录列表:

dig nodejitsu.com 

更改 A 记录的过程取决于我们的域名提供商。通常可以在提供商的控制面板/管理区域的 DNS 区域找到它。

使用 jitsu 为数据库提供服务

在第六章的最后一个配方中,使用 Express 加速开发,我们构建了一个使用 MongoDB 支持的 Express 应用程序。现在我们将使用 Nodejitsu 将profiler应用程序上线,并利用jitsu的数据库提供功能。

所以让我们为profiler数据库提供一个 Mongo 数据库,如下所示:

jitsu databases create mongo profiler 

jitsu将通过第三方数据库 PaaS 提供商(在 Mongo 的情况下,PaaS 提供商是 MongoHQ)为我们提供数据库。输出的倒数第二行为我们提供了新数据库的 MongoDB URI,看起来像以下代码:

info: Connection url: mongodb://nodejitsu:14dce01bda24e5fe53bbdaa8f2f6547b@flame.mongohq.com:10019/nodejitsudb169742247544

因此,我们将profiler/tools/prepopulate.js的第二行更新为:

client = mongo.db('mongodb://nodejitsu:14dce01bda24e5fe53bbdaa8f2f6547b@flame.mongohq.com:10019/nodejitsudb169742247544'),

然后我们从profiler/tools文件夹运行它:

node prepulate.js 

这将填充我们的远程数据库与配置文件和登录数据。

我们在另外两个地方profiler/profiles.jsprofiler/login/login.js中更新了我们的数据库 URI,在这两个地方,第二行被修改为:

db = mongo.db('mongodb://nodejitsu:14dce01bda24e5fe53bbdaa8f2f6547b@flame.mongohq.com:10019/nodejitsudb169742247544'),

最后,我们输入以下内容:

jitsu deploy 

jitsu将要求我们设置某些设置(子域,scripts.startengines),我们可以只需按下Enter并使用默认设置(除非profiler.nodejitsu.com已被占用,这种情况下我们应该选择不同的 URL)。

然后jitsu将部署我们的应用程序,我们应该能够在profiler.nodejitsu.com上访问它。

另请参阅

  • 在本章讨论的部署到服务器环境

  • 在本章讨论的自动崩溃恢复

  • 在本章讨论的持续部署