Skip to main content

6 posts tagged with "nodejs"

View All Tags

· 5 min read

女朋友正在学习python,写的脚本都是从stdin输入,从stdout输出的;而我又是一个js全栈,感觉写一个简单的python脚本调用的界面+nodejs微服务应该很有意思。

#demo http://ali.wusisu.com/sun/

child_process

首先我要先实现在nodejs中调用python,并且要转发输入输出流。

'use strict'
var child_process = require('child_process')
var cp = child_process.spawn('python3', ['./files/circumference.py'])
cp.stdout.pipe(process.stdout)
process.stdin.pipe(cp.stdin)
cp.on('close', (code) => {
process.exit(code)
})

做的事情简单清晰易懂:

  1. 用child_process的spawn调用一个python3进程cp。
  2. 转发cp的标准输出到当前程序的标准输出(命令行)。
  3. 转发当前程序的标准输入到cp的标准输入。
  4. 监听cp的结束事件。

结果是调用这个node wrapper.js跟直接调用python3 ./files/circumference.py没有任何区别。

曲折

事实上开发的时候走了点弯路的。 我很幼稚的认为,使用Content-Type: text/event-stream能够解决问题。 只要client端发起一个POST请求,但不马上结束连接,而是持续的发送内容;server端使用event-stream持续地返回内容。 事实上我已经把模拟的client端和server端写好了,也完全达到了目标效果,然并卵,在js里没法用ajax慢慢地发POST请求。

event-stream: 告诉client/browser服务器会慢慢地返回内容,你收到多少直接拿去用就好。

socket.io

socket.io也并没有什么需要说的了,就简单推荐一下,好用。

websocket

在socket.io之前我考虑过直接上websocket,因为之前用event-stream实现的时候并没有使用第三方的包,直接用原生的http然后把streampipe出去可爽了。于是就想着原生上websocket。 nodejs的文档其实提到了一下websocket的。文中让我响应upgrade事件然后可以拿到一个socket然后可以pipe给它自己。 然后就没有更多关于怎么发起一个websocket的资讯了。然而在google/baidu等搜索引擎中,并未能找到关于websocket足够给我用的实践,全都在不清不楚地大概说websocket有什么好处,像什么样子。(如果以后有空,自己实践一下,再上文章) 反倒是有了新的东西,在要求upgrade的时候要带一个base64的key,然后在服务器response的时候要给另外一个encrypted的base64。然后还不说这个key是干什么的,简直就是各种不清不楚。

文件遍历/Promise

之前习惯用stage-2async来写同步执行。不过现在babel还没有全面进入node-v5.7.1,要用async需要引入许多babel包和配置,正好前面都做比较原生的事情,于是想着用原生来写。 首先考虑了callback写法,遍历./files目录后调用回调。结果发现Promise.all还是做了不少事情,把这个写到我的递归函数中还是比较难看的,于是还是上Promise吧。

var fs = require('fs')
var lookForwardFiles = function(path){
return new Promise((res,rej)=>{
fs.stat(path, (err, stat)=>{
if(err) return res([])
if(!stat.isDirectory()) return res([path])
fs.readdir(path, (err, files)=>{
if(err) return res([])
var allP = files.map(f=>lookForwardFiles(path + '/' + f))
Promise.all(allP).then(data=>res([].concat.apply([],data)))
})
})
})
}

node支持了arrow functions还是蛮爽的,就是用=>来代替'function'。 这里用到了一个技巧[].concat.apply([],data),可以把形如[[1,2],[3],[4],5]的data转化为[1,2,3,4,5]。 其实也可以用

data.reduce((all,d)=>all.concat(d),[])

来代替。

commit

给个当前commit的链接,万一以后不用socket.io上原生websocket了呢。

· 3 min read

做服务器开发,必不可少的一个模块是日志系统。 在Java界,有大牛log4j,感觉都可以毫无顾虑地直接选择这个模块。但是在nodejs就没有这样事实上的标准了。 于是我花了些时间研究哪个nodejs日志模块好用。

结论

最后我选中的日志插件是

  • bunyan: a simple and fast JSON logging module for node.js services
  • morgan: HTTP request logger middleware for node.js

评价

morgan

morgan是expressjs默认带着的超简单的日志系统,专门用来生成http的请求记录。其生成的日志个人感觉于Nginx的日志十分类似。用来产生一个http请求日志再好不过了。 使用非常简单:

