Xiaopei's DokuWiki

These are the good times in your life,
so put on a smile and it'll be alright

User Tools

Site Tools


it:nodejs

Table of Contents

node.js

node 开发时需要打开的文档:

通信协议相关开发

前端开发

node 开发中的常见问题:

  • cb() 后未 return
  • Error: read ECONNRESET, 未处理 socket 的 on('error'). 应该:
    1. 绑定 on('error') 记录错误
    2. error 后会自动调用 close, 在 on('close') 中处理连接关闭
  • 异步/同步同时操作数组元素, 异步操作中元素不符合期望 → 使用临时变量保存数组元素
  • event 事件机制最适合处理同一个对象上反复发生的事情, 如 – keyup、touchstart 等等. 当碰到异步请求成功/失败的时候, 用 promise

tips

  • ashtuchkin/iconv-lite iconv 字符编码处理 锟斤拷 gbk gb2312 gb18030
    var iconv = require('iconv-lite');
     
    // Convert from an encoded buffer to js string.
    str = iconv.decode(new Buffer([0x68, 0x65, 0x6c, 0x6c, 0x6f]), 'win1251');
     
    // Convert from js string to an encoded buffer.
    buf = iconv.encode("Sample input string", 'win1251');
     
    // Check if encoding is supported
    iconv.encodingExists("us-ascii")
     
    // 也可以扩展 node 自身的编码
    // After this call all Node basic primitives will understand iconv-lite encodings.
    iconv.extendNodeEncodings();
  • cli
    • 启动服务:
      pm2 -i 0 start server.js --name "my-server" -e my.err -o my.log
    • Startup script generation
      $ pm2 startup
      # render startup-script
      $ pm2 save
  • npm 安装技巧
    # 不安装 devDependencies
    $ npm i --production
     
    # 不安装 optionalDependencies
    $ npm i --no-optional
  • npm shrinkwrap: Lock down dependency versions
    • To shrinkwrap an existing package:
      1. Run npm install in the package root to install the current versions of all dependencies.
      2. Validate that the package works as expected with these versions.
      3. Run npm shrinkwrap, add npm-shrinkwrap.json to git, and publish your package.
    • To add or update a dependency in a shrinkwrapped package:
      1. Run npm install in the package root to install the current versions of all dependencies.
      2. Add or update dependencies. npm install each new or updated package individually and then update package.json. Note that they must be explicitly named in order to be installed: running npm install with no arguments will merely reproduce the existing shrinkwrap.
      3. Validate that the package works as expected with the new dependencies.
      4. Run npm shrinkwrap, commit the new npm-shrinkwrap.json, and publish your package.
    • You can use npm-outdated(1) to view dependencies with newer versions available.
  • 图片处理
    • node-canvas 不支持 node > 0.10 2)
    • aheckmann/gm GraphicsMagick for node
  • node 没有太合适的 captcha 库,歪果仁用 recaptcha 就行了
  • nvm
    • nvm 比 n 好用,star 更多,且 n 有可能切换不了版本
    • 但 nvm 安装不同版本的 node 时,默认安装到 $HOME
    • 若要全局安装 node,需在当前用户下执行 3)
      n=$(which node);n=${n%/bin/node}; chmod -R 755 $n/bin/*; sudo cp -r $n/{bin,lib,share} /usr/local

服务器上还是用固定版本更可靠,可参考:Node.js v0.12, io.js, and the NodeSource Linux Repositories | NodeSource - Enterprise Node.js Training, Support, Software & Consulting, Worldwide

ansible playbook 如下(@todo 没有处理 curl 多次运行)

roles/node/tasks/main.yml
---
- name: Add node 0.12 source
  shell: curl -sL https://deb.nodesource.com/setup_0.12 | sudo bash -
- name: Install node 0.12
  apt: name=nodejs=0.12 state=present
  • rest/restful HTTP client
    • node.js 没有原生的 restangular 式的 rest client,有个 grahamj/node-restangular 但只是加载 angular、restangular 再把 restangular 暴露出来,效率肯定低
    • 一般用 visionmedia/superagent,superagent 的特点为链式调用
       request
         .post('/api/pet')
         .send({ name: 'Manny', species: 'cat' })
         .set('X-API-Key', 'foobar')
         .set('Accept', 'application/json')
         .end(function(res){
           if (res.ok) {
             alert('yay got ' + JSON.stringify(res.body));
           } else {
             alert('Oh no! error ' + res.text);
           }
         });
    • 不过 superagent 不是也不准备做 promise,promise 可用一个插件 KyleAMathews/superagent-bluebird-promise
  • md5
    require("crypto")
      .createHash("md5")
      .update("123456")
      .digest("hex");
    // 'e10adc3949ba59abbe56e057f20f883e'
  • 对唯一性没有严格要求的随机 id
    Math.random().toString(36).substring(7);
    // 'v1l9m3x47vi'
  • string repeat
    // python: str * times
    // php: str_repeat(str, times)
    new Array(times + 1).join(str);
  • string pad
    // left pad
    ("0000" + str).slice(-4)
  • 如果在使用 fs 时遇到 Error: EMFILE, too many open files 的问题,应先排查原因 (见此),再考虑使用 graceful-fs
  • cnpm
    alias cnpm="npm --registry=http://registry.npm.taobao.org \
    --cache=$HOME/.npm/.cache/cnpm \
    --disturl=http://dist.cnpmjs.org \
    --userconfig=$HOME/.cnpmrc"
  • extend util Node.js
    var util = require("util");
    var events = require("events");
     
    function MyStream() {
        events.EventEmitter.call(this);
    }
     
    // 必须先 inherits
    util.inherits(MyStream, events.EventEmitter);
     
    // 再定义自己的方法
    MyStream.prototype.write = function(data) {
        this.emit("data", data);
    }
     
    var stream = new MyStream();
     
    console.log(stream instanceof events.EventEmitter); // true
    console.log(MyStream.super_ === events.EventEmitter); // true
     
    stream.on("data", function(data) {
        console.log('Received data: "' + data + '"');
    })
    stream.write("It works!"); // Received data: "It works!"
  • 获得随机 TCP 端口
    var net = require('net');
    var portrange = 45032;
     
    var getPort = function (cb) {
        var port = portrange + Math.floor(Math.random() * 100);
     
        var server = net.createServer();
        server.listen(port, function (err) {
            server.once('close', function () {
                cb(port);
            });
            server.close();
        });
        server.on('error', function (err) {
            getPort(cb);
        });
    }
     
    getPort(function(port) {console.log(port);});
  • async 的 collections 系列方法 不能对 object 使用, 只能对 array 使用. 针对 object 可以用: async.each(_.keys(obj), iterator, callback)
  • node 中可使用 require('vm'); 来让一段 js runInNewContext Executing JavaScript
  • 使用 Docco 可以实现 underscore.js 的 Annotated Source(docco 就是 underscore 的作者写的)。从 underscore 的 源码 可以看出 docco 使用 // 作为注释开头,注释中可使用 markdown
  • 使用 n 可做 node 的版本管理
    $ npm i -g n
     
    # 查看可用的最新稳定版本
    $ n --stable
    0.10.33
     
    # 切换到最新稳定版本(如果没有,会安装)
    $ n stable
     
         install : v0.10.33
           mkdir : /usr/local/n/versions/0.10.33
           fetch : http://nodejs.org/dist/v0.10.33/node-v0.10.33-darwin-x64.tar.gz
       installed : v0.10.33
     
    $ node -v
    v0.10.33
     
    # 查看可用的最新不稳定版本
    $ n --latest
    0.11.14
     
    # 切换到最新不稳定版本(如果没有,会安装)
    $ n latest
     
         install : v0.11.14
           mkdir : /usr/local/n/versions/0.11.14
           fetch : http://nodejs.org/dist/v0.11.14/node-v0.11.14-darwin-x64.tar.gz
       installed : v0.11.14
     
    $ node -v
    v0.11.14
     
    # 从已安装的版本(不包括安装 n 前的 node 版本)中,选一个版本
    $ n
        0.10.33
        0.11.12
      ο 0.11.14
  • mbostock/queue,一个简单的队列,可随时添加任务。但它的 .await()/.awaitAll() 方法不正常
    var queue = require('./queue'),
        q = queue(),
        t = function(cb) {
            console.log(new Date());
            cb();
        };
     
    q.defer(t);
     
    setTimeout(function() {
        q.defer(t);
    }, 2000);
     
    // output:
    // Tue Nov 11 2014 12:22:13 GMT+0800 (CST)
    // Tue Nov 11 2014 12:22:15 GMT+0800 (CST)
  • 按 github 的分支安装
    • npm i --save git://github.com/<user>/<project>.git#<branch>
  • 格式化数字、钱

pdf

PDFKit 可以用 node 或浏览器制作 pdf。

但是 CJK 字符必须自行载入字体才能输出,而字体载入现在还有“不支持包含 127 个字以上的字体”的问题,有一种解决方法是将字体多次载入,但这还得知道每个字在字体文件的哪个位置(0~127,还是 128~255),应该是 mission impossible 了。字体拆分可能能通过 aui/font-spider 字蛛 或 ecomfe/fontmin 实现,但没研究(好像 fontmin 更合适,font-spider 可能是 fontmin 的 web 专有用途的包装,Fontmin 快速指南 | EFE Tech)。

详见:Font Subset Failure with Multiple Languages · Issue #244 · devongovett/pdfkit

node.js v4.0.0

classes - 各种 ‘类’,再也无需用 CoffeeScript 的语法糖写类了

class SkinnedMesh extends THREE.Mesh {
  constructor(geometry, materials) {
    super(geometry, materials);
 
    this.idMatrix = SkinnedMesh.defaultMatrix();
    this.bones = [];
    this.boneMatrices = [];
    //...
  }
  update(camera) {
    //...
    super.update();
  }
  get boneCount() {
    return this.bones.length;
  }
  set matrixType(matrixType) {
    this.idMatrix = SkinnedMesh[matrixType]();
  }
  static defaultMatrix() {
    return new THREE.Matrix4();
  }
}  

typed arrays - 类型数组

用来做二进制处理:Int8Array、Uint8Array、Int16Array、Uint16Array、Int32Array、Uint32Array、Float32Array、Float64Array

generators - 未来的.js 代码中将有无数生成器,不学一点就看不懂 JS 代码了哦

var fibonacci = {
  [Symbol.iterator]: function*() {
    var pre = 0, cur = 1;
    for (;;) {
      var temp = pre;
      pre = cur;
      cur += temp;
      yield cur;
    }
  }
}
 
for (var n of fibonacci) {
  // truncate the sequence at 1000
  if (n > 1000)
    break;
  console.log(n);
}
 
// The generator interface is (using TypeScript type syntax for exposition only):
 
interface Generator extends Iterator {
    next(value?: any): IteratorResult;
    throw(exception: any);
}

collections - 集合、映射、弱集合、弱映射

Map + Set + WeakMap + WeakSet

// Sets
var s = new Set();
s.add("hello").add("goodbye").add("hello");
s.size === 2;
s.has("hello") === true;
 
// Maps
var m = new Map();
m.set("hello", 42);
m.set(s, 34);
m.get(s) == 34;
 
// Weak Maps
var wm = new WeakMap();
wm.set(s, { extra: 42 });
wm.size === undefined
 
// Weak Sets
var ws = new WeakSet();
ws.add({ data: 42 });
// Because the added object has no other references, it will not be held in the set

arrow functions - 箭向函数

array function 是用来简化匿名函数写法的,没法使用 array function 时命名

// Expression bodies
var odds = evens.map(v => v + 1);
var nums = evens.map((v, i) => v + i);
var pairs = evens.map(v => ({even: v, odd: v + 1}));
 
// Statement bodies
nums.forEach(v => {
  if (v % 5 === 0)
    fives.push(v);
});
 
// Lexical this
var bob = {
  _name: "Bob",
  _friends: [],
  printFriends() {
    this._friends.forEach(f =>
      console.log(this._name + " knows " + f));
  }
}

block scoping - 使用 let 、const 作用域,块辖域

function f() {
  {
    let x;
    {
      // okay, block scoped name
      const x = "sneaky";
      // error, const
      x = "foo";
    }
    // error, already declared in block
    let x = "inner";
  }
}

template strings - 模板字串

// Basic literal string creation
`In JavaScript '\n' is a line-feed.`
 
// Multiline strings
`In JavaScript this is
 not legal.`
 
// String interpolation
var name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`
 
// Construct an HTTP request prefix is used to interpret the replacements and construction
GET`http://foo.org/bar?a=${a}&b=${b}
    Content-Type: application/json
    X-Credentials: ${credentials}
    { "foo": ${foo},
      "bar": ${bar}}`(myOnReadyStateChangeHandler);

promises - 用标准化了的方法进行延迟和异步计算

function timeout(duration = 0) {
    return new Promise((resolve, reject) => {
        setTimeout(resolve, duration);
    })
}
 
var p = timeout(1000).then(() => {
    return timeout(2000);
}).then(() => {
    throw new Error("hmm");
}).catch(err => {
    return Promise.all([timeout(100), timeout(200)]);
})

symbols - 唯一的、不可修改的数据

var MyClass = (function() {
 
  // module scoped symbol
  var key = Symbol("key");
 
  function MyClass(privateData) {
    this[key] = privateData;
  }
 
  MyClass.prototype = {
    doStuff: function() {
      ... this[key] ...
    }
  };
 
  return MyClass;
})();
 
var c = new MyClass("hello")
c["key"] === undefined
 

semver

  • semver 的标准中,定义了 MAJOR.MINOR.PATCH,但是并没定义修饰版本的符号
  • npm 对 semver 的实现中,定义了 ~, ^ 这些 Ranges 符号
    • node.js v0.10.26 (2014/02/19 UTC) 升级时,npm: upgrade to 1.4.3,此后 npm i –save 默认使用 ^,但之前的 npm 版本不支持 ^,便会出现 No compatible version found
    • ~1.2.3 := >=1.2.3-0 <1.3.0-0 “Reasonably close to 1.2.3”.
    • ^1.2.3 := >=1.2.3-0 <2.0.0-0 “Compatible with 1.2.3”.

将 node 项目安装为 service

如果某 node.js 项目只要 npm i -g 就能成为一个 linux 服务就太好了… LOL

安装

参考 npm-scripts

npm 的 package.json 提供了 “scripts”, 其中包括 {pre,post}?{install,uninstall,update} 等脚本, 可控制安装的流程.

但对于这些 安装 相关的脚本, npm 说了:

  1. NOTE: INSTALL SCRIPTS ARE AN ANTIPATTERN
  2. Use a .gyp file for compilationsr, and prepublish for anything else (比如 coffee script 转为 js)
  3. The only valid use of install or preinstall scripts is for compilation which must be done on the target architecture. In early versions of node, this was often done using the node-waf scripts, or a standalone Makefile, and early versions of npm required that it be explicitly set in package.json. This was not portable, and harder to do properly.

那 gyp 到底适不适合做 cp etc/* /etc 这种工作呢? 好像不适合, gyp 是 make 工具, source & target

hij1nx/complete 可以做 bash-complete, 但是必须修改用户的 .bashrc. 所以如果命令行工具 (比如 jitsu, express, yo) 能提供 compeletion, 则很可能修改了 /etc, 便可做参考. 但检查过上列命令, 都没提供 completion.

那怎么办呢? 找找有没有 npm i -g 就能变服务的 package, 看看他们怎么做… FIXME

服务相关

  • upstart
    #!upstart
    description "Bar"
    author      "Xiaopei Li"
    
    start on runlevel [2345]
    stop on runlevel [016]
    
    # respawn 默认会尝试重启服务 10 次. 
    # 但如果需要控制内存使用, 可使用 monit, 或用 cron 定期重启
    # @todo 增加 respawn 达到最大重试后, 邮件通知管理员的功能
    respawn
    
    ## 生产环境
    env bin="/home/foo/bar/bin/index.js"
    
    exec $bin
  • logrotate
    /home/foo/bar/logs/*.log {
      weekly
      rotate 12
      compress
      notifempty
      missingok
      copytruncate
    }

buffer

  • buffer 比较
    // 使用 buffertools
    bt = require('buffertools'),
     
    // 使用 equals 相等时返回 true
    bt.equals(new Buffer('hehe'), new Buffer('hehe')).should.be.true;
     
    // 使用 compare 相等时返回 0, 否则返回差值
    bt.compare(new Buffer('hehe'), new Buffer('hehe')).should.equal(0);

restify

如果要做 RESTFUL 的 API 服务器, 不需页面渲染, 就用 restify

redis

var BPromise = require('bluebird'),
    redis = require('redis'),
    server = "127.0.0.1",
    port = 6379,
    options = {},
    redisClient = redis.createClient(port, server, options);
 
 
// 1. 将 redisClient 由 callback 转为 promise
// 2. 调用 promise 方法都需加 Async 后缀
// 3. 虽然 promisifyAll 可加 {suffix: 'Promise'},但无法将 suffix 设为空字符串
BPromise.promisifyAll(redisClient);
 
// 根据目前观察,不需要在结束 redisClient 使用时 quit()。node 进程启动
// 后,会一直保持着一个并复用这个 redis 连接,实验如下:
//
// $ sudo redis-cli
//
// 1. node 进程刚启动:
// 127.0.0.1:6379> client list
// id=38 addr=10.0.2.2:56284 fd=7 name= age=3 ...
//
// 2. 做了若干次请求后
// 127.0.0.1:6379> client list
// id=38 addr=10.0.2.2:56284 fd=7 name= age=11 ...
//
// 一直是同一个连接
 
module.exports = redisClient;
 
 
//////////////////////////
 
redisClient.incrAsync('hehe')
    .then(function(result) {
        console.log(result);
        res.send(200, result);
    })
    .catch(function(error) {
        console.error(error)
        res.send(500, error);
    });

(tcp) net

对于已建立连接的 socket, 如果网线断了, socket 会触发 error/close 事件么?

不会! 那怎样才能检测到此类 close 呢? 如下:

  1. 可以用 socket 的 readable/writable 检测网络情况么?
  2. 不能! socket 的 readable/writablenew Socket(opts)opts, 是用户设置的属性, 而不是系统在 socket 状态时修改的变量
  3. 如果 socket 只接受数据, 则可 socket.setTimeout(1000, function() {this.close();});
  4. 一定要注意 socket.setTimeout() 的参数是时间在前
  5. 但如果应用一直向 socket 发数据, 则 socket 中一直有流量, 就不会达到 timeout, 故此方法无效
  6. 另外, 有 socket.setKeepAlive(true) 方法, 这是 TCP 专门为网络断线提供的检测机制 (默认为关), 看起来就是它啊! LOL
    1. TCP keepalive overview: (one target is) Preventing disconnection due to network inactivity
  7. 不过测试打开后还是有问题, keepAlive 根本不触发啊!!! m(
  8. 查了更多资料, 都说 keepalive 不可靠, 应用自己实现检测更靠谱
      1. it can take up to 30 minutes for transmission retries to time out before the keepalive timer starts
      2. The usual recommendation is to implement communication timeouts at the application level rather than use TCP-based keepalive anyway
      1. KeepAlive机制很多情况无法检测出来,如网络连接被软件禁用等,不够可靠,网络状态复杂的情况下这种情况尤其严重
      2. 自己实现的心跳机制通用,可以无视底层的UDP或TCP协议
  9. 而实现方法可以是 在 socket 接收到数据后, 设置过期时间

crypto

以下使用 openssl_* 的 php 代码:

<?php
const IV = "\0\0\0\0\0\0\0\0";
 
$key = 'jnramone';
 
$text = "hehe+<>哈哈";
echo $text ."\n";
// hehe+<>哈哈
 
$code = openssl_encrypt($text, 'des', $key, FALSE, IV);
echo $code . "\n";
// igHUhk4mEOb2R65x8pFL8g==
 
$decode = openssl_decrypt($code, 'des', $key, FALSE, IV);
echo $decode . "\n";
// hehe+<>哈哈

等同于以下使用 crypto 的 node.js 代码:

var IV = "\0\0\0\0\0\0\0\0";
 
// des 需要的 key 是 8 位
// key 是个 buffer 可能无法原文 ascii 传输, 所以用 base64
var key = new Buffer(8);
key.write(key_base64, 0, 8, 'base64');
 
// 加密
var cipher = crypto.createCipheriv("des", key, IV);
var code = cipher.update(raw_data, 'binary', 'base64');
code += cipher.final('base64');
 
// 解密
var decipher = crypto.createDecipheriv("des", key, IV);
var raw_data = decipher.update(code, 'base64', 'utf8');
raw_data += decipher.final('utf8');

关于 json 中对长字符 utf8 的 escape

// json spec (RFC 4627) 中, json does not demand the conversion
// from unicode characters to escape-sequences (\u0000)
// 但某些语言 (如 php) 中可能默认会 escape, 可如下处理
 
// 序列化
raw_data = raw_data.replace(/[\u007f-\uffff]/g, function(c) {
    return '\\u'+('0000'+c.charCodeAt(0).toString(16)).slice(-4);
});
 
// 恢复
raw_data = raw_data.replace(/\\u([\d\w]{4})/gi, function (match, grp) {
    return String.fromCharCode(parseInt(grp, 16));
});

PKCS5Padding

PKCS5Padding - swordinhand - ITeye技术网站

  • 缺几位,补几个 0X
  • 正好 8 位,还要补 8 个 08

node 的 crypto 里有相关的选项 setAutoPadding

socket.io

基础

//// node 端
io.on('connection', function(socket){
  console.log('a user connected');
  socket.on('disconnect', function(){
    console.log('user disconnected');
  });
});
 
// chat message 为自定义事件
socket.on('chat message', function(msg){
  // 包括 this socket
  io.emit('chat message', msg);
 
  // 不包括 this socket
  // io.broadcast.emit('chat message', msg);
});
 
 
//// html 端
<script>
  var socket = io();
 
  $('form').submit(function(){
    socket.emit('chat message', $('#m').val());
    $('#m').val('');
    return false;
  });
 
  socket.on('chat message', function(msg){
    $('#messages').append($('<li>').text(msg));
  });
</script>

The main idea behind Socket.IO is that you can send and receive any events you want, with any data you want. Any objects that can be encoded as JSON will do, and binary data is supported too.

几种 emit:

  • html
    • socket.emit() 只可向服务器触发事件
  • node.js
    • io.emit() 对所有 sockets 广播
    • socket.emit() 单独对该 socket 发送
    • socket.broadcast.emit() 对除该 socket 外的 sockets 广播
    • socket.volatile.emit() 在消息非常频繁的情况下, 如果消息不丢掉几条也没事, 则可以用 volatile 方式 emit
    • emit() 默认是不期待回复的, 如果需要回复, 则可在 emit 和 on 中增加 cb
      • html: socket.emit(event, data, function(result) {})
      • node: socket.on(event, function(data, response) {response(result)})

namespace

因为 socket.io 是绑在 server 而非 route 上的, 所以, 实际应用中, 应使用 namespace 来分割一个系统的多个 socket.io 应用.

app.js
var io = require('socket.io').listen(80);
var chat = io
  .of('/chat')
  .on('connection', function (socket) {
    socket.emit('a message', {
        that: 'only'
      , '/chat': 'will get'
    });
    chat.emit('a message', {
        everyone: 'in'
      , '/chat': 'will get'
    });
  });
 
var news = io
  .of('/news')
  .on('connection', function (socket) {
    socket.emit('item', { news: 'item' });
  });
index.html
<script>
  var chat = io.connect('http://localhost/chat')
    , news = io.connect('http://localhost/news');
 
  chat.on('connect', function () {
    chat.emit('hi!');
  });
 
  news.on('news', function () {
    news.emit('woot');
  });
</script>

Node.js 的 GC 机制

参考

grunt 和 gulp

gulp.task('js', function () {
   return gulp.src('js/*.js')
      .pipe(jshint())
      .pipe(jshint.reporter('default'))
      .pipe(uglify())
      .pipe(concat('app.js'))
      .pipe(gulp.dest('build'));
});

NeDB

NeDB (Node embedded database)

如果要在资源紧缺的设备 (pi/bbb) 上使用数据库: mongodb 占用资源高, 效率不高; 纯文件储存操作麻烦. 此时就适合用 NeDB (Node embedded database).

NeDB 的特点包括:

  1. 纯 JS, 无操作系统服务的依赖
  2. 数据可持久化 (而非纯内存 DB)
  3. API 类似 mongodb
  4. 性能较同类产品 (taffydb, 已不更新) 更好

tips

  • 删除时必须加 {multi: true} 才会删除匹配的多条数据
    People.remove({}, {multi: true}); // 删除所有用户
  • 对象的属性 搜索, 和按 对象数组 成员的属性搜索, 语法一样
    // { _id: 'id2', planet: 'Earth', system: 'solar', inhabited: true, humans: { genders: 2, eyes: true } }
    // { _id: 'id5', completeData: { planets: [ { name: 'Earth', number: 3 }, { name: 'Mars', number: 2 }, { name: 'Pluton', number: 9 } ] } }
     
     
    // Use the dot-notation to match fields in subdocuments
    db.find({ "humans.genders": 2 }, function (err, docs) {
      // docs contains Earth
    });
     
    // Use the dot-notation to navigate arrays of subdocuments
    db.find({ "completeData.planets.name": "Mars" }, function (err, docs) {
      // docs contains document 5
    });

promise

目前的经验 Promise 主要用在 request 上, timeout 的处理比较方便, 复杂的异步流程控制还是用 async — DokuWiki Administrator 2014/05/11 08:24

基础用法, 取代 cb(err, res)

The core idea behind promises is that a promise represents the result of an asynchronous operation. A promise is in one of three different states:

  1. pending - The initial state of a promise.
  2. fulfilled - The state of a promise representing a successful operation.
  3. rejected - The state of a promise representing a failed operation.

Once a promise is fulfilled or rejected, it is imutable (i.e. it can never change again).

var Promise = require('promise');
var fs = require('fs');
 
/// 以下是使用 promise 的三种方法
 
/// 1. 普通的 promise 嵌套 promise
function readFile(filename, options) {
    return new Promise(function (fulfill, reject) {
	fs.readFile(filename, options, function (err, res) {
	    if (err) reject(err);
	    else fulfill(res);
	});
    });
}
 
function readJSON(filename) {
    return new Promise(function (fulfill, reject) {
	readFile(filename, 'utf8').done(function (res) {
	    try {
		fulfill(JSON.parse(res));
	    }
	    catch (ex) {
		reject(ex);
	    }
	}, reject);
    });
}
 
/// 2. promise 提供了 then, 来简化 "promise 嵌套 promise". causes errors to bubble up the stack by default. 
function readFile(filename, options) {
    return new Promise(function (fulfill, reject) {
	fs.readFile(filename, options, function (err, res) {
	    if (err) reject(err);
	    else fulfill(res);
	});
    });
}
 
function readJSON(filename) {
    return readFile(filename, 'utf8').then(JSON.parse);
}
 
 
/// 3. Promise 对于 node.js 提供了一些扩展, 
///    可将 foo(cb(err, res)) 式的函数转换
///    为 promise 式
var readFile = Promise.denodeify(require('fs').readFile);
// now `readFile` will return a promise rather than expecting a callback
 
function readJSON(filename, callback){
  // If a callback is provided, call it with error as the first argument
  // and result as the second argument, then return `undefined`.
  // If no callback is provided, just return the promise.
  return readFile(filename, 'utf8').then(JSON.parse).nodeify(callback);
}
 
 
/// 使用
readJSON('package.json').done( function(res) {
    console.log('res', res);
}, function(err) {
    console.log('err', err);
});

生成 Promise 的其他方法 Generators

将任意值转换成 一定 fulfill 的 Promise (不知道有什么用):

var value = 10;
var promiseForValue = Promise.resolve(value);

如果 Promise 中有 sync-函数, 可能 throw 错误. 为了外层同一使用 promise 接口, 可 try…catch (e){Promise.reject(e)}. 这样就一定 reject:

var rejectedPromise = Promise.reject(new Error('Whatever'));

对数组 map all 执行:

function readJsonFiles(filenames) {
  // N.B. passing readJSON as a function, not calling it with `()`
  return Promise.all(filenames.map(readJSON));
}
readJsonFiles(['a.json', 'b.json']).done(function (results) {
  // results is an array of the values stored in a.json and b.json
}, function (err) {
  // If any of the files fails to be read, err is the first error
});

race, 简化 timeout 处理 (如对于 request-response 中的 request 方法), 但我对比后觉得不用 race 更清晰, 不用 race 的例子如下, 用 race 的可以看文档:

var Promise = require('promise');
 
var req;
 
/** 执行业务逻辑的函数 */
function mockRequest(text) {
    return new Promise(function (fulfill, reject) {
        req = { // 模拟一个 request-response
            text: text,
            fulfill: fulfill,
            reject: reject
        };
    });
}
 
