Skip to main content

One post tagged with "node.js"

View All Tags

· One min read
const puppeteer = require('puppeteer');
const pageUrl = 'https://some-url.com';

(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setRequestInterception(true);

page.on('request', (interceptedRequest) => {
// Don't intercept main document request
if (interceptedRequest.url === pageUrl) {
interceptedRequest.continue();
return;
}

// Intercept if request url starts with https
if (interceptedRequest.url.startsWith('https://')) {
interceptedRequest.continue({
// Replace https:// in url with http://
url: interceptedRequest.url.replace('https://', 'http://'),
});
return;
}

// Don't override other requests
interceptedRequest.continue();
})

await page.goto(pageUrl);
await browser.close();
})();

· One min read

问题背景

发现生产环境的业务报了好多错误, 涉及的 Node.js 代码是一个基于 Redis 的频率计数器,那部分逻辑大概是这样

// 查询并增加一次计数
async incr (id) {
const key = `${this.namespace}:${id}`
const now = getMicrotime()
const start = now - this.duration * 1000

const operations = [
['zremrangebyscore', key, 0, start],
['zcard', key],
['zadd', key, now, now],
['pexpire', key, this.duration]
]

const res = await this.redis.multi(operations).exec()
const count = toNumber(res[1][1])
return count
}

错误是:

Cannot read property '1' of undefined

· One min read

Mac 环境下 node 安装 canvas@2.6.1 出现以下错误时

node: cairo-pattern.c:1127: cairo_pattern_destroy: Assertion failed. none - catched error

使用 brew 安装一下以下几个库

brew install pixman cairo pango

不过你可能会遇到 python2.x 升级失败的问题

可以试试

brew uninstall python@2
brew install python
brew upgrade python

升级到 python3.x

来源: https://github.com/Automattic/node-canvas/issues/1065#issuecomment-373381272

· One min read
const Fs = require('fs')
const Path = require('path')
const Axios = require('axios')

async function downloadImage () {
const url = 'https://unsplash.com/photos/AaEQmoufHLk/download?force=true'
const path = Path.resolve(__dirname, 'images', 'code.jpg')
const writer = Fs.createWriteStream(path)

const response = await Axios({
url,
method: 'GET',
responseType: 'stream'
})

response.data.pipe(writer)

return new Promise((resolve, reject) => {
writer.on('finish', resolve)
writer.on('error', reject)
})
}

downloadImage()

主要注意的是

  • responseType: 'stream'
  • response.data.pipe(writer)

· One min read

错误

如果你在使用 nodemailer smtp 发送邮箱时遇到了以下错误:

Error: Invalid login: 535 Error: authentication failed

也许是因为需要使用「客户端授权码」的问题。

解决

const _ = require('lodash')
const nodemailer = require('nodemailer')

async function main () {
const mailer = nodemailer.createTransport({
host: 'smtp.126.com',
port: 465,
pool: true,
secure: true,
auth: {
type: 'login',
user: 'xxxxx@126.com',
pass: 'xxxxx' // 如果开启了客户端授权码,则这里需要填写客户端授权码
},
tls: {
rejectUnauthorized: false
}
})

const sendMailOptions = {
from: 'xxxxxxx@126.com',
to: 'xxxxxx@163.com',
subject: ' 测试主题 ',
html: ' 测试内容 '
}

const result = await mailer.sendMail(sendMailOptions)

if (!_.startsWith(_.get(result, 'response'), '250 Mail OK')) {
return Promise.reject(new Error('Send mail fail'))
}

return result.response
}

main()

参考

https://cnodejs.org/topic/55b78babf30671210b35fa31

· One min read

可以通过这个地址访问到

http://alinode.aliyun.com/dist/new-alinode/alinode.json

其中

