一.node知识
1.回调函数,即异步IO的node处理方式:时间循环,观察者,请求对象,io线程。
先在线程池处理函数,函数处理完了之后对应不同操作系统置标志位,事件循环的观察者一直在while(true)循环观察各个标志位,一批一批的拉,直到这个事件结束了查看这个函数有没有回调函数,有的话执行回调函数。
对于不同操作系统,事件完成时发送的信号,linux下面是通过epoll进行的,而windows下是通过内核(IOCP)实现的。
2.settimeout()
,process.nextTick()
,setInterval()
,setImmediate()
settimeout
和setInterval
实际上是相同的,不一样在于第一个是一次执行,而第二个是多次执行。
原理:设定时间的时候,在红黑树中将这个定时器插入到一个贯彻着内部的一个红黑树中,系统有个Tick定时器,每次Tick执行时候,会拿出这个红黑树中的定时器节点,看是否超过时间,超过就执行。所以不是精确的时间。
对于立即执行的任务,有些人会使用setTimeout(function(),0)
来进行,而这个方法的经度不够,并且要使用红黑树,比较重。这时候可以采用process.nextTick()
进行取代。
setImmediate
和nexttick
比较类似,也有一些细小的额差别,就是nexttick
的使用idle观察者,优先级高于I/O观察者,回调函数放在数组中,而setImmediate
使用check观察者,优先级低于I/O观察者,回调放在链表中。最终的区别就是nexttick
的优先级高于setImmediate
。
自己写的一个倒计时器
//倒计时器
var jb=setInterval(function(){
console.log(time)
time--;
if(time==0){
clearInterval(jb);
}
},1000);
3.node中发布订阅模式
采用自带的events
库,使用:
var events = require('events');//引入
var x =new events.EventEmitter();//创建实例
x.on('y', function(a,b,c){
console.log('it\'s work1!'+a+b+c);
});//订阅一个字段,可以是多个
x.emit('y','111','222', '3333');//发布一个字段
//注意:需要先订阅再发布
也可以将on改成once,代表只接收一次这个消息
events类似于android中的handle,massage和post
4.回调函数
用一个事例来讲
var i = 0;
function sleep(ms, callback) {
setTimeout(function () {
console.log('我执行完啦!');
i++;
if (i >= 2) callback(new Error('i大于2'), null);
else callback(null, i);
}, ms);
}
sleep(3000, function (err,val) {
if(err) console.log('出错啦:'+err.message);
else console.log(val);
})
上面,将callback函数通过高阶函数,参数的方式传入进去,然后再在里面直接调用,外面就能够获取到数据了。
5.node的异步编程,其中有Promise和Async/Await
events能够解决代码之间的耦合,使得事件的写法不在一块。对于流程化的,分模块之间的额异步编程,就必须使用新的框架。
建议:异步的优先级过程:async/await > promise > 事件events > 回调
对于上面sleep的函数,我想在成功之后再执行一次,就必须在sucess中嵌套写代码,多次呢,就要写多次。而使用promise可以这样写:
var i = 0;
//函数返回promise
function sleep(ms) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('我执行好了');
i++;
if (i >= 2) reject(new Error('i>=2'));
else resolve(i);
}, ms);
})
}
sleep(1000).then(function (val) {
console.log(val);
return sleep(1000)
}).then(function (val) {
console.log(val);
return sleep(1000)
}).then(function (val) {
console.log(val);
return sleep(1000)
}).catch(function (err) {
console.log('出错啦:' + err.message);
})
返回一个Promise的对象,对象中还是采用回调,第一个参数是resolve,第二个参数是reject,第一个代表成功,第二个代表错误。
然后再调用then一步步处理,使用一个大的catch来处理错误。这样解决了函数嵌套的问题,可是还是没有解决函数回调的问题呀,回调的问题使用事件events可以解决,不使用回调,而是在成功挥着失败后直接抛出事件,然后再在别的地方捕获就行。
这时候async/await出现了。
async/await在node7.0中出现,需要使用harmony模式运行,在7.6以上就能够直接使用了。
定义一个异步函数:
async function fn(){
return 0;
}
其实返回的是一个Task(Promise)
await写在async中,类比C#, promise其实就是C#中的Task,async和await和C#中的async/await使用一样。
所以,只要是Task就能够await,而不一定是async返回的函数才能await。比如,一个http request返回的是一个promise,就能够进行await进行同步,或者一个settimeout的函数,返回的是promise,也能记性await进行同步,如下:
var i = 0;
//函数返回promise
function sleep(ms) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('我执行好了');
i++;
if (i >= 2) reject(new Error('i>=2'));
else resolve(i);
}, ms);
})
}
(async function () {
try {
var val;
val = await sleep(1000);
console.log(val);
val = await sleep(1000);
console.log(val);
val = await sleep(1000);
console.log(val);
}
catch (err) {
console.log('出错啦:'+err.message);
}
} ())
sleep函数是一个异步方法,需要执行一定时间之后才有返回。
- 结论:使用Promise处理异步函数,使用async/await处理异步函数的同步和步骤控制问题。
写一下Promise的最基本用法:
var promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
6.Promise使用补充
使用resolve和reject进行数据传出,里面不用使用return字段,直接使用resolve(数据)和reject(数据)就能够传出,所以不管你的调用函数里面有回调有嵌套,都不管,该返回正确值的时候直接调用reslove()就行,有错误直接reject()就行啦!
另外,而async/await的作用是当你要调用这个promise的方法时,可以不使用then这样一步一步,而是使用await来控制进度,其本质是通过promise实现的。
7.node的class机制
node是为服务端进行的,所以使用面向对象是很关键的。现在node能够直接使用class进行对象操作
举例:
class HondClass{
constructor(name){
this.name = name;
}
setName(name){
this.name=name;
}
getName(){
return this.name;
}
}
module.exports = {
INIT : new HondClass("hond"),
}
这个js定义了一个class HondClass,导出了一个INIT初始化对象实例,使用constructor进行构造。
在另一个js中,
var HondClass =require('./HondClass.js');
可以获得这个对象,这种方式可以作为单例。
一般的,不使用单例,可以这样:
class HondClass{
constructor(name){
this.name = name;
}
setName(name){
this.name=name;
}
getName(){
return this.name;
}
}
module.exports = HondClass;
然后在使用的时候
var HondClass =require('./HondClass.js');
var hond1 = new HondClass("namesadas");
console.log(hond1.getName())
这样的使用习惯和面向对象的C#和java一致。当然,class只是语法糖,es6之前,都是通过function创建class的,类似下面这样:
function HondClass(name){
this.name=name;
this.getName=function(){
return this.name;
}
}
module.exports = HondClass;
这个格式是es6之前的格式,很有js风格。而es6之后就简洁的多。其实差不多就是把function改成class,然后把输入放到construstor中去。
而调用就是一模一样。
8.node的buffer
buffer是字节数组的意思,那js对string支持这么好,为什么不直接使用string而要使用buffer呢,原因在于在网络传输中,最终都是要使用字节流进行数据传输,如果事先将一些固定的返回值变成字节流,那么就能够减少将string转换为buffer的时间消耗了。
demo代码如下:
var str1 = "hongyangdu";
//string --> buffer
var buf1 = new Buffer(str1,'UTF-8');
console.log(buf1)
//重写部分内容,覆盖原来的0-4位[)
buf1.write("12345",0,4,'UTF-8')
//buffer --> string
var str2 = buf1.toString('UTF-8');
//buffer拼接
//Buffer.concat(chunks,size)//参数是想要拼接的buffer list,size是想要拼接成的最终buffer的字节数
//举例
chunks=[]
chunks.push(new Buffer("1111",'UTF-8'));
chunks.push(new Buffer("2222",'UTF-8'));
var ss = Buffer.concat(chunks,8);
console.log(ss)
9.讲到这里,中途插一个node的使用场景
node是异步IO和无阻塞IO的代表,处理io密集项目很在行,当然因为其使用了C++的内核,所以处理cpu密集的也不差。《深入浅出nodejs》书中的斐波那契测试就验证了。
比较合适的使用场景:
(1)实时应用,比如在线聊天,实时推送等等,可以使用socket.io进行websocket推送
(2)restful的api。restful的api的大部分时间都在从io接口请求数据,然后从数据库拿数据,再将数据交给用户。或者往数据库写数据,然后异步返回结果。
(3)前后端统一的环境。毕竟node是架构在javascript上的语言,而前端用的就是js,所以如果想在前后端的数据上面进行完全统一,使用node是个不错的做法
(4)有大量ajax请求的应用,比如定位上传,地图应用等等。
10.网络请求
(1)自带的http和https
这里就不说了,感兴趣的可以去官网看事例。这里主要介绍一个强大的第三方库。
(2)第三方库request
request的github
基本使用:
var request = require('request');
request('http://www.google.com', function (error, response, body) {
console.log('error:', error); // Print the error if one occurred
console.log('statusCode:', response && response.statusCode); // Print the response status code if a response was received
console.log('body:', body); // Print the HTML for the Google homepage.
});
表单提交
采用:application/x-www-form-urlencoded
的Content-type
有三种方法:
request.post('http://service.com/upload', {form:{key:'value'}})
// or
request.post('http://service.com/upload').form({key:'value'})
// or
request.post({url:'http://service.com/upload', form: {key:'value'}}, function(err,httpResponse,body){ /* ... */ })
对于文件提交:
request.post({url:'http://service.com/upload', formData: formData}, function optionalCallback(err, httpResponse, body) {
if (err) {
return console.error('upload failed:', err);
}
console.log('Upload successful! Server responded with:', body);
});
然后再formdata里面写上一些参数,如下:
var formData = {
// Pass a simple key-value pair
my_field: 'my_value',
// Pass data via Buffers
my_buffer: new Buffer([1, 2, 3]),
// Pass data via Streams
my_file: fs.createReadStream(__dirname + '/unicycle.jpg'),
// Pass multiple values /w an Array
attachments: [
fs.createReadStream(__dirname + '/attachment1.jpg'),
fs.createReadStream(__dirname + '/attachment2.jpg')
],
// Pass optional meta-data with an 'options' object with style: {value: DATA, options: OPTIONS}
// Use case: for some types of streams, you'll need to provide "file"-related information manually.
// See the `form-data` README for more information about options: https://github.com/form-data/form-data
custom_file: {
value: fs.createReadStream('/dev/urandom'),
options: {
filename: 'topsecret.jpg',
contentType: 'image/jpeg'
}
}
};
一般正常的使用如下:
var request = require('request');
var options = {
url: 'https://api.github.com/repos/request/request',
headers: {
'User-Agent': 'request'
}
};
function callback(error, response, body) {
if (!error && response.statusCode == 200) {
var info = JSON.parse(body);
console.log(info.stargazers_count + " Stars");
console.log(info.forks_count + " Forks");
}
}
request(options, callback);
可以自己添加header,然后返回是json,使用options和callback分离request和response,
使用request.post和request.get分别代表get和post操作。
如果需要返回值,就使用第三种方法
11.websocket
推荐一篇文章通俗易懂
websock是客户端和服务器之间的全双工通信,使得服务器能够主动向客户端发送信息。
以前的流程是这样的:
- sync
Note left of Client: blockServer-->Client: Response
- async
异步就需要每隔一段时间去轮询一次数据,直到得到数据
而websocket能够由某一方主动发出数据,这样客户端请求数据,server有数据的时候就会自己向客户端推送。
websocket流程:浏览器通过JavaScript向服务端发出建立WebSocket连接的请求,在WebSocket连接建立成功后,客户端和服务端就可以通过 TCP连接传输数据。因为WebSocket连接本质上是TCP连接,不需要每次传输都带上重复的头部数据,所以它的数据传输量比轮询和Comet技术少了很多。
node.js中的websocket
现在socket.io是比较火的node.js中国的websock框架,下面简单介绍下其使用:
官网
官网里面有一个聊天程序,是一个非常好的例子。
(1).安装socket.io,express等
(2) 最简单的demo:
var app = require('express')();
var http = require('http').Server(app);
var io = require('socket.io')(http);
app.get('/', function(req, res){
res.sendfile('index.html');
});
//下面就是socketio用法,有链接时打印connected
io.on('connection', function(socket){
console.log('a user connected');
});
http.listen(3000, function(){
console.log('listening on *:3000');
});
(3)从web前端发送数据到server:
首先是前端代码:
<script src="/socket.io/socket.io.js"></script>
<script src="http://code.jquery.com/jquery-1.11.1.js"></script>
<script>
var socket = io();
$('form').submit(function(){
socket.emit('chat message', $('#m').val());
$('#m').val('');
return false;
});
</script>
就是使用jquery提交了一个表单,表单里使用socket.emit
来进行数据提交,提交用的是key-value形式。
然后再node的后端,
io.on('connection', function(socket){
socket.on('chat message', function(msg){
console.log('message: ' + msg);
});
});
使用socket.on来获取这个数据,完成了单向的数据传输
(4) 从server到web,如果是所有用户,
向每一个用户都发送一个event,则
io.emit('some event', { for: 'everyone' });
如果想发送给所有人(不包括自己),则
io.broadcast.emit('some event', { for: 'everyone' });
如果针对事件发送,则
socket.emit(event",'message');
双方一样,发送用emit,监听使用on.
socket.io的其他方法
set: 保存会话数据,get:获取临时会话数据,使用就如hashmap一般
demo
Server:
var app = require('express').createServer();
var io = require('socket.io')(app);
app.listen(80);
app.get('/', function (req, res) {
res.sendfile(__dirname + '/index.html');
});
io.on('connection', function (socket) {
socket.emit('news', { hello: 'world' });
socket.on('my other event', function (data) {
console.log(data);
});
});
client:
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io.connect('http://localhost');
socket.on('news', function (data) {
console.log(data);
socket.emit('my other event', { my: 'data' });
});
</script>
12.express中间件
参考网站:使用中间件
简单的中间件:
// 没有挂载路径的中间件,应用的每个请求都会执行该中间件
app.use(function (req, res, next) {
console.log('Time:', Date.now());
next();
});
// 挂载至 /user/:id 的中间件,任何指向 /user/:id 的请求都会执行它
app.use('/user/:id', function (req, res, next) {
console.log('Request Type:', req.method);
next();
});
// 路由和句柄函数(中间件系统),处理指向 /user/:id 的 GET 请求
app.get('/user/:id', function (req, res, next) {
res.send('USER');
});
中间件可以传递下去,如下:
app.get('/user/:id', function (req, res, next) {
console.log('ID:', req.params.id);
next();
}, function (req, res, next) {
res.send('User Info');
});
因此,可以将公共的中间件使用var定义下来,需要的时候加进去就好了。
错误处理中间件:
app.use(function(err, req, res, next) {
console.error(err.stack);
res.status(500).send('Something broke!');
});
像cookie,auth验证,路由清洗,日志,错误处理。cookie解析中间件可以使用插件cookie-parser
13.多进程
为什么要使用多进程呢?node是事件驱动,单用单线程处理,避免了一些切换和开销。
而如果有好几个独立的处理任务,有多个处理器,那么不使用多处理器不就是浪费吗?这时候就可以采用多进程。每一个进程都是一个独立的空间和存储,基本使用:
main.js
var cp = require('child_process');
var n = cp.fork('./sub.js');
n.on('message', function(m) {
console.log('PARENT got message:', m);
});
n.send({ hello: 'world' });
sub.js
process.on('message', function(m) {
console.log('CHILD got message:', m);
});
process.send({ foo: 'bar' });
child_process
有四个方法,spawn()
,exec()
,execFile()
,fork()
。对于js文件,直接执行fork()
,对于文件,执行execFile
,可以自己添加命令和回调。
对spawn是最简单的,exec
在spawn
上面加了一个回调。这个过程是异步的,如果需要同步,则使用execSync
,execFileSync
和spawnSync
node的cluster模块能够方便地进行worker的分配,非常不错。
14.知识积累
在request中,有时候返回的body是string形式,有时候返回的body是object形式,能够直接当json使用,因此最好事先判断下。
二.读nodejs基础
1.工程目录
node和javascript其实没有本质的关系,即和android与java没有本制度额关系一样。node只是代表了一种时间机制和io模型的框架结构,而javascript只是代表一种语言,使用commonjs来实现这么一种机制的框架是node。
一个node项目的目录结构一般如下:
- /home/user/workspace/node-echo/ # 工程目录
- bin/ # 存放命令行相关代码
node-echo
+ doc/ # 存放文档
- lib/ # 存放API相关代码
echo.js
- node_modules/ # 存放三方包
+ argv/
+ tests/ # 存放测试用例
package.json # 元数据文件
README.md # 说明文件
其中bin下面存放的命令行代码,和下面这这样差不多:
/* bin/node-echo */
var argv = require('argv'),
echo = require('../lib/echo');
console.log(echo(argv.join(' ')));
即提取命令行用户输入,这个命令行在server端几乎没什么用,只对某些调试或者中间查看的js有些用。
2.package.json
一个package.json长成下面这样:
{
"name": "node-echo", # 包名,在NPM服务器上须要保持唯一
"version": "1.0.0", # 当前版本号
"dependencies": { # 三方包依赖,需要指定包名和版本号
"argv": "0.0.2"
},
"main": "./lib/echo.js", # 入口模块位置
"bin" : {
"node-echo": "./bin/node-echo" # 命令行程序名和主模块位置
}
}
3.文件操作
node是从v8而来,因此默认的内存可使用很小。对于一般我们的想法,例如拷贝文件,先从一个文件的file编程stream,然后再将stream编程第二个file。
程序如下:
var fs = require('fs');
function copy(src, dst) {
fs.writeFileSync(dst, fs.readFileSync(src));
}
这种做法是很吃内存的使用管道传递stream会大大降低内存的使用量。
var fs = require('fs');
function copy(src, dst) {
fs.createReadStream(src).pipe(fs.createWriteStream(dst));
}
对于那些无法一次装下需要的数据,或者为了节约内存(建议),则使用stream流进行处理。stream流不会将数据全部读进内存,只会在里面存一段,然后存下文件的初始位置,以及目前读到的位置开始和结尾。
有人有疑问了,数据流的读写,读和写的数据不一样,怎么保证读出来的数据能够一定送达到写的文件中去呢。其实在pipi管道内部已经封装好了,类似下面这样的:
var rs = fs.createReadStream(src);
var ws = fs.createWriteStream(dst);
rs.on('data', function (chunk) {
if (ws.write(chunk) === false) {
rs.pause();
}
});
rs.on('end', function () {
ws.end();
});
ws.on('drain', function () {
rs.resume();
});
stream内部是继承了EventEmitter的,所以可不有许多消息机制在里面。采用等待通知消息的形式一步一步下去。
fs模块的基本方法:
fs.start,
fs.chmod,
fs.chown,
fs.readFile,
fs.readdir,
fs.writefile,
fs.mkdir,
fs.open,
fs.read,
fs.write,
fs.close
并且所有fs的操作都提供两个回调参数,一个error,一个是data,便是当前操作需要的data,具体的data与具体函数相关。
4.querystring模块
querystring模块将post上传的(urlencode格式)数据变成json的object,比如下面:
querystring.parse('foo=bar&baz=qux&baz=quux&corge');
/* =>
{ foo: 'bar', baz: ['qux', 'quux'], corge: '' }
*/
非常实用