/** 增加 timeout 功能的函数 */
function timeout(promise, time) {
 
    /** 
     包装原 promise, 增加一个 timeout 的判断.
     Promise 的机制是 fullfill/reject 只会
     执行一次 (imutable), 所以可放心使用.
     */
    return new Promise(function(fulfill, reject) {
        promise.then(fulfill, reject);
        setTimeout(function() {
            reject(new Error('timeout'));
        }, time);
    });
}
 
/** request */
timeout(mockRequest('hehe'), 1000).done(function(res) {
    console.log('res', res);
}, function(err) {
    console.log('err', err);
});
 
/** response */
setTimeout(function() {
    req.fulfill(req.text);
    // req.reject(new Error('request errored'));
}, 2000);
 
// 只会输出一次

generator

另外 promise 还支持 es6/harmony 的 generator, 暂未学习, 详见 Generators… 和 Koa - next generation web framework for node.js

feedparser

features

  • 将不同协议/版本的 feed 的 meta 和 articles 解析为统一的格式,如 meta.description
  • 但同时也保留原协议的特定数据,如 meta['atom:subtitle']
  • 所有统一属性初始为 null (or empty arrays or objects for certain properties)。4)
  • 所有属性名(无论是统一属性,还是协议特定属性)都用小写字母5)),更易用。
  • meta 的 title 和 description,article 的 title,都做了 HTML escape。raw data 见 meta['atom:subtitle']['#']

