前言
刚刚看完了《Node.js入门》,参考node官网和一些相关文章,终于感觉和node亲近了一步,迫不及待的想写篇文章分享一下,希望本文的读者读完后像读了一本好书一样,获益良多。
ps:《Node.js入门》一书仅有38页,是一本很好的入门书籍,如果你也是刚上手node,和我一样手中有一本《深入浅出node.js》相似的高级书籍,但感觉读起来晦涩难懂,不如试试《Node.js入门》吧,会感觉豁然开朗,
借用《Node.js入门》中的编程应用,稍作修改,加上自己的一些思考,讲一下“异步,事件驱动与回调,非阻塞”这些使node.js轻便和高效的特点。特点穿插在应用中讲解。如有错误,欢迎指教!
应用目标:
• 用户可以通过浏览器使用我们的应用。
• 当用户请求http://domain/start时,可以看到一个欢迎页面,页面上有一个 文件上传的表单。
• 用户可以选择一个图片并提交表单,随后文件将被上传到 http://domain/upload,该页面完成上传后会把图片显示在页面上。
实现思路
先来思考一下怎样实现这样一个应用呢?
--啊,要实现应用了,我我...还不知道代码写在哪里,
首先我们要用node跑起js来。
--看用户的请求,这个应用是是跑在服务器的,node要链接服务器吗?
node中可以加载‘http’模块,可以让我们轻松的搭建服务器,真的一点也不难哦。
--还可以学习模块的加载,简直是福利多多,咦,请求页面和显示页面不是一个路径,这个要怎么实现呀?
我们可以创建一个路由模块,对不同的请求有不同的处理,再用总文件将请求路由到相应的处理程序。
--哇哦,有了这个路由机制,我们就可以将start 的路由处理程序设置为上传图片,将upload的处理程序设置为显示图片了
没错,大体的思路就是这样,让我们开始上手吧!
应用实现:
跑起node
首先要让node跑起js来
我们知道,node.js是一个基于chrome V8的一个javaScript运行环境,所以我们这里说的是让node.js跑起js来,而不是让node.js跑起来。
我们先创建一个 helloworld.js 程序,内容为:
console.log("Hello World");
然后在命令行终端中用 node 运行本文件:
node helloworld.js
终端中就会运行出js的结果:Hello World
创建服务器
接下来我们来创建服务器,创建一个server.js文件:
var http = require("http");//引入node内置的‘http’模块
http.createServer(function(request, response) {//创建一个服务器,并在创建方法中规定服务器触发请求的回调函数
response.writeHead(200, {"Content-Type": "text/plain"});//规定响应头部信息(状态码和文本格式)
response.write("Hello World");//设定了响应的内容
response.end();//响应结束
}).listen(8888);//设定服务器监听8888端口。
console.log("Server has started.");
用node跑一下:
node server.js
终端的输出结果为:
Server has started.
Request received.
浏览器端访问:http://localhost:8888/ 显示结果为:
Hello World
代码解读:
1.模块调用
我们把一定实现一定功能的代码放在一个文件里,这个文件也就是一个模块,要实现模块间的调用,就需要模块导出和模块引入,
这里‘http’模块是node内置的,他自身有导出功能,(一会我们还要自己实现模块的导出功能),我们引入了‘http’模块,便能应用它的功能了。
2.事件回调
http的createServer方法,需要传入一个参数,这个参数是一个函数(js中函数是可以作为参数传递的),当请求事件发生时便会触发这个函数,这便是回调函数,回调函数可以是匿名的(就像我们这里一样),也可以是命名的。
事件回调就是:知道有一些事情随时可能发生,回调函数时刻准备着对将要发生的事件作出响应
3.http相关解读:
createServer函数参数解读:
createServer的函数参数中有两个参数,第一个表示请求,可以规定请求相关的内容(这里用'request'表示,也可以用'req'...叫什么都可以),第二个表示响应,可以规定响应相关的内容(这里用'response'表示).
createServer返回值解读:
createServer返回一个http.server的实例,通过这个server实例调用listen方法,规定了监听的端口号。
加入路由
我们设置路由及相应处理程序,并把请求和他们关联起来
这里有4个程序,需要都看完才能理解,建议大家亲自跑一下。
四个程序关联图:
在服务器文件server.js中加入路由处理:
var http = require("http");
var url = require("url");//引入node内置的‘url’模块
function start(route, handle) {//route:路由程序,handle:路由处理程序
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname; //获取路径名
console.log("Request for " + pathname + " received.");
route(handle, pathname);
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;//将模块暴露出来
说明:url既可以取出路径名,亦可以取出其他的部分,相关的方法如下图:
创建一个路由文件router.js,对应服务器中的路由程序:
function route(handle, pathname) {
console.log("About to route a request for " + pathname);
if (typeof handle[pathname] === 'function') {//如果有相应的处理程序就处理
handle[pathname]();
} else {//如果没有就会返回无处理信息
console.log("No request handler found for " + pathname);
}
}
exports.route = route;
创建一个路由处理文件requesthandlers.js,对应服务器中的路由处理:
function start() {
console.log("Request handler 'start' was called.");
}
function upload() {
console.log("Request handler 'upload' was called.");
}
exports.start = start;
exports.upload = upload;
创建一个主文件 index.js,将以上三个程序关联起来:
var server = require("./server");
var router = require("./router");
var requestHandlers = require("./requestHandlers");
var handle = {}
handle["/"] = requestHandlers.start;
handle["/start"] = requestHandlers.start;
handle["/upload"] = requestHandlers.upload;
server.start(router.route, handle);
用node跑一下:
node index.js
在浏览器先访问:http://localhost:8888/start ,再访问:http://localhost:8888/upload ,
终端的输出结果为:
Server has started.
Request for /start received.
About to route a request for /start
Request handler 'start' was called.
Request for /upload received.
About to route a request for /upload
Request handler 'upload' was called.
http://localhost:8888/start 输出:
Hello World
http://localhost:8888/upload 输出:
Hello World
仔细阅读完这四个小程序之后,想必会使用基本的模块导入和导出了吧。
在主程序中,将handle对象的键设为路径,将对象的值设为相应的路由处理函数,将路由和相应的处理对应了起来。
实现逻辑
我们来让start请求上传图片,upload请求显示图片
这两个文件的关联便是一张图片,我们应该先请求start,再请求upload,
我们希望,upload会等待start响应完之后再响应.
让我们先来做个小测试吧,看看如果start请求如果要处理10秒,upload是否会等它执行完,并且我们希望浏览器会根据请求的不同给出不同的响应,而不是永远输出‘Hello world’,
阻塞式响应
我们修改下requestHandlers.js:
function start() {
console.log("Request handler 'start' was called.");
function sleep(milliSeconds) {//休眠函数
var startTime = new Date().getTime();
while (new Date().getTime() < startTime + milliSeconds);
}
sleep(10000);//休眠10000ms,即10s
return "Hello Start";
}
function upload() {
console.log("Request handler 'upload' was called.");
return "Hello Upload";
}
exports.start = start;
exports.upload = upload;
router.js增加返回参数:
function route(handle, pathname) {
console.log("About to route a request for " + pathname);
if (typeof handle[pathname] === 'function') {
return handle[pathname]();
} else {
console.log("No request handler found for " + pathname);
}
}
exports.route = route;
server.js输出返回参数:
var http = require("http");
var url = require("url");
function start(route, handle) {
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " received.");
response.writeHead(200, {"Content-Type": "text/plain"});
var content = route(handle, pathname)
response.write(content);
response.end();
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
index.js不用修改。
终端运行index.js,
在浏览器访问完 http://localhost:8888/start 后立即访问 http://localhost:8888/upload
我们会发现upload也休眠了10s,并输出了我们想要的结果。
事情似乎进展的很好,但是逻辑似乎有点想不通,我没有让upload延时啊,为什么它也有10秒延时呢?作者说是因为:start中有一个模拟的休眠函数,它阻塞了其他的事件处理。
啊哈,node不是宣称自己非阻塞吗?
node表示很无辜:它一向是这样来标榜自己的:“在node中除了代码,所有 一切都是并行执行的”。这句话的意思是说,Node.js可以在不新增额外线程的情况下,依然可以对任务进行 并行处理,也就是说,node有这个并行非阻塞的能力。
node是单线程的,只是说js的执行是单线程的,node的底层还是多线程的。
node构建服务器的流程图:
针对这个程序,我的理解是,
node先创建了start的请求,通过start的请求对象调用了底层libuv的事件循环,事件循环中的I/O观察者循环按序处理当前的事件,发现start请求有回调函数(createServer中的回调函数),然后把回调函数传给js线程,执行回调.
我们请求完start,又请求了upload,upload采用同样的方式通过libuv的事件轮训向js线程中传递回调,因为start 的回调函数还没有执行完,所以upload等待start的回调执行完再执行)
上面是一个阻塞操作,怎么样才能非阻塞呢?快让node大显身手吧!
让我们再看一个非阻塞的例子吧:
非阻塞响应
修改requestHandlers.js:
var exec = require("child_process").exec;//引入node的子进程模块
function start() {
console.log("Request handler 'start' was called.");
var content = "empty";
exec("find /",{ timeout: 10000, maxBuffer: 20000*1024 }, function (error, stdout, stderr) {//子进程执行了一个‘find /’的命令,这个命令时很耗时的
content = stdout;
console.log(content);//为了说明结果而在终端打印
});
return content;
}
function upload() {
console.log("Request handler 'upload' was called.");
return "Hello Upload";
}
exports.start = start;
exports.upload = upload;
如上运行程序,发现start请求页输出 "empty",而upload会立即执行,没有被start阻塞,终端最后才输出content 的,
为什么会是这个结果呢?
我的理解:
还是那副图
,
首先说明为什么content的内容是初始值,因为js是异步操作的,start 的回调不等内部的子进程执行完就直接返回了,让出了当前进程。
然后再解释下为什么upload没有被阻塞
start这次的回调函数内创建了一个子进程(node可以通过创建子进程来实现对多核多cpu的充分利用),所以这个start在经过事件轮询进入回调之后,他并没有阻塞掉这个js的线程,而是另辟蹊径把耗时的操作交给别的进程了,当upload请求时,进入回调函数阶段后,start一返回upload也立即返回了。
我们了解了阻塞之后,再回到程序上来,
现在我们的目标是用在不阻塞upload的情况下,用非阻塞的形式,正确的得到content的内容。
为了拿到正确的结果,我们决定做一个大胆的尝试,把reponse参数转移到处理函数中来,将子进程的结果直接给response,而不是通过return返回,问题就解决了,
上代码:
迁移response
server.js 把response传递给router:
var http = require("http");
var url = require("url");
function start(route, handle) {
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " received.");
response.writeHead(200, {"Content-Type": "text/plain"});
route(handle, pathname, response);
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
router.js 把response传给requestHandlers.js
function route(handle, pathname, response) {
console.log("About to route a request for " + pathname);
if (typeof handle[pathname] === 'function') {
handle[pathname](response);
} else {
console.log("No request handler found for " + pathname);
}
}
exports.route = route;
requestHandlers.js设置response参数:
var exec = require("child_process").exec;
function start(response) {
console.log("Request handler 'start' was called.");
var content = "empty";
exec("find /",{ timeout: 10000, maxBuffer: 20000*1024 }, function (error, stdout, stderr) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.write(stdout);
response.end();
});
}
function upload(response) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello Upload");
response.end();
}
exports.start = start;
exports.upload = upload;
index.js不用修改
正如所愿,一次访问start和upload,浏览器中,start在一段时间后输出了‘find /’的内容,upload立即返回了‘Hello upload’.
我们终于用非阻塞的方法实现请求啦!
加入postData
接下来呢,我们希望start和upload程序有交互,即把start请求中输入内容,upload将start的内容输出出来,要传递内容,中间必然需要一个东西先把内容给存起来,应用的目的是要上传图片,用什么存图片呢?有点困难,不如我们先把流程跑通,先让它们传递文本。
我们希望应用的模型是这样的:请求start,然后用start上传内容,在start页面点击上传后便触发了upload请求,进而upload页面显示了内容,也就是说,upload请求是在start页面触发的,start页面触发请求后便跳转到upload页面了。哇哦,酷,原来这还是个多页面应用。
当内容是文本时,我们把start提交的内容存入postData中,upload请求就显示postData中的内容。
思考一下,哪里放postData合适呢?我们想一下什么时候存取postData?
先说存:
start页面会提交一个表单,表单一提交,也就要存postData,表单一提交,start请求也就完成了,所以我们要在start请求完成时存postData。再说取:
upload请求一触发也就取出了postData,
存取都和请求有关,好像想到了什么,没错!创建服务器时,回调函数中有设置请求的参数request,我们就在server.js中存取将postData设置为中转站。
赶快开始吧:
server.js中我们设置了request,加入了postData:
var http = require("http");
var url = require("url");
function start(route, handle) {
function onRequest(request, response) {
var postData = "";
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " received.");
request.setEncoding("utf8"); //规定请求的编码格式
request.addListener("data", function(postDataChunk) {//数据上传过程,当数据量很大时,会多次追加postData
postData += postDataChunk;
console.log("Received POST data chunk '"+ postDataChunk + "'.");
});
request.addListener("end", function() {//请求完成时的执行
route(handle, pathname, response, postData);
});
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
router.js加入了postData参数:
function route(handle, pathname, response,postData) {
console.log("About to route a request for " + pathname);
if (typeof handle[pathname] === 'function') {
handle[pathname](response,postData);
} else {
console.log("No request handler found for " + pathname);
response.writeHead(404, {"Content-Type": "text/plain"});
response.write("404 Not found");
response.end();
}
}
exports.route = route;
requestHandlers.js 也加入了postData参数,并且实现了文本上传显示的逻辑:
var querystring = require("querystring");
function start(response,postData) {
console.log("Request handler 'start' was called.");
var body = '<html>'+
'<head>'+
'<meta http-equiv="Content-Type" content="text/html; '+ 'charset=UTF-8" />'+
'</head>'+
'<body>'+
'<form action="/upload" method="post">'+
'<textarea name="text" rows="20" cols="60"></textarea>'+ '<input type="submit" value="Submit text" />'+ '</form>'+
'</body>'+
'</html>';
response.writeHead(200, {"Content-Type": "text/html"});
response.write(body);
response.end();
}
function upload(response, postData) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("You've sent: " + querystring.parse(postData).text);//因为我们只关注文本的内容,所以这里专门解析出文本内容。
response.end();
}
exports.start = start;
exports.upload = upload;
不用修改index.js
最终代码实现
感觉离成功很近了,现在的问题就是我们把文本改成图片,文本可以用一个参数存储,图片呢?图片一般是通过路径来操作的,怎么通过路径存储图片呢?怎么知道图片上传后存在哪里呢?node中有什么关于请求资源路径的模块吗?
真的有诶,那就是node-formidable模块,
先把它下载下来:
npm install formidable
看下它如何工作:
var formidable = require('formidable'),
http = require('http'),
sys = require('util');
http.createServer(function(req, res) {
if (req.url == '/upload' && req.method.toLowerCase() == 'post') {
// parse a file upload
var form = new formidable.IncomingForm();//获取到表单
form.parse(req, function(err, fields, files) {//对表单内容进行解析
res.writeHead(200, {'content-type': 'text/plain'});
res.write('received upload:\n\n');
res.end(sys.inspect({fields: fields, files: files}));
});
return;
}
// show a file upload form
res.writeHead(200, {'content-type': 'text/html'});
res.end('<form action="/upload" enctype="multipart/form-data" '+ 'method="post">'+
'<input type="text" name="title"><br>'+
'<input type="file" name="upload" multiple="multiple"><br>'+ '<input type="submit" value="Upload">'+
'</form>'
);
}).listen(8888);
案例并不难,很好理解,就是页面提交了post请求,然后post页面接收到表单,随便选了一个文件,它解析的表单内容如下:
received upload:
{ fields: { title: '' },
files:
{ upload:
File {
domain: null,
_events: {},
_eventsCount: 0,
_maxListeners: undefined,
size: 13435,
path: '/var/folders/2w/dr7185jx1y78vc215d29dp6c0000gn/T/upload_d50704d47069efb62043db1fdc868ae4',
name: 'tide.png',
type: 'image/png',
hash: null,
lastModifiedDate: 2016-06-23T11:44:45.463Z,
_writeStream: [Object] } } }
重点是path字段,它能解析到资源的路径
我们试着想一下,最终应用该如何实现呢?首先start页面可以进行文件的上传,我们选择了一张图片,表单被提交到upload请求,upload可以拿到post请求的资源的路径,然后我们把路径指向的文件给打印出来
开始吧,可是有个问题啊,upload显示内容的操作是在requestHandlers.js处理程序中的,解析文件路径的操作当然也要在处理程序中,可是解析需要拿到request参数,而request参数是在server.js中设置的,这里我们就要像迁移response那样,也去迁移一下request,当然不需要postData了,去掉它。
修改server.js,去掉postData,传递request参数。
var http = require("http");
var url=require('url');
function start(route,handle) {
function onRequest(request, response) {
var pathname=url.parse(request.url).pathname;
console.log('Request for '+pathname+' received.');
route(handle,pathname,response,request);
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start=start;
修改router.js,传递request参数:
function route(handle,pathname,response,request) {
console.log("About to route a request for " + pathname);
if(typeof handle[pathname]==='function'){
handle[pathname](response,request);
}else{
console.log("No request handler found for "+pathname);
response.writeHead(404,{"Content-Type":"text/plain"});
response.write("404 Not found");
response.end();
}
}
exports.route = route;
修改requestHandlers.js,实现应用逻辑:
var querystring=require("querystring"),
fs=require("fs");//引入文件模块
formidable=require("formidable");
function start(response) {
console.log("Request handler 'start' was called.");
var body = '<html>'+
'<head>'+
'<meta http-equiv="Content-Type" content="text/html; '+
'charset=UTF-8" />'+
'</head>'+
'<body>'+
'<form action="/upload" enctype="multipart/form-data" '+
'method="post">'+
'<input type="file" name="upload">'+
'<input type="submit" value="Upload file" />'+
'</form>'+
'</body>'+
'</html>';
response.writeHead(200, {"Content-Type": "text/html"});
response.write(body);
response.end();
}
function upload(response,request) {
var form=new formidable.IncomingForm();
console.log("about to parse");
form.parse(request,function (error,fields,files) {
console.log("parsing done");
fs.readFile(files.upload.path,"binary",function (error,file) {//读取保存图片的文件
if(error){//如果发生读取错误,返回错误信息
response.writeHead(500, {"Content-Type": "text/plain"});
response.write(error+'\n');
response.end();
}else{//如果正确读取到图片就显示它
response.writeHead(200, {"Content-Type": "image/png"});
response.write(file,"binary");
response.end();
}
});
})
}
exports.start = start;
exports.upload = upload;
index.js不用修改
展示下效果吧:
访问start页面,我选择了一只泰迪熊的照片上传:
点击Upload file按钮,
当当当当:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。