node-pnglib 跨平台画 png 的轻量基础库

NPM Version Build Status Coveralls Status

最近手痒造的轮子。。一个可以用纯 JavaScript 撸 png 的库,在路由器、树莓派之类的平台上有画图需求的同学可以看看。 Github 地址:https://github.com/Lellansin/node-pnglib

安装

npm install node-pnglib

示例

使用 http server

const http = require('http');
const PNGlib = require('node-pnglib');

http.createServer(function (req, res) {
  if(req.url == '/favicon.ico') return res.end('');

  // width 100, height 40
  let png = new PNGlib(100, 40);

  // from (0, 20)
  let lineIndex = png.index(0, 20);
  for (let i = 0; i < 100; i++) {
    // draw a line to (0, 75)
    png.buffer[lineIndex + i] = png.color('blue');
  }

  res.setHeader('Content-Type', 'image/png');
  res.end(png.getBuffer());
}).listen(3001);

Output:

line.png

保存文件

const fs = require('fs');
const PNGlib = require('node-pnglib');

let png = new PNGlib(150, 150);
for (let i = 20; i < 100; i++) {
  for (let j = 20; j < 100; j++) {
    png.setPixel(i + 10, j + 25, '#cc0044');
    png.setPixel(i + 20, j + 10, '#0044cc');
    png.setPixel(i + 30, j, '#00cc44');
  }
}

fs.writeFileSync('./block.png', png.getBuffer());

Output:

block.png

来画几个波浪线

const http = require('http');
const PNGlib = require('node-pnglib');

http.createServer(function (req, res) {
  if(req.url == '/favicon.ico') return res.end('');

  let png = new PNGlib(100, 100);

  for (let i = 0; i < 65; i++) {
    for (let j = 10; j < 65; j++) {
      png.setPixel(i + 10, j + 20, '#cc0044');
      png.setPixel(i + 20, j + 10, '#0044cc');
      png.setPixel(i + 30, j, '#00cc44');
    }
  }
  res.setHeader('Content-Type', 'image/png');
  res.end(png.getBuffer());
}).listen(3001);

Output:

wave.png

基准测试

# 简单的划线

pnglib x 1,021 ops/sec ±3.37% (76 runs sampled)
pnglib-es6 x 3,293 ops/sec ±4.79% (79 runs sampled)
node-pnglib x 17,027 ops/sec ±0.93% (87 runs sampled)
Fastest is node-pnglib

node v8.1.1
MacBook Pro (Retina, 13-inch, Early 2015)
2.7 GHz Intel Core i5

比同类型库要快(单纯划线比原版要快 10+ 倍)。 你可以在这里 查看详细的基准测试内容。

验证码

应用上使用这个替换了 captchapng (因为这个库有没维护了所以撸了这些基础库) 发了个平滑迁移的 captchapng2

之前用 captchapng 的同学欢迎转过来,有需求可以在 github 提,最近半年应该都会维护。

Advertisements

Node.js 8.0.0 async pattern benchmark

Node.js 的 8.0 版本终于发布了,等了两天没人测评,于是决定抛砖引玉。
首先需要说明的是,由于不方便拿业务代码测试来评测,所以本次基准测试(benchmark)毫无新意的沿用了 bluebird 的基准测试。测试比较的的内容是多个异步模式(async pattern)之间的性能对比。

目前 Node.js 业内比较异步模式性能比较靠谱的方式是基于 Gorki Kosev’s 的 《Analysis of generators and other async patterns in node》 的 benchmark。该 benchmark 模拟会模拟同时混合大量同步与异步操作的处理场景。具体指标是处理这些操作消耗的时间(ms)以及内存(MB)。

直接上一个 Node.js v8.0.0 的测试报告:

results for 10000 parallel executions, 1 ms per I/O op

file                                       time(ms)  memory(MB)
callbacks-baseline.js                           328       24.11
callbacks-suguru03-neo-async-waterfall.js       409       35.66
promises-bluebird-generator.js                  703       40.76
promises-bluebird.js                            781       51.74
promises-then-promise.js                        922       67.02
promises-cujojs-when.js                         963       65.11
promises-lvivski-davy.js                       1071       96.27
promises-tildeio-rsvp.js                       1159       87.05
callbacks-caolan-async-waterfall.js            1345      102.79
promises-dfilatov-vow.js                       1504      131.96
promises-calvinmetcalf-lie.js                  1870      164.99
promises-ecmascript6-native.js                 2167      167.91
promises-obvious-kew.js                        2269      225.37
generators-tj-co.js                            2273      119.98
observables-pozadi-kefir.js                    3170      188.55
promises-medikoo-deferred.js                   3202      114.25
observables-Reactive-Extensions-RxJS.js        4622      238.07
streamline-generators.js                       6096      128.54
promises-kriskowal-q.js                       12051      391.93
observables-caolan-highland.js                12269      534.94
streamline-callbacks.js                       62359      198.94
observables-baconjs-bacon.js.js                 OOM         OOM