List of meta properties

  • title
  • description
  • link (website link)
  • xmlurl (the canonical link to the feed, as specified by the feed)
  • date (most recent update)
  • pubdate (original published date)
  • author
  • language
  • image (an Object containing url and title properties)
  • favicon (a link to the favicon – only provided by Atom feeds)
  • copyright
  • generator
  • categories (an Array of Strings)

List of article properties

  • title
  • description (frequently, the full article content)
  • summary (frequently, an excerpt of the article content)
  • link
  • origlink (when FeedBurner or Pheedo puts a special tracking url in the link property, origlink contains the original link)
  • date (most recent update)
  • pubdate (original published date)
  • author
  • guid (a unique identifier for the article)
  • comments (a link to the article's comments section)
  • image (an Object containing url and title properties)
  • categories (an Array of Strings)
  • source (an Object containing url and title properties pointing to the original source for an article; see the RSS Spec for an explanation of this element)
  • enclosures (an Array of Objects, each representing a podcast or other enclosure and having a url property and possibly type and length properties)
  • meta (an Object containing all the feed meta properties; especially handy when using the EventEmitter interface to listen to article emissions)

多核 / 多进程 / child process / cluster / forever monitor

如何开启监听同一端口的多进程

句柄传递, 见 深入浅出 node.js » 9.2.3

child.send(message, [sendHandle])

forever monitor 也对 child 暴露了 send 方法

node.js 的 sendHandle 同时支持 send socket. 当 master 要监听多端口, 或要更好的控制重复连接时, master send socket 比 send server 更合适.

使用 cp 或 forever-monitor 要注意:

  • 一般情况下, 主进程被 kill 时, 使用 child process fork 或 forever-monitor 开启的子进程不会关闭!
  • 但我发现如果在 express 中, 主进程被 kill 时, (好像) 子进程也会被 kill. 暂未查到 express 是如何实现 kill 子进程的.

JSONRPC

对比若干模块后, jayson 是一个比较好的 JSONRPC 客户端及服务端的模块.

普通用法:

/** Server */
var jayson = require(__dirname + '/../..');
 
// create a server
var server = jayson.server({
  add: function(a, b, callback) {
    callback(null, a + b);
  }
});
 
// Bind a http interface to the server and let it listen to localhost:3000
server.http().listen(3000);
/** Client */
var jayson = require(__dirname + '/../..');
 
// create a client
var client = jayson.client.http({
  port: 3000,
  hostname: 'localhost'
});
 
// invoke "add"
client.request('add', [1, 1], function(err, error, response) {
  if(err) throw err;
  console.log(response); // 2!
});

server 端还可作为 connect/express 的中间件:

var jayson = require(__dirname + '/../..');
var connect = require('connect');
var app = connect();
 
var server = jayson.server({
  add: function(a, b, callback) {
    callback(null, a + b);
  }
});
 
// parse request body before the jayson middleware
app.use(connect.bodyParser());
app.use(server.middleware());
 
app.listen(3000);

exports/module.exports

Node.js Module – exports vs module.exports

按我的理解:

  • 在一个模块中, exports 可多次使用, exports 多个方法
    var PI = Math.PI;
    exports.area = function (r) {
        return PI * r * r;
    };
    exports.circumference = function (r) {
        return 2 * PI * r;
    };
  • 而 module.exports 只能使用一次, 一般用于 exports 一个 “类”
    // module.exports
    module.exports = function(name, age) {
        this.name = name;
        this.age = age;
        this.about = function() {
            console.log(this.name +' is '+ this.age +' years old');
        };
    };

日志 logging log

成型的应用都有记录业务日志到文件的需求. 对比了 winston, bunyan, caterpillar, log4js 后, 觉得 log4js 从易用性/功能/维护程度看最适合, 可以用传统 linux 统计日志的方法做统计. — DokuWiki Administrator 2014/02/18 01:53

log4js

var log4js = require('log4js'); 
 
// console log is loaded and added by default, so you won't normally need to do this
// log4js.loadAppender('console');
// log4js.addAppender(log4js.appenders.console());
 
log4js.loadAppender('file');
log4js.addAppender(log4js.appenders.file('logs/cheese.log'), 'cheese');
 
var logger = log4js.getLogger('cheese');
 
// 高于 ERROR 等级的 log 才会记录
logger.setLevel('ERROR');
 
logger.trace('Entering cheese testing');
logger.debug('Got cheese.');
logger.info('Cheese is Gouda.');
logger.warn('Cheese is quite smelly.');
logger.error('Cheese is too ripe!');
logger.fatal('Cheese was breeding ground for listeria.');
 
// 使用 log() 方法时, 内置 level 必须大写! 
logger.log('FATAL', 'Cheese was breeding ground for listeZgbE'EZgbr0">);
 
// 使用 log() 还能记录自定 level 的log
// 自定 level 的 log 不受 setLevel 限制, 一定会记录
logger.log('customLevel', 'content');
 
// 设置另外的 logger
var anotherLogger = log4js.getLogger('burger');
anotherLogger.fatal('Burger kinnnnnnng');
 
/*
$ node lib/log.js
[2014-02-18 11:14:04.124] [ERROR] cheese - Cheese is too ripe!
[2014-02-18 11:14:04.126] [FATAL] cheese - Cheese was breeding ground for listeria.
[2014-02-18 11:14:04.126] [FATAL] cheese - Cheese was breeding ground for listeriaaaaaa.
[2014-02-18 11:14:04.126] [anyLevel] cheese - content
[2014-02-18 11:14:04.126] [FATAL] burger - Burger kinnnnnnng
 
$ cat logs/cheese.log
[2014-02-18 11:14:04.124] [ERROR] cheese - Cheese is too ripe!
[2014-02-18 11:14:04.126] [FATAL] cheese - Cheese was breeding ground for listeria.
[2014-02-18 11:14:04.126] [FATAL] cheese - Cheese was breeding ground for listeriaaaaaa.
[2014-02-18 11:14:04.126] [anyLevel] cheese - content
*/

winston

winston 的日志是以 json 记录的, 可以记录 json 对象, 但是 winston 对 buffer 的显示有问题 6)

var winston = require('winston');
 
var logger = new (winston.Logger)({
  transports: [
    new (winston.transports.Console)({
      level: 'debug',
      colorize: true: false, // Boolean flag indicating whether to suppress output (default false)
      timestamp: function() { return moment().format('HH:mm:ss'); }, // 任意时间格式
      // timestamp: true // 默认时间格式
    }),
    new (winston.transports.File)({
      level: 'debug',
      filename: log_file
    })
  ]
});
 
// 清除所有 transports
logger.clear();

测试

常用 / 易出错 的 should 写法

以下是一些常用的 should 写法:

// callback(err) 中, err 为空
(err === null).should.be.true;
 
 
// 代码块 block 检查 throw
(function(){
  throw new Error('fail');
}).should.throw();
 
// (更通常的) 检查已有方法的 throw
function isPositive(n) {
    if(n <= 0) throw new Error('Given number is not positive')
}
isPositive.bind(null, 10).should.not.throw();
isPositive.bind(null, -10).should.throw();

should.throw 默认只能检查代码块是否 throw.

多数情况下, 我们是用 should 检查已有的方法, 这种情况要用 bind 生成新的代码块, 再 should

Function.prototype.bind() - JavaScript | MDN: The bind() method creates a new function that, when called, has its this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called.

  • 有人觉得 should 这个词不好,要用 must:moll/js-must/ An assertion library for JavaScript and Node.js with a friendly BDD syntax (awesome.must.be.true()). It ships with many expressive matchers and is test runner and framework agnostic. Follows RFC 2119 (一个规范 MUST、SHOULD 等词用法的 RFC) with its use of MUST. Good stuff and well tested.

mocha 中, 测试和 hook 执行的顺序

Hooks in Mocha — The JavaScript Collection — Medium

describe(‘test the order of Mocha hooks’, function(){

 before( function(){ console.log(‘before’); });
 after( function(){ console.log(‘after’); });

 beforeEach( function(){ console.log(‘beforeEach’); });
 afterEach( function(){ console.log(‘afterEach’); });

 it(‘test 1', function(){ console.log(‘1'); });
 it(‘test 2', function(){ console.log(‘2'); });
 
});