{
"version": "v4.7.2",
"date": "2019-03-11",
"files": "linux-x64,osx-x64-tar",
"npm": "6.4.1",
"v8": "6.8.275.32",
"uv": "1.23.2",
"zlib": "1.2.11",
"openssl": "1.1.0j",
"modules": "64",
"node": "v10.15.3",
"notes": [
""
]
}

version 是 alinode 版本,node 是官方 node 版本

· One min read

node-worker-farm 源码解读

node-worker-farm 主要是用来将特定的逻辑,作为子进程的方式执行,由 worker-farm 来统一调度,分发调用任务。

模块构造函数

https://github.com/rvagg/node-worker-farm/blob/master/lib/index.js

可以看到构造函数主要是初始化 farm 容器,将给定的脚本文件路径加载进来等待调用的动作的分发。

可以看到入口有一个 farms 数组,用于存放 farm 容器,当你初始化多个 farm 实例的时候,都会将实例存储在这里,当你需要结束进程时,直接调用 node-worker-farm 暴露的 end 方法,即会销毁所有的 farm 实例。

不过这个特性也许是个坑吧,有人可能会不小心销毁掉不需要销毁的实例。

Farm

Farm 即是整个模块的核心了,它负责的功能有:

  1. 控制子进程的创建/销毁

  2. 新进来的函数执行任务的分发

  3. 控制调用的并发

各个参数的作用:

参数名说明默认值
maxCallsPerWorker每个子进程最多可以处理多少调用Infinity
maxConcurrentWorkers最大并发子进程数CPU 核数
maxConcurrentCallsPerWorker每个子进程最大的并发处理数10
maxConcurrentCalls全局最大并发处理数Infinity
maxCallTime单个 worker 最大处理时间Infinity
maxRetries最大重试次数Infinity
forcedKillTime在进程退出时,如果子进程未正常退出,则会强制退出100ms
autoStart初始化时,就自动最大数量的子进程false
onChild创建子进程时触发的函数空函数

函数调用

每次函数的执行是,构造了一个方法、参数的对象,发送给子进程排队处理。如果有限制最大全局并发数时,达到并发会抛出错误。

https://github.com/rvagg/node-worker-farm/blob/master/lib/farm.js#L312

每次调用函数在内部会执行 addCall 方法,这时就是构造了一个调用的对象信息。将调用信息存入全局队列中,执行 processQueue ,进行分发。

当前活跃的子进程个数小于最大并发子进程数时,创建一个子进程。这时,检查所有子进程正在处理的调用是否小于子进程的并发处理数。小于则将任务分配给这些子进程。

当发送任务时,如果有配置超时时间,则会注册一个超时回调,若超时,则整个子进程会被杀死,如果你的子进程中有多个调用正在执行,那么都会被干掉

处理完之后,通过 receive 方法进行后续的处理。

应用

现在默认的情况下,是最调用效率最优的方案,因为子进程被创建后,有调用的情况下,一直不会被销毁,省去了冷启动的时间。

但是,如果你需要在超时的情况下,cancel 掉正在处理的逻辑,maxCallTime 可以实现这个需求,但有个问题是,子进程被杀死时,所有其他的调用都会被终止,这不符合我们的期望,因此我们需要将 maxCallsPerWorker设为 1 ,让每个子进程同时只处理一个任务,处理完就退出,这样如果任务超时,不会对其他的任务造成影响。

思考

这样做的执行时间将会大幅增加,还有什么办法能够保证任务互不影响的情况,尽可能不降低执行效率呢。

目前想到的思路是,每次虽然只执行一个,但是每次执行完后不将子进程销毁,知道超时的时候才销毁一次,这样的话也可以减少每次创建子进程带来的开销。

你还有更好的方案吗?欢迎在下面留言讨论。

· One min read