Platform info:
Linux 2.6.32-042stab117.14 x64
Node.JS 7.7.4
V8 5.5.372.42
Intel(R) Xeon(R) CPU           L5640  @ 2.27GHz × 2

其中的测试项中, 比较常见的详情分别是:

执行速度最快的还是原生的 callback

在内存占用方面可以看到 native 的 promise 较之前的版本有明显的下降,已经达到 async 模块的水平了。

完整执行数据 (ms)

pattern 6-1 6-2 7-1 7-2 8-1 8-2
callbacks-baseline.js 335 348 345 328 357 370
callbacks-suguru03-neo-async-waterfall.js 426 433 428 409 462 428
promises-bluebird.js 829 892 750 781 779 770
promises-bluebird-generator.js 838 973 707 703 782 789
promises-cujojs-when.js 916 961 978 963 977 965
promises-then-promise.js 827 913 940 922 987 1102
promises-lvivski-davy.js 1014 1149 1035 1071 1131 1142
promises-tildeio-rsvp.js 1085 1124 1189 1159 1186 1142
promises-ecmascript6-native.js 2530 2540 2286 2167 1216 1350
callbacks-caolan-async-waterfall.js 1317 1312 1427 1345 1433 1430
generators-tj-co.js 2753 2642 2267 2273 1616 1671
promises-dfilatov-vow.js 1382 1496 1665 1504 1688 1770
promises-calvinmetcalf-lie.js 1650 1795 1784 1870 2031 2177
promises-obvious-kew.js 2326 2421 2173 2269 2610 2667
promises-medikoo-deferred.js 4231 4416 3477 3202 3227 3048
observables-pozadi-kefir.js 60466 64587 3177 3170 3442 3161
streamline-generators.js 5533 6048 6682 6096 3013 3228
streamline-callbacks.js 8340 8784 79046 62359 4360 4339
observables-Reactive-Extensions-RxJS.js 4488 4947 4514 4622 5614 5578
observables-caolan-highland.js 152903 164368 12903 12269 17796 17243
promises-kriskowal-q.js 14618 15963 13198 12051 21159 19219
observables-baconjs-bacon.js.js 37014 33282 45257

完整内存数据 (MB)

pattern 6-1 6-2 7-1 7-2 8-1 8-2
callbacks-baseline.js 30 30 24 24 31 31
callbacks-suguru03-neo-async-waterfall.js 43 43 35 35 39 39
promises-bluebird-generator.js 39 40 40 40 41 43
promises-bluebird.js 49 49 50 51 49 48
promises-cujojs-when.js 59 60 65 65 61 60
promises-then-promise.js 59 59 68 67 73 74
generators-tj-co.js 131 131 119 119 74 75
promises-tildeio-rsvp.js 89 89 88 87 80 82
promises-lvivski-davy.js 91 90 97 96 103 102
streamline-generators.js 179 179 129 128 102 102
promises-ecmascript6-native.js 187 187 163 167 95 104
callbacks-caolan-async-waterfall.js 101 101 102 102 108 108
streamline-callbacks.js 163 251 199 198 127 126
promises-dfilatov-vow.js 130 147 132 131 153 159
promises-calvinmetcalf-lie.js 152 135 164 164 147 166
promises-medikoo-deferred.js 190 190 114 114 183 183
observables-pozadi-kefir.js 146 152 188 188 202 202
promises-obvious-kew.js 217 216 79 225 228 232
observables-Reactive-Extensions-RxJS.js 229 230 236 238 243 244
promises-kriskowal-q.js 837 848 370 391 387 388
observables-caolan-highland.js 485 492 517 534 585 559
observables-baconjs-bacon.js.js 826 833 582

小结

  • neo 版的 async 依旧是最快的异步解决方案。
  • 依赖 native 版本的实现(promise/co 等)都有一定程度的性能、内存提升(不过提升的不是很多)。
  • native 的 promise 在提升之后性能已与 async 模块相当,并且 bluebird 的实现与 native 版本差距进一步缩小,bluebird 已经没有成倍的优势。

PS:目前该 benchmark 尚未加入 async/await (笔者已经提了 issue,在考虑要不要自己撸),加入之后会更新本文。

http://echarts.min.js

Node.js 的 require.resolve 简介