// 得到 (点 . 是 mocha 的输出)

Joeys-MacBook-Air:SocketTest joey$ mocha test.js

before
beforeEach
1
․
afterEach
beforeEach
2
.
afterEach
after

 2 passing (7ms)
  • describe 里的 before 是 before suit 在 describe 前执行的
  • describe 里的 beforeEach 是 beforeEach case, 在 describe 的每个 it 前执行的
  • 不能在 it 里写 before, 即没法修改一个 it 前的环境

超时测试

mocha 默认每个测试 2 秒为超时, 但在 describe() 和 it() 中都可设置 this.timeout(10000); 重载超时限制.

且命令行运行时也可设置 mocha -t 10s 重载.

私有方法的测试 Inversion of control 控制反转 Dependency injection 依赖注入(一种 IoC 的实现)

rewire 是一个 Easy dependency injection for node.js unit testing. 它可以 “暴露 get” 和 “修改 set” module 未 export 的变量. 从而方便 对模块内部做单元测试 和 mocking 测试.

lib/myModule.js
// 待测试的文件
 
// With rewire you can change all these variables
var fs = require("fs"),
    http = require("http"),
    someOtherVar = "hi",
    myPrivateVar = 1;
 
function readSomethingFromFileSystem(cb) {
    // But no scoped variables
    var path = "/somewhere/on/the/disk";
 
    console.log("Reading from file system ...");
    fs.readFile(path, "utf8", cb);
}
 