import morgan from 'koa-morgan'
export const loggerMiddleware = morgan('combined')
app.use(loggerMiddleware)

我这里用的是koa2,所以用封装过的morgan来写。

bunyan

bunyan是一个会产生JSON格式的日志的日志模块。 其实可读性不高。其提供了把json转成易读性高日志的阅读工具,但是总觉得不太实用。 不过当我们把它作为一种记载重要操作的操作记录工具(这属于业务功能而不是技术功能了)时,就挺合适使用了。

const Writable = require('stream').Writable
const writeStream = new Writable({ objectMode: true })
const DEBUG = process.env.DEBUG
writeStream._write = function(log, encoding, next){
if(log.level>40) console.error(log)
if (DEBUG && log.level<=40) {
console.log(log)
}
if (log.level<30) return
let item = new Model
item.obj = log
item.save(next)
}
import bunyan from 'bunyan'
export const logger = bunyan.createLogger({
name:'app',
src: true,
streams: [{
level: 'trace',
type: 'raw',
stream: writeStream
}]
})

写入一个记录的方式是:

logger.info({message:'hello world!'})

这里用的比较复杂。本来bunyan的那个streams直接放console或者文件流是非常简明的,在这里我创建了一个writeStream来接受数据流,然后再写一个回调来把数据流存入mongodb中。这样的用例在业务上是非常常见的。

· 6 min read

nodejs很适合做http数据服务器,用上express之类的框架很容易就搭出一套http service来。然后数据库感觉就跟mongodb很搭,这样子一套下来全都是javascript技术栈。但是nodejs跟mongodb之间的接口应该选择哪一个呢?

我关注到的库有两个:node-mongodb-nativemongoose

node-mongodb-native是mongodb的官方nodejs封装。其实应该说是简单封装。使用它的感觉就像是直接在shell里用mongo操作,特别的灵活以及效率。甚爱之。
mongoose是一个深封装的ORM库。需要像mysql设计表结构那样子设计Schema,然后把Schema具象为Model,然后可以面向对象的使用数据库了。


我是更爱高效灵活地node-mongodb-native的,然而使用之有一点不好,就是:感觉这个项目的team并没有花多少心思和时间在这上面,更多地是应付的做了一个专业的封装。
我这是什么意思呢?
node-mongodb-native库的封装很专业,但是文档是比较应付式的,阅读和资讯获取都挺难,至少我做了近一个月还是不知道web service应该怎样标准地去连接数据库(应该说怎么用数据库,连接一次然后一直使用这个db呢;还是每次用db.opendb.open是不是表示从连接池里拿一个连接?)。其开发人员是从对接口的封装出发来做这个项目,而不是从用户使用的角度来开发和写文档。
我在网上看人家对这两者的比较时看到他们说,大概意思是使用node-mongodb-native会比较折腾。
我现在认为这里的折腾不是指写起来麻烦,而是没有响应的指导和合适的文档,许多事情需要去读源码,去做测试(这种测试还不好做,也许某种用法低并发的时候适用,多了就不行了)。

这个项目的开发人员总该知道怎样使用吧!你倒是做个demo或者在doc中写出来啊!你倒是告诉我是不是针对每一个http请求都应该发起一个db的connection啊!你告诉我是不是对每一个http请求不用发起新connection但是需要重新db.open啊!你不说谁知道在web service中应该怎么用这个库啊!!!文档里每一个demo都是连接数据库-打开db-写入-关闭连接。也是醉了。

最后的结果是遇到不知名原因的bug,一个collection.find返回的promisepending,然后找不到原因和答案,放弃之。


mongoose这种把动态类型当作静态类型的思路其实我是不喜欢的(其实不是静态类型,只是做类型转换啦,我是说我不喜欢这样的思路而已)。就像我不喜欢微软的TypeSrcipt(包括ES6)给javascript弄上静态类型和class一样,人家javascript明明本身是基于prototype设计的,硬是弄上class搞毛!
但是mongoose的开发看来是熟知node-mongodb-native的源码的,这二次封装(在node-mongodb-native的基础上进行封装)然后提供了较为完善的文档,也算是一个合适的选择。