It is in fact possible to listen to the "expired" type keyevent notification using a subscribed client to the specific channel ( and listening to its message event.

通过 subscribe client 可以监听 __keyevent@{db}__:expired ( db 是你传入的配置 ) 频道来接收过期的事件 ,

const redis = require('redis')
const CONF = {db:3}

let pub, sub

// Activate "notify-keyspace-events" for expired type events
pub = redis.createClient(CONF)
pub.send_command('config', ['set','notify-keyspace-events','Ex'], subscribeExpired)

// Subscribe to the "notify-keyspace-events" channel used for expired type events
function subscribeExpired(e, r){
sub = redis.createClient(CONF)
const expired_subKey = `__keyevent@${CONF.db}__:expired`

sub.subscribe(expired_subKey, function () {
console.log(' [i] Subscribed to "'+expired_subKey+'" event channel : '+r)
sub.on('message', function (chan, msg){
console.log('[expired]', msg)}
)
TestKey()
})
}

//例如,设置一个 key 并设置 10s 超时
function TestKey(){
pub.set('testing', 'redis notify-keyspace-events : expired')
pub.expire('testing', 10)
}

· One min read
// "apn": "^2.1.2",
var apn = require('apn');

var service = new apn.Connection(
"passphrase":"secret",
"pfx":"/path/to/cert.p12",
"production":false
});

var apnVoipProvider = new apn.Provider(voipOptions);

apnNotification = new apn.Notification();
apnNotification.badge = 1;
apnNotification.sound = 'msg.mp3';
apnNotification.alert = 'hello';
apnNotification.payload = {
title: notification.title, id: notificationID, path: notification.path,
};

apnVoipProvider.send(apnNotification, token);

· One min read

比如我们注册了如下的路由

app.get('/', function (...) { ... });
app.get('/foo/:id', function (...) { ... });
app.post('/foo/:id', function (...) { ... });

然后想要获得的东东是这样的形式:

{
get: [ '/', '/foo/:id' ],
post: [ '/foo/:id' ]
}

该肿么办呢?

在 express 3.x 中

只需要 app.routes 就可以啦

在 express 4.x 中

Applications - built with express()

app._router.stack

Routers - built with express.Router()

router.stack

更多的答案尽在 StackOverflow how-to-get-all-registered-routes-in-express

· One min read

某些场景可能需要动态的获取端口来启动服务,可能服务化场景,不直接硬配置端口。

哈哈,这看来是要为分布式、服务注册、服务发现做准备呢。

/**
* auto port
* @authors luoyjx (yjk99@qq.com)
* @date 2016-10-16 20:42:57
*/
var net = require('net');

var server = net.createServer();
server.on('error', console.log);
server.on('listening', function () {
var port = server.address().port;

server.close(function () {
console.log('auto port: %s', port);
});
}.bind(this));
// tcp 使用端口 0 表示系统分配端口
server.listen(0);

预想的场景

服务在启动的时候,首先去获取一个可用端口,将自己使用此端口启动,再注册到配置管理中,这样即可实现服务注册了。 配合 etcd 即可实现服务发现,不过还木有实践。

· One min read

文件下载实际上是二进制流的发送,所以在服务端是就向客户端写流。

而在node.js中,读流和写流又可以通过pipe连接起来,那么思路清晰之后,就是Content-type类型的问题了。

代码如下

var fs = require('fs');
var pdf = fs.createReadStream(path);

res.writeHead(200, {
'Content-Type': 'application/force-download',
'Content-Disposition': 'attachment; filename=test.rar'
});

pdf.pipe(res);

· One min read

RE-Build

Build regular expressions with natural language.

由来

你是否遇到过这么复杂的正则表达式呢?

var ipMatch = /(?:(?:1\d\d|2[0-4]\d|25[0-5]|[1-9]\d|\d)\.){3}(?:1\d\d|2[0-4]\d|25[0-5]|[1-9]\d|\d)\b/;

怎么办

使用re-build

var ipNumber = RE.group(
RE ("1").then.digit.then.digit
.or ("2").then.oneOf.range("0", "4").then.digit
.or ("25").then.oneOf.range("0", "5")
.or .oneOf.range("1", "9").then.digit
.or .digit
),