exports.readSomethingFromFileSystem = readSomethingFromFileSystem;
test/myModule.test.js
// 使用 rewire 的测试脚本
 
var rewire = require("rewire");
 
// rewire acts exactly like require.
var myModule = rewire("../lib/myModule.js");
 
// Just with one difference:
// Your module will now export a special setter and getter for private variables.
myModule.__set__("myPrivateVar", 123);
myModule.__get__("myPrivateVar"); // = 123
 
// This allows you to mock almost everything within the module e.g. the fs-module.
// Just pass the variable name as first parameter and your mock as second.
myModule.__set__("fs", {
    readFile: function (path, encoding, cb) {
        cb(null, "Success!");
    }
});
myModule.readSomethingFromFileSystem(function (err, data) {
    console.log(data); // = Success!
});
 
// You can set different variables with one call.
myModule.__set__({
    fs: fsMock,
    http: httpMock,
    someOtherVar: "hello"
});
 
// You may also override globals. These changes are only within the module, so
// you don't have to be concerned that other modules are influenced by your mock.
myModule.__set__({
    console: {
        log: function () { /* be quiet */ }
    },
    process: {
        argv: ["testArg1", "testArg2"]
    }
});
 
// But be careful, if you do something like this you'll change your global
// console instance.
myModule.__set__("console.log", function () { /* be quiet */ });
 