这里有两个点需要注意一下:

  1. 我现在使用的mongoose版本是4.2.5,其依赖"mpromise": "0.5.4"。也就是说mongoose返回的Promisempromise提供的promise实现,而这个promise实现实测不支持promise.reject方法。
  2. 同样是这个版本。我使用文档里说的方法mongoose.model('User', userSchema);注册Model。这个时候model是被注册到mongoose.models的。于是在mocha -w的时候,会出现OverwriteModelError,估计是mocha -w的时候里面的机制并不会清空mongoose.models,然后重载model注册模块,于是报错。根据issue#1251,网友提供的mongoose.models = {};mongoose.modelSchemas = {};似乎可以解决这个问题。比较逗的是aheckmann拼死不认为这是一个bug,一直说我们用错了。没太注意看,不过也许用connection.model('User', userSchema);来注册也能解决这个问题,这也许是aheckmann的意思。

· One min read

我做爬虫的时候,遇到一个问题:我想把页面的标题作为文件夹名字,结果出错了,仔细一看,发现标题里含有字符/

使用windows的人应该都遇到过,给一个文件命名带有?的名字时,他就会告诉你有七八个字符是不能作为文件名的。
所以写一个函数把这些字符滤掉。

var makeFilenameValid = function (rawFilename) {
var InvalidCharRegex = /[\/:\*\?\"< >\|]/;
var okFilename = '';
_.each(rawFilename, function (char) {
var match = InvalidCharRegex.test(char);
if (!match){
okFilename += char;
}
});
return okFilename;
}

· One min read

由于浏览器里存在跨域问题,之前一段时间各种各样的跨域手法都出来了。之后却突然都销声匿迹了,估计是因为现在的浏览器都支持cors模式来解决跨域问题,而许多新的服务器都开启了这样子的服务。看了一眼发现开这样的服务原来这么简单!

cors的原理挺简单的,弄一个Access-Control-Allow-Origin字段在header里就差不多了。但是在express下使用还有更简单的

npm install cors

然后再express的app那里:

var cors = require('cors');
app.use(cors());

就好了。

· 3 min read

这是关于如何在nodejs环境下,发起http请求时带上cookie的教程。

其实我用python写爬虫/发起http请求会熟手一些,不过nodejs作为一个能开发命令行程序的引擎,我又想多熟练js的编程,于是就试着用nodejs来做一个爬虫。
也是我对cookie的原理不熟悉吧,爬虫带cookie的时候,我试着在header上通过setHeader设置Cookie属性a=123; b=456这样子,但是服务器不认,也不知道是为什么。但是看到好像是说服务器可以设置不让本地读和写cookie,不知道是不是相关。

我设置header不行,但是人家可以啊!我使用的是:

npm install request

文档里有cookie的使用说明

request.cookie
Function that creates a new cookie.
request.cookie('key1=value1')

以及

var j = request.jar();
var cookie = request.cookie('key1=value1');
var url = 'http://www.google.com';
j.setCookie(cookie, url);
request({url: url, jar: j}, function () {
request('http://images.google.com')
})

看起来知道应该使用jar来组一个cookie然后扔给http请求使用就好了。
但在这里有一个要注意的坑在,就是 var cookie = request.cookie('key1=value1'); 一句,谁家的cookie只有一个property的啊!于是我就 var cookie = request.cookie('key1=value1; key2=value2'); ,然后服务器不认。
苦心孤诣地尝试很多遍之后,才挖掘出来应该是:

var cookie;
cookie = request.cookie('key1=value1');
j.setCookie(cookie, url);
cookie = request.cookie('key2=value2');
j.setCookie(cookie, url);

文档不能写清楚一点吗!

扔上代码,url是比较敏感的所以改成example了。

var collectionHost = 'www.example.com';
var collectionDomain = 'http://' + collectionHost;
var jarWithCookie = request.jar();
var cookieString = 'key1=value1; key2=value2; key3=value3; key4=value4; key5=value5';
for (kv of cookieString.split('; ')) {
jarWithCookie.setCookie(request.cookie(kv), collectionDomain);
}

var getBufferWithHeader = co.wrap(function *(url) {
return new Promise(function (resolve) {
var buffer = [];
request({url, jar: jarWithCookie}, function (error, response, body) {})
.on('data', function (chunk) {
buffer.push(chunk);
})
.on('end', function () {
buffer = Buffer.concat(buffer);
resolve(buffer);
});
})
});

其中,cookieString可以轻松从chrome的dev工具中搞出来。