ipMatch = RE.matching.exactly(3).group( ipNumber.then(".") )
.then(ipNumber).then.wordBoundary.regex;

安装

通过npm : `

npm install re-build

通过bower

bower install re-build

文档

https://github.com/MaxArt2501/re-build

· One min read

rabbitmq

我用的是amqplib 这个包,github上的项目名是amqp.node

这个包貌似是没有实现断线自动重连的,看了看issues,也和我自己实现的版本差不多。

思路

思路主要是两个地方:

  1. connection实例的error事件.
  2. 初始化的promise链报错.

针对这两种情况都去尝试重新连接,因为可能不能立即恢复,所以得隔一段事件重连一次,直到恢复为止。

封装实现

/**
* Mq Factory
* @authors yanjixiong
* @date 2016-07-22 09:56:19
*/

var Connection = require('./Connection');
var Channel = require('./Channel');
var Exchange = require('./Exchange');
var Queue = require('./Queue');
var Consume = require('./Consume');
var ExchangeTypes = require('../constant').ExchangeTypes;
var RouteKey = require('../constant').RouteKey;

var config = require('../../config');
var log = require('../../common/logger').getLogger('Core:mq:index');

var ROUTE_KEY = RouteKey.RECEIVE_PA;
var QUEUE_NAME = RouteKey.RECEIVE_PA; // 队列名称

function MQ() {
this.connection = null;
this.init();
}

/**
* 初始化消息队列
* @return {[type]} [description]
*/
MQ.prototype.init = function init() {
var self = this;

// 创建连接
Connection
.createConnection(config.rabbitMQ_url)
.then(function (conn) {

// 实例中存储当前连接
self.connection = conn;

// 监听连接错误
conn.on('error', function(err) {
log.error('[mq] connection error ', err);
self.reconnect();
});

log.info('[mq] create connection success');

// 创建通道
return Channel
.createChannel(conn);
})
.then(function (ch) {

// 进程被杀死关闭连接
process.once('SIGINT', function() {
log.info('kill by signal SIGINT');
ch.close();
self.connection.close();
self.connection = null;
process.exit(0);
});

ch.on('error', function(error) {
// ch.close();
log.error('[mq] channel error: ', error);
});

log.info('[mq] create channel success');

// 创建交换机
return Exchange
.assertExchange(ch, config.exchange_name, ExchangeTypes.DIRECT, {durable: false})
.then(function () {
log.info('[mq] assert exchange [%s] [%s]', config.exchange_name, ExchangeTypes.DIRECT);

// 创建队列
return Queue
.assertQueue(ch, QUEUE_NAME, {exclusive: false, durable: false}); // exclusive 是否排它 durable : 是否持久化
})
.then(function (queue) {
log.info('[mq] assert queue [%s] success', QUEUE_NAME);

log.debug(queue);

// 绑定队列到交换机
return Queue.
bindQueue(ch, QUEUE_NAME, config.exchange_name, ROUTE_KEY);
})
.then(function() {
log.info('[mq] bind queue [%s] to exchange [%s]', QUEUE_NAME, config.exchange_name);

// 消费
return Consume
.consume(self.connection, ch, QUEUE_NAME);
})
})
.catch(function (err) {
log.error('[mq] Init failed , error: ', err);
self.reconnect();
});
};

/**
* 重新连接
* @return {[type]} [description]
*/
MQ.prototype.reconnect = function() {
var self = this;

log.info('[mq] try reconnect 3 seconds later');

setTimeout(function () {
self.init();
self.reconnectCount++;
}, 3000);
}

/**
* 获取连接
* @return {[type]} [description]
*/
MQ.prototype.getConnection = function getConnection() {
var self = this;

if (this.connection) {
return Promise.resolve(self.connection);
} else {
return Connection
.createConnection(config.rabbitMQ_url)
.then(function (conn) {
// 实例中存储当前连接
self.connection = conn;

// 进程被杀死关闭连接
process.once('SIGINT', function() {
log.info('kill by signal SIGINT');
conn.close();
self.connection = null;
process.exit(0);
});

log.info('[mq] create connection success');

return Promise.resolve(conn);
});
}
}

module.exports = MQ;

· One min read

Promise.coroutine

可以像co一样包裹generator函数,进行使用yeild的异步操作。

使用

Promise.coroutine(GeneratorFunction(...arguments) generatorFunction) -> function

示例

var Promise = require("bluebird");

function PingPong() {

}

PingPong.prototype.ping = Promise.coroutine(function* (val) {
console.log("Ping?", val)
yield Promise.delay(500)
this.pong(val+1)
});

PingPong.prototype.pong = Promise.coroutine(function* (val) {
console.log("Pong!", val)
yield Promise.delay(500);
this.ping(val+1)
});

var a = new PingPong();
a.ping(0);
Running the example:

$ node test.js
Ping? 0
Pong! 1
Ping? 2
Pong! 3
Ping? 4
Pong! 5
Ping? 6
Pong! 7
Ping? 8
...

· One min read

全局安装了无法找到命令

大家都知道,FIS 是要求全局安装的,是因为避免由于 FIS 多版本不同项目目录下而导致编译时有差异,而导致不必要的麻烦。

有些同学可能遇到了

npm install -g fis

命令行执行 fis 说找不到这个命令。这时候一般都开始抓瞎了。

解决办法:

  • 执行 npm prefix -g 会输出全局安装路径
  • Windows 用户把输出的路径添加到环境变量 %PATH% 里面,环境变量的设置请参考 百度
  • 类 Unix 用户把 $(npm prefix -g)/bin 目录设置到 PATH 中。
    • bash echo -e "export PATH=$(npm prefix -g)/bin:$PATH" >> ~/.bashrc && source ~/.bashrc
    • zsh echo -e "export PATH=$(npm prefix -g)/bin:$PATH" >> ~/.zshrc && source ~/.zshrc

· One min read

需要cookie和session的测试案例

在web开发中,Cookie有着非常重要的作用。因为HTTP是无状态的,所以需要用cookie来辅助实现用户认证。我们先来简单介绍一下cookie的工作机制。

untitled1.png

如果所示,如果通过cookie和session协同识别一个用户需要两次请求,第一次请求的时候,服务器并不认识你,但是他给你标记了一个他独有的id,等到第二次请求的时候,浏览器自动给你带上了之前的标签,这样服务器就知道你之前请求过了。

那么问题来了,如果我们写测试案例的时候,需要两次请求来实现的话,会非常麻烦,测试案例也会很冗长。怎么才能一次请求就能使用cookie和session呢?

这时候express的中间件的好处就体现了。 首先,我们在用supertest进行HTTP请求的时候,可以通过下面的形式设置cookie:

set('Cookie', cookieValue)

然后,我们写一个非常简单的中间件:

app.use(function(req, res, next) {
if (config.debug && req.cookies['mock_user']) {
var mockUser = JSON.parse(req.cookies['mock_user']);
req.session.user = new UserModel(mockUser);
return next();
}
next();
});

原理就是先判断当前是否为开发环境,通过config来设置,通常在开发阶段这个值设置为true。其次判断是否具有键为mock_user的cookie键值对,如果存在,设置session里面的user值,这样,只要一次请求我们就能实现用户标识。 最后要解决的问题就是怎么设置字段键为mock_user的cookie了,具体的用法可参照test目录里面的support/support.js,这里不多说。


补充

测试时加上

.set('Cookie', cookie)

那么这个cookie是什么形式呢?

Cookie: mock_user=xxxxxxxxxx

var cookie = [ 'mock_user', encodeURIComponent(JSON.stringify(support.user))].join('=');