// There is another difference to require:
// Every call of rewire() returns a new instance.
rewire("./myModule.js") === rewire("./myModule.js"); // = false

code coverage

Blanket.js | Seamless javascript code coverage

// 安装
$ npm i --save-dev blanket


// 配置需要测试覆盖的代码
$ vi package.json

  "devDependencies": {
    "blanket": "~1.1.6"
  },
  "config": {
    "blanket": {
      "pattern": "/lib/",
      "data-cover-never": "node_modules"
    }
  }


// 运行
$ mocha --require blanket -R html-cov > coverage.html

测试 HTTP 服务

supertest! supertest 可接收 http.Server 对象, 所以无需手动为测试环境架设服务.

 
var request = require('supertest')
  , express = require('express');
 
var app = express();
 
app.get('/user', function(req, res){
  res.send(200, { name: 'tobi' });
});
 
request(app)
  .get('/user')
  .expect('Content-Type', /json/)
  .expect('Content-Length', '20')
  .expect(200)
  .end(function(err, res){
    if (err) throw err;
  });

测试 JSON RPC

能用 supertest, 但是 supertest 未提供 JSONRPC 的测试接口, 生成请求和解析回复都需手动处理, 不方便.

或者自己写一个适用于 supertest 的 JSONRPC 的 ENCODER/DECODER, 或者手动在 before() 中架服务, 在 it() 中用某些 JSONRPC 的客户端测试.