简单的说, 在 Node.js 中使用 fs 读取文件的时候, 经常碰到要拼一个文件的绝对路径的问题 (fs 处理相对路径均以进程执行目录为准). 之前一直的方法都是, 使用 path 模块以及 __dirname 变量 :

fs.readFileSync(path.join(__dirname, './assets/some-file.txt'));

使用 require.resolve 可以简化这一过程:

fs.readFileSync(require.resolve('./assets/some-file.txt'));

此外, require.resolve 还会在拼接好路径之后检查该路径是否存在, 如果 resolve 的目标路径不存在, 就会抛出 Cannot find module './some-file.txt' 的异常. 省略了一道检查文件是否存在的工序 (fs.exists).

这个报错并不会加重你的检查负担, 毕竟使用 fs 去操作文件时, 如果发现文件不存在也会抛出异常. 反之, 通过 require.resovle 可以在提前在文件中作为常量定义, 那么在应用启动时就可以抛异常, 而不是等到具体操作文件的时候才抛异常.

Node.js Mongodb 密码特殊字符 @

在去年的 DB 勒索事件之后, 不少的同学开始加强 Mongodb 的安全性, 其中一种办法就是设置复杂的密码. 那么问题来了, 如果设置的密码里包含一些如 “@”, “:” 一样的特殊字符怎么办?

mongodb://username:password@host:port/db

这种情况可能使得你的 Mongodb 连接串不能被正常解析, 并且完全有可能出现. 烦人的地方在于:

  1. 使用 “” 双引号将 password 包起来没有用
  2. 使用 \@ 转义也没有用

解决方案1

开启 uri_decode_auth 功能, 拼接连接串之后先 encode 一下, 然后通过 uri_decode_auth 在 driver 内部 decode 来绕过这个问题

mongoClient.connect("mongodb://username:p%40ssword@host:port/dbname", {
    uri_decode_auth: true
    }, function(err, db) {

    }
);

更新 Driver 2.2.25 is a bugfix release and contains the following fixes.
2.2.25 2017-03-17

之后的版本已经不需要显示指定 uri_decode_auth: true, 直接 encode 即可。

解决方案2

 

老老实实查文档, 在 options 中指明:

mongoose.connect('mongodb://localhost/test',
                 {user: 'username', pass: 'p@ssword'},
                 callback);

在 Node.js 中看 Javascript 的引用

本文首发于《程序员》杂志 2017 年第 3 期

早期学习 Node.js 的时候 (2011-2012),有挺多是从 PHP 转过来的,当时有部分人对于 Node.js 编辑完代码需要重启一下表示麻烦(PHP不需要这个过程),于是社区里的朋友就开始提倡使用 node-supervisor 这个模块来启动项目,可以编辑完代码之后自动重启。不过相对于 PHP 而言依旧不够方便,因为 Node.js 在重启以后,之前的上下文都丢失了。

虽然可以通过将 session 数据保存在数据库或者缓存中来减少重启过程中的数据丢失,不过如果是在生产的情况下,更新代码的重启间隙是没法处理请求的(PHP可以,另外那个时候 Node.js 还没有 cluster)。由于这方面的问题,加上本人是从 PHP 转到 Node.js 的,于是从那时开始思考,有没有办法可以在不重启的情况下热更新 Node.js 的代码。

最开始把目光瞄向了 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.data
var 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 的时候,内存中的情况大概是:

|   Addr   |  内容  |
|----------|--------
| obj.data |  内存1 |
|   data   |  内存1 |

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

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

|   Addr   |  内容  |
|----------|--------
| obj.data |  内存1 |
|   data   |  内存2 |

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

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

Node.js 中的 module.exports 与 exports

上述例子中的 obj.data 与 data 的关系,就是 Node.js 中的 module.exports 与 exports 之间的关系。让我们来看看 Node.js 中 require 一个文件时的实际结构:

function require(...) {
  var module = { exports: {} };
  ((module, exports) => { // Node.js 中文件外部其实被包了一层自执行的函数
    // 这中间是你模块内部的代码.
    function some_func() {};
    exports = some_func;
    // 这样赋值,exports便不再指向module.exports
    // 而module.exports依旧是{}

    module.exports = some_func;
    // 这样设置才能修改到原本的exports
  })(module, module.exports);
  return module.exports;
}

所以很自然的:

console.log(module.exports === exports); // true
// 所以 exports 所操作的就是 module.exports

Node.js 中的 exports 就是拷贝的一份 module.exports 的引用。通过 exports 可以修改Node.js 当前文件导出的属性,但是不能修改当前模块本身。通过 module.exports 才可以修改到其本身。表现上来说:

exports = 1; // 无效
module.exports = 1; // 有效

这是二者表现上的区别,其他方面用起来都没有差别。所以你现在应该知道写module.exports.xx = xxx; 的人其实是多写了一个module.。

更复杂的例子

为了再练习一下,我们在来看一个比较复杂的例子:

var a = {n: 1};  
var b = a; 
a.x = a = {n: 2};  
console.log(a.x);
console.log(b.x);

按照开始的结论我们可以一步步的来看这个问题:

var a = {n: 1};   // 引用a指向内存1{n:1}
var b = a;        // 引用b => a => { n:1 }

内部结构:

|   Addr  |     内容     |
|---------|-------------|
|    a    |  内存1 {n:1} |
|    b    |  内存1       |

继续往下看:

a.x = a = {n: 2};  //  (内存1 而不是 a ).x = 引用 a = 内存2 {n:2}

a 虽然是引用,但是 JavaScript 是值传的这个引用,所以被修改不影响原本的地方。

|    Addr   |          内容         |
|-----------|-----------------------|
| 1) a	    |  内存2({n:2})         |
| 2) 内存1.x |  内存2({n:2})         |
| 3) b	    |  内存1({n:1, x:内存2}) |

所以最后的结果

  • a.x 即(内存2).x ==> {n: 2}.x ==> undefined
  • b.x 即(内存1).x ==> 内存2 ==> {n: 2}

总结

JavaScrip t中没有引用传递,只有值传递。对象(引用类型)的传递只是拷贝一个新的引用,这个新的引用可以访问原本对象上的属性,但是这个新的引用本身是放在另外一个格子上的值,直接往这个格子赋新的值,并不会影响原本的对象。本文开头所讨论的 Node.js 热更新时碰到的也是这个问题,区别是对象本身改变了,而原本拷贝出来的引用还指向旧的内存,所以通过旧的引用调用不到新的方法。

Node.js 并没有对 JavaScript 施加黑魔法,其中的引用问题依旧是 JavaScript 的内容。如 module.exports 与 exports 这样隐藏了一些细节容易使人误会,本质还是 JavaScript 的问题。另外推荐一个关于 Node.js 的进阶教程 《Node.js 面试》

注[1]:

  1. 老实说,模块在函数内声明有点谭浩强的感觉。
  2. 把 b = include(xxx) 写在调用内部,还可以通过设置成中间件绑定在公共地方来写。
  3. 除了写在调用内部,也可以导出一个工厂函数,每次使用时 b().num 一下调用也可以。
  4. 还可以通过中间件的形式绑定在框架的公用对象上(如:ctx.b = include(xxx))。
  5. 要实现这样的热更新必须在架构上就要严格避免旧代码被引用的可能性,否则很容易写出内存泄漏的代码。

ERR_INCOMPLETE_CHUNKED_ENCODING koa node

请求突然出现了 ERR_INCOMPLETE_CHUNKED_ENCODING 这个报错,貌似 http 返回的 chunk 跪了。虽然已经修复了,但还是没搞清楚发生了什么。

谷歌娘说在 nginx 的 location 里面加上 proxy_buffering off; 然后重启 nginx 果然好了。

问题是,我随后注释掉这个依旧能没问题,所以是 nginx 闹脾气了需要重启?

5628dd6ecd9fa100f371_size30_w521_h534

Cannot read property pm2_env of undefined

[PM2][WARN] Applications x-filter not running, starting...
/usr/local/lib/node_modules/pm2/lib/CLI.js:461
        Common.printOut(cst.PREFIX_MSG + 'App [%s] launched (%d instances)', data[0].pm2_env.name, data.length);
                                                                                    ^

TypeError: Cannot read property 'pm2_env' of undefined
    at /usr/local/lib/node_modules/pm2/lib/CLI.js:461:85
    at /usr/local/lib/node_modules/pm2/node_modules/pm2-axon-rpc/lib/client.js:45:10
    at Parser.<anonymous> (/usr/local/lib/node_modules/pm2/node_modules/pm2-axon/lib/sockets/req.js:67:8)
    at emitOne (events.js:96:13)
    at Parser.emit (events.js:188:7)
    at Parser._write (/usr/local/lib/node_modules/pm2/node_modules/amp/lib/stream.js:91:16)
    at doWrite (_stream_writable.js:307:12)
    at writeOrBuffer (_stream_writable.js:293:5)
    at Parser.Writable.write (_stream_writable.js:220:11)
    at Socket.ondata (_stream_readable.js:555:20)

碰到这个心情不好,最后发现。其实只要跑一下 pm2 update 就好了。大致是因为更新了 pm2 没有跑这个命令出来的问题。