HTTP mock

Nock 是一个用来 mock (第三方) HTTP Server 的模块.

Nock works by overriding Node's http.request function. Also, it overrides http.ClientRequest too to cover for modules that use it directly.

但 nock 只能做简单的 reply mocking, 如果要做更复杂的如 RPC mocking, 还是需要自架服务.

var nock = require('nock');
 
var couchdb = nock('http://myapp.iriscouch.com')
    .get('/users/1')
    .reply(200, {
        _id: '123ABC',
        _rev: '946B7D1C',
        username: 'pgte',
        email: 'pedro.teixeira@gmail.com'
    });
 
var request = require('supertest');
 
request('http://myapp.iriscouch.com')
    .get('/users/1')
    .end(function(err, res) {
        if (err) throw err;
 
        // 该请求会由 nock 返回
        console.log(res);
    });
  • 另一个 mock 模块,也能做 TCP mock:moll/node-mitm Intercept and mock outgoing Node.js network TCP connections and HTTP requests for testing. Intercepts and gives you a Net.Socket, Http.IncomingMessage and Http.ServerResponse to test and respond with. Super useful when testing code that hits remote servers.
  • h2non/toxy:Hackable HTTP proxy to simulate server failure scenarios and unexpected network conditions

mocha 以外的测试

  • mocha 是运行在 node.js 环境的测试框架, 而 jasmine 是运行在浏览器环境的测试框架
    describe("A suite", function() {
      it("contains spec with an expectation", function() {
        expect(true).toBe(true);
      });
    });
  • jasmine 原生并不提供 BOM/DOM 测试方法, 即只是 js 测试框架, 而 非 Web 自动化测试工具 (如 selenium 以下列出)
  • jasmine 原始用法需要准备一个网页, 引入 jasmine.js 和 jasmine-html.js, 还需引入待测的 lib.js 和测试脚本 test.js 外, jasmine 1.* 再要写一个固定内容的 report.js 来启动测试, 2 jasmine 提供了一个 boot.js, 看起来总之很麻烦
  • 所以 angular 团队开发了 karma, karma 可以根据配置自动准备环境并运行 jasmine 的测试 (一直开着, watch 到文件修改就自动跑一遍测试), 并且由于 karma 中已经配置了要测试的文件, 增加 istanbul 做代码覆盖率检查也很方便 (npm i karma-coverage)
  • angular 内置 e2e end-to-end test 一个例子如下, 这样看 angular 测试挺方便的:
    // angular e2e test
    describe('PhoneCat App', function() {
     
      describe('Phone list view', function() {
     
        // 在每个 ''it() {}'' 前 setup 环境
        beforeEach(function() {
          browser().navigateTo('../../app/index.html');
        });
     
     
        it('should filter the phone list as user types into the search box', function() {
          expect(repeater('.phones li').count()).toBe(3);
     
          input('query').enter('nexus');
          expect(repeater('.phones li').count()).toBe(1);
     
          input('query').enter('motorola');
          expect(repeater('.phones li').count()).toBe(2);
        });
      });
    });
  • 除了在真的浏览器中测试外, phantomJS is a headless WebKit scriptable with a JavaScript API 可以 headless 地自动化做 Web 测试. 不过 phantomJS 并不是 node.js 的 npm, 是单独的 webkit 项目, 安装起来会比较麻烦. 另外, 也有评论说 hard to use
  • CasperJS : based on PhantomJS with an easy test API. Still hard to install. 另外 casper 还可用于 SlimerJS (Gecko (firefox)), 但 CasperJS 也不能作为 npm 模块使用
  • slimerjs 在安装上看起来方便一些: npm install slimerjs, It will detect your platform and download the standalone package. You can then launch slimerjs with ./node_modules/.bin/slimerjs, slimerjs 也不能作为 npm 模块使用
  • Nightmare is A high level wrapper for Phantomjs.
  • nodejs 中也有类似的 headless 工具: Zombie.js, 但它最大的问题是 no real rendering, 从而对于 测试 不可靠

sinon.js

Sinon.JS Standalone test spies, stubs and mocks for JavaScript. No dependencies, works with any unit testing framework.

注释/文档规范 JSDoc

cli ui blessed ncurses

DokuWiki Administrator 2014/06/25 06:46

linux 中提供 curses/ncurses 来制作 cli 下的 GUI-like 界面.

  • CURSES的命名是来自一个叫做 “cursor optimization”(光标最优化)的双关语。CURSES库通过对终端原始控制代码(转义序列)的封装,向用户提供了一个灵活高效的API(应用 程序接口
  • n for new

node.js 中如果要做 cli 的 GUI-like 界面, 有 2 个工具可选择:

使用 cli pipe 和 net.server 包装的测试脚本分别如下.

net_wrap.js
#!/usr/bin/env node
 
 
/**
 * widget 是一个 curses UI 的应用,
 * hpipe UI 给 telnet 连接
 */
var net = require('net'),
    child = require('child_process'),
    bin = child.spawn('./widget.js', ['InputBox']);
 
var server = net.createServer(function(c) {
 
    console.log('server connected');
    c.on('end', function() {
        console.log('server disconnected');
    });
 
    c.pipe(bin.stdin);
    bin.stdout.pipe(c);
 
}).listen(8124, function() {
    console.log('server bound 8124');
});
child_wrap.js
#!/usr/bin/env node
 
 
/**
 * widget 是一个 curses UI 的应用,
 * hpipe UI 给 telnet 连接
 */
var child = require('child_process'),
    bin = child.spawn('./widget.js', ['InputBox']);
 
bin.stdout.pipe(process.stdout, { end: false });
 
process.stdin.resume();
 
process.stdin.pipe(bin.stdin, { end: false });
 
bin.stdin.on('end', function() {
    process.stdout.write('REPL stream ended.');
});
 
bin.on('exit', function (code) {
    process.exit(code);
});

开发 CLI 工具的要点

  • liftoff 可以提供多级 (当前目录, ~/) 搜索配置文件的功能, 用来简化 cli 工具开发

Building CLI Tools with Node.js

  1. A command-line interface should be testable –> treating the CLI as a module instead of an executable script.
    1. slim down the bin/ to proxy into the CLI module
    2. CLI should be a module that consumes the main library. This allows us to test the CLI as a module by faking process.argv
    3. the CLI module should only have CLI logic keeping it focused. Allowing us to mock the main library and keep tests running fast
  2. A command-line interface should be helpful. USAGE & HELP!!
  3. 命令行应该有 verbose 和 quiet 选项
    1. event emitters work well
    2. main library is verbose like a chatty kid
    3. And the CLI module can subscribe or unsubscribe to the chatty kid
  4. A command-line interface should be interoperable.
    1. exit codes are important.
    2. We may also want consumable output for other tools to parse.
      1. –json
      2. –format
      3. etc..
    3. 尝试兼容 windows
      1. Try to avoid hard-coding paths into your code.
      2. 注意环境变量
      3. etc..

Node 旨在解决什么问题? (deprecated)

Node.js 究竟是什么?

Node 公开宣称的目标是 “旨在提供一种简单的构建可伸缩网络程序的方法”。当前的服务器程序有什么问题?我们来做个数学题。在 Java™ 和 PHP 这类语言中,每个连接都会生成一个新线程,每个新线程可能需要 2 MB 的配套内存。在一个拥有 8 GB RAM 的系统上,理论上最大的并发连接数量是 4,000 个用户。随着您的客户群的增长,如果希望您的 Web 应用程序支持更多用户,那么,您必须添加更多服务器。当然,这会增加服务器成本、流量成本和人工成本等成本。除这些成本上升外,还有一个潜在技术问题,即用户可能针对每个请求使用不同的服务器,因此,任何共享资源都必须在所有服务器之间共享。鉴于上述所有原因,整个 Web 应用程序架构(包括流量、处理器速度和内存速度)中的瓶颈是:服务器能够处理的并发连接的最大数量。

Node 解决这个问题的方法是:更改连接到服务器的方式。每个连接发射一个在 Node 引擎的进程中运行的事件,而不是为每个连接生成一个新的 OS 线程(并为其分配一些配套内存)。Node 声称它绝不会死锁,因为它根本不允许使用锁,它不会直接阻塞 I/O 调用。Node 还宣称,运行它的服务器能支持数万个并发连接。

现在您有了一个能处理数万个并发连接的程序,那么您能通过 Node 实际构建什么呢?如果您有一个 Web 应用程序需要处理这么多连接,那将是一件很 “恐怖” 的事!那是一种 “如果您有这个问题,那么它根本不是问题” 的问题。在回答上面的问题之前,我们先看看 Node 的工作原理以及它的设计运行方式。 正如您此前所看到的,Node 非常适合以下情况:在响应客户端之前,您预计可能有很高的流量,但所需的服务器端逻辑和处理不一定很多。Node 表现出众的典型示例包括:

RESTful API

提供 RESTful API 的 Web 服务接收几个参数,解析它们,组合一个响应,并返回一个响应(通常是较少的文本)给用户。这是适合 Node 的理想情况,因为您可以构建它来处理数万条连接。它仍然不需要大量逻辑;它本质上只是从某个数据库中查找一些值并将它们组成一个响应。由于响应是少量文本,入站请求也是少量的文本,因此流量不高,一台机器甚至也可以处理最繁忙的公司的 API 需求。

Twitter 队列

想像一下像 Twitter 这样的公司,它必须接收 tweets 并将其写入数据库。实际上,每秒几乎有数千条 tweet 达到,数据库不可能及时处理高峰时段所需的写入数量。Node 成为这个问题的解决方案的重要一环。如您所见,Node 能处理数万条入站 tweet。它能快速而又轻松地将它们写入一个内存排队机制(例如 memcached),另一个单独进程可以从那里将它们写入数据库。Node 在这里的角色是迅速收集 tweet,并将这个信息传递给另一个负责写入的进程。想象一下另一种设计(常规 PHP 服务器会自己尝试处理对数据库本身的写入):每个 tweet 都会在写入数据库时导致一个短暂的延迟,因为数据库调用正在阻塞通道。由于数据库延迟,一台这样设计的机器每秒可能只能处理 2000 条入站 tweet。每秒处理 100 万条 tweet 则需要 500 个服务器。相反,Node 能处理每个连接而不会阻塞通道,从而能够捕获尽可能多的 tweets。一个能处理 50,000 条 tweet 的 Node 机器仅需 20 台服务器即可。

电子游戏统计数据

如果您在线玩过《使命召唤》这款游戏,当您查看游戏统计数据时,就会立即意识到一个问题:要生成那种级别的统计数据,必须跟踪海量信息。这样,如果有数百万玩家同时在线玩游戏,而且他们处于游戏中的不同位置,那么很快就会生成海量信息。Node 是这种场景的一种很好的解决方案,因为它能采集游戏生成的数据,对数据进行最少的合并,然后对数据进行排队,以便将它们写入数据库。使用整个服务器来跟踪玩家在游戏中发射了多少子弹看起来很愚蠢,如果您使用 Apache 这样的服务器,可能会 有一些有用的限制;但相反,如果您专门使用一个服务器来跟踪一个游戏的所有统计数据,就像使用运行 Node 的服务器所做的那样,那看起来似乎是一种明智之举。

用 nodejs 做日志服务器的一个例子: Log Collection Server with Node.js - Irrational Exuberance

实时程序

实时程序的大用途在于线下数据的实时化! 实时化百度称作云(Cloud)端(Client)一体化

客户端用 JS 写的程序

前后台语言同步, 方便对象传输, 减少分工

活跃项目 (deprecated)

底层

1)
类似的库还有 CrossEye/ramda
4)
This should save you from having to do a lot of checking for undefined, such as, for example, when you are using jade templates.
5)
(“xmlUrl” and “pubDate” also are still used to provide backwards compatibility. 但这些属性同时也有全小写的键值
it/nodejs.txt · Last modified: 2017/01/10 15:24 by admin