也许,你已经高频屡次听到了node。毕竟它真的很火。但是你还在犹豫,毕竟,学习一门语言以及库,是一个开坑和被坑的过程。也担忧学习后不知道能够作点什么。javascript
我也和你同样。通过半年的学习,阅读了很多代码,我试图以此文,引导你作一个http server。php
东西成了,学习也就成了。html
安装node,在windows/mac 上很是简单,和其余应用软件也没有什么区别:下载安装包,而后执行,遵从它的指示,一步步的走。完成后,在command line输入命令:java
$node -v v0.12.4
看到版本号?成功。版本号的话,偶数(偶数是稳定版,奇数是开发版)就好,大点就好。node
Linux 上复杂点。不过这和咱们的内容关系不大。能够看官方的安装指南。本身消化下。git
Hello world 太多,但是初学者都喜欢。因此,我老着脸,就再来一个。程序员
建立一个helloworld.js文件(哦,我爱sublime text)。代码:github
console.log("Hello World");
保存文件,到command line执行:golang
node helloworld.js
正常的话,就会在终端输出Hello World 。ajax
选择一个叫作“简洁”的角度闻过去,有点c的味道,比c的味道更浓。你看,不须要#include,不须要main{}。
也不须要设置环境变量。关于最后一条,java,golang两位同窗,我没有针对你。
我喜欢这种一点点多余的泡泡肉也没有的感受。
输入node app.js ,ctrl+c ,而后一百遍的重复,以便重写测试代码。这样的输入一天下来也真是厌倦。若是你和我同样,那么 nodemon 能够帮忙你。
它会监视当前目录,若是发现代码有修改,就会自动重启代码。
npm i nodemon nodemon app.js
npm i表示从npm仓库安装nodemon。npm是node社区一位领袖建立,依我看是目前最好的模块系统。模块数量也是主流脚本中数量最高的。虽然这不表明质量,可是说明门槛低,方便,你们所以愿意提交模块。npm内置,简单,极其方便,算得上node的一大特点。
而后修改你的app.js ,会发现nodemon自动运行app.js 。
个人双显示器正好派上用场。一块运行nodemon,另一块做为编辑器的工做台,编写个人app.js,而后save。这个小小的机器人不厌其烦的检测file save->重启app.js->显示错误(甚至app.js也crash。固然nodemon不会所以也crash)->待你修正保存。直接正确为止。
虽然功能简单,可是恰如其分,一个好工具。
当我准备好代码app.js
console.log("hi")
而后nodemon app.js ,能够看到输出:
6 Jul 08:45:12 - [nodemon] v1.3.7 6 Jul 08:45:12 - [nodemon] to restart at any time, enter `rs` 6 Jul 08:45:12 - [nodemon] watching: *.* 6 Jul 08:45:12 - [nodemon] starting `node app.js` hi 6 Jul 08:45:12 - [nodemon] clean exit - waiting for changes before restart
打印了hi。这时我想要改下代码,输出点具体的:
console.log("hi,node")
在保存,就能够看到:
6 Jul 08:47:17 - [nodemon] restarting due to changes... 6 Jul 08:47:17 - [nodemon] starting `node app.js` hi,node 6 Jul 08:47:17 - [nodemon] clean exit - waiting for changes before restart
你看,我不须要在本身执行node app.js ,它会执行后等待变化,而后启动。
即便我改变代码为:
process.exit(0)
也不会整体退出:
6 Jul 08:51:22 - [nodemon] restarting due to changes... 6 Jul 08:51:22 - [nodemon] starting `node app.js` 6 Jul 08:51:22 - [nodemon] clean exit - waiting for changes before restart
虽然感受稍微慢了点,总比我编码快,够用了。
要是想要启动后延时1秒在say hi,怎么办?
function hi(){console.log("hi")} setTimeout(hi,1000)
setTimeout是一个全局函数,文档这样说明它的规格:
setTimeout(callback, delay[, arg][, ...])#
第一个参数,名字为callback,做为js的文档约定,说明此参数能够是一个函数。咱们能够把函数做为变量传递给SetTimeout。这里传递的不是hi的结果,而是hi 自己!setTimeout会在它的实现内调用它。
还能够简洁。hi这个名字的存在不太必要,咱们能够在应用hi的地方,直接定义这个函数:
setTimeout(function(){console.log("hi")},1000)
这个函数定义存在,功能可用,可是无名。它就是“匿名函数"。
一个函数能够做为变量传递给另外一个函数。咱们能够先定义一个函数,而后传递,也能够在传递参数的地方直接定义函数。
简洁还在。可是有了callback,感受稍微不太同样了,特别是和php等相比。
当setTimeout执行时,1s后会打印,那么<1s的时间,在干啥?等待。内部实现来讲,node会把这个hi做为callback排到队列内。当道setTimeout的时间一到就会触发callback的执行。
setTimeout(function(){console.log("hi")},1000) console.log("ready")
输出:
ready hi
这个期间,node能够继续处理其余的工做,setTimeout 不会被阻塞,而是能够继续执行后面的代码。2行代码,其实执行线索上看有两条。
node大量使用异步代码,以此为卖点。怎么强调这个特性也不为过。对于强调并发的服务器编码,能够无需诉诸于多线程就能多线索的处理并发客户端需求。后面会看到在http sever内对此特性的使用和分析。
由于来了事件就调用callback,因此异步编程和事件驱动就经常一块儿出现了。尽管他们并不相同,在node 内经常是一回事,咱们也不去细分了。
以往个人主语言是c#,那会儿,做为程序员,只能是IIS的用户。用户这个词,深深的伤害了我。如今node能够帮我报一箭之仇。
看看咱们能够作点什么:
咱们来分解一下这个应用,为了实现上文的用例,咱们须要实现哪些部分呢?
路由这样的工做,以往是有Web Server会处理。但是咱们如今要本身作。
如今创建一个目录,比如是frodo. touch 一个 server.js的文件出来,输入:
var http = require("http"); http.createServer(function(request, response) { response.setHeader('content-type', 'text/plain') response.end("42"); }).listen(8888); // visit http://localhost:8888
呃。完了?嗯。用node跑跑。
nodemon server.js
开一个浏览器(我爱chrome)访问http://localhost:8888/,看到 42 就成了。
不少时候咱们须要基于他人的工做。作http就应该引用http模块。它是node的内置模块。
咱们能够先看以上代码的主线索,启动服务器,并侦听8888端口:
var http = require("http"); var server = http.createServer(); server.listen(8888);
createServer。建立一个http server,侦听 8888端口。若是有请求到,就调用匿名函数:
function(request, response) { response.setHeader('content-type', 'text/plain') response.end("42"); }
在此函数内,调用response.end,把内容(42)发送给Browser。
setHeader指明返回给浏览器的内容的格式。这里指明内容为平文本(text/plain)。还有比较多的经常使用格式,包括text/html,image/jpeg ,text/script 。望文生义便可。我不写这一行的话,现代的浏览器经常能够自动识别内容的格式。因此我经常也偷个懒。
这样固然并不严谨。为了快速的观其大略,有些细节能够暂时忽略。
启动服务后
nodemon server.js
能够在chrome内访问 localhost:8888,多开几个标签,都来打开 http://localhost:8888/,能够看到这个server总能够沉着的、稳定而单调的返回42 。多用户访问哦。
更多时候,我会用curl,一个命令行的browser模拟器。
curl http://localhost:8888/ 42
实际上,开发node应用,第一次我经常会用chrome访问测试,后来的反复越多,我越会倾向于使用curl。若是我作这样app,我只有关心返回的是否是我指望的42,而没必要关心chrome的进度条,菜单,状态栏。。。多好。42 !最低眼球识别成本。
所以我不爱ide,而爱 sublime text 也基于一样的理由。
易如反掌:
var http = require("http"); http.createServer(function(request, response) { response.end("<b>it works</b><a href='/start'>start</a>"); }).listen(80); $curl localhost <b>it works</b><a href='/start'>start</a>
说明:
为了再省点事儿,我侦听改成 80 ,这样browser输入url的时候,不须要输入port。
http server过来的都是URL,而咱们的代码是一个个的函数。URL 映射到函数的方法,就是路由。
所以,咱们须要查看HTTP请求,从中提取出请求URL:
var http = require("http"); http.createServer(function(request, response) { var pathname = url.parse(request.url).pathname; console.log(pathname); response.end("<b>it works</b><a href='/start'>start</a>"); }).listen(80);
点击start url,会看到/start 打印出来。
http 模块来的url,形如 http://domain.com:80/start?foo=bar&baz=bzz。能够经过url模块,解析它的pathname。这里的pathname = "/start"
var url = require("url"); var assert = require("assert") var u = "http://domain.com:80/start?foo=bar&baz=bzz" assert.equal("/start",url.parse(u).pathname)
有了路由,来自/start和/upload的请求会导流到不一样函数。因此,咱们应该有一个结构,map二者的关系
var m = [ {path:"/",func:function (){return "/"}}, {path:"/start",func:function (){return "/start"}}, {path:"/upload",func:function (){return "/upload"}} ]
首先,加入路由函数:
var http = require("http"); http.createServer(function(request, response) { var pathname = require("url").parse(request.url).pathname; var r = route(pathname) if (r) response.end(r()); else response.end("<b>it works</b>"); }).listen(80); function route(pathname){ for(var i=0;i<m.length;i++){ if (m[i].path == pathname) return m[i].func } return null }
咱们故伎重演,用curl解放眼球:
$ curl localhost/upload upload $ curl localhost/start start $ curl localhost/ /
数学上,有时候仅仅是改变下公式内元素的位置,就可让解析或者证实变得更加容易。代码也是。咱们把上面的m 映射改为:
var m ={} m["/"] = function (){return "/"} m["/start"] = function (){return "/start"} m["/upload"] = function (){return "/upload"}
表达的内容是等效的 。可是对于解析函数route会更加简单。
function route(pathname){ return m[pathname] }
目前咱们什么都混在一块儿。也会继续混到一块儿:代码还很少,这样有利于把握总体。
客户端总要考虑客户的使用友好,不要卡死,界面漂亮;而服务器须要处理的就是减小阻塞。
何为阻塞?
让代码慢下来,就能够看到阻塞。咱们来让start()睡一会,模拟下。
function sleep(milliSeconds) { var startTime = new Date().getTime(); while (new Date().getTime() < startTime + milliSeconds); } function start() { sleep(5000); return "/start"; }
故伎重演。不过稍做变化。由于curl能够帮助统计运行时间,因此咱们来利用下:
curl -w %{time_total}\\n localhost:8888/upload /upload 0.002
很快出结果,0.002,就是2毫秒。
$ curl -w %{time_total}\\n localhost:8888/start start 5.001
5毫秒。多一点。正如所愿。
一个一个的,很好。若是并发呢。
打开两个命令行窗口。
一个输入curl -w %{time_total}\n localhost:8888/upload,可是不执行
一个输入curl -w %{time_total}\n localhost:8888/start,可是不执行
而后,一二三,执行第二个,而后执行第一个。快点。
$ curl -w \\n%{time_total}\\n localhost:8888/start /start 5.013 $ curl -w \\n%{time_total}\\n localhost:8888/upload /upload 4.353
upload没有任何修改,原本执行很快,如今却慢到须要几乎5ms呢?
由于upload被start()阻塞了。start()的慢速,阻塞了其余的工做。
Node是单线程的。它经过事件轮询(event loop)来实现并行操做。若是轮询过来执行的代码时间长,就会没法处理后来的请求。所以,咱们须要尽量快的完成操做,以便返回控制权给node,让它能够抽身处理队列内等待的任务。
简单的用例:
/start请求处理程序用于生成带文本区的表单,所以,咱们将 app.js修改成以下形式:
var http = require("http"); var url = require("url"); var m ={} m["/form"] = form m["/upload"] = upload m[404] = h404 function onRequest(request, response) { var postData = ""; var pathname = url.parse(request.url).pathname; console.log("Request for " + pathname + " received."); var f = m[pathname] if(f) f(request, response) else h404() } http.createServer(onRequest).listen(80); function h404(request, response){ response.writeHead(404, {"Content-Type": "text/plain"}); response.write("404 Not found"); response.end(); } function upload(request, response){ request.setEncoding("utf8"); var postData var count = 0 request.addListener("data", function(postDataChunk) { console.log("postDataChunk.length:",postDataChunk.length); postData += postDataChunk; count++ }); request.addListener("end", function() { console.log(count); }); } function form(request, response){ var body = '<form action="/upload" method="post">'+ '<textarea name="text" rows="20" cols="60"></textarea>'+ '<input type="submit" value="Submit text" />' response.writeHead(200, {"Content-Type": "text/html"}); response.write(body); response.end(); }
POST数据可能很大,为了使整个过程不会阻塞,Node会将POST数据拆分红小块。这也要求咱们经过侦听触发事件,把它们从新拼接起来。咱们须要:
以下所示:
request.addListener("data", function(postDataChunk) { console.log("postDataChunk.length:",postDataChunk.length); postData += postDataChunk; count++ }); request.addListener("end", function() { console.log(count); response.writeHead(200, {"Content-Type": "text/plain"}); response.write("Received: " + postData); response.end(); });
实验体会:尝试着去输入大段内容,就会发现data事件会触发屡次。就是说,打印出来的count可能不是1,而每一个postDataChunk.length也不尽相同。
咱们在/upload页面,展现用户输入的内容。
request.addListener("end", function() { console.log(count); response.writeHead(200, {"Content-Type": "text/plain"}); response.write("Received: " + postData); response.end(); });
最后,实现用例:
咱们要用到的外部模块:node-formidable,用来处理文件上传。
完成模块安装:
npm install formidable
用require语句引入:
var formidable = require("formidable");
该模块能够解析来自HTTP POST的表单:
var formidable = require('formidable'), http = require('http'), util = require('util'); http.createServer(function(req, res) { if (req.url == '/upload' && req.method.toLowerCase() == 'post') { var form = new formidable.IncomingForm(); form.parse(req, function(err, fields, files) { res.end('received upload:\n',files.upload.path); }); } // 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);
在表单中添加一个文件上传元素。只须要在HTML表单中,添加一个multipart/form-data的编码类型。
formidable 会把此上传文件放到一个当前用户的临时目录内。并在files.upload.path 通知调用者具体位置:
received upload:C:\Users\rita\AppData\Local\Temp\upload_b3fa645d2425bc9f768494573a09b8ce
咱们来添加/show 请求处理程序,它硬编码显示刚刚传递的png到浏览器中。
var http = require("http"); var url = require("url"); var m ={} m["/show"] = show m["/favicon"] = favicon function onRequest(request, response) { var pathname = url.parse(request.url).pathname; console.log("Request for " + pathname + " received."); var f = m[pathname] if(f) f(request, response) else h404(request, response) } http.createServer(onRequest).listen(80); function show(request,response) { var fs = require("fs") // 替换为你的文件 var last_uploadfile ="C:/Users/rita/AppData/Local/Temp/upload_b3fa645d2425bc9f768494573a09b8ce" fs.readFile(last_uploadfile, "binary", function(error, file) { if(error) { h404(request,response) } else { response.writeHead(200, {"Content-Type": "image/png"}); response.write(file, "binary"); response.end(); } }); } function h404(request, response){ if (response){ response.writeHead(404, {"Content-Type": "text/plain"}); response.write("404 Not found"); response.end();} } function favicon(request, response){}
重启服务器以后,经过访问http://localhost/show,就能够看到保存在刚刚上传的图片了
恭喜,咱们的半成品完成了。关于语言自己,须要理解的就是模块和Callback。做为服务器端脚本,概念就稍微多点点:阻塞与非阻塞,事件驱动,以及HTTP协议,文件Post上传,MIME类型。
一回生二回熟。至此,Node对咱们而言,有些亲切了。
和路由相关的代码展现了做为服务器框架的一个重要构成的概念。对此有兴趣的话,能够继续研究express框架。
另外,代码也都堆积到一个文件,根本没有考虑重构,也没有考虑到模块划分。对于较大的程序来讲,这固然会构成一个问题。我在(极简node模块开发)[note.md]探究此技术。
学无止境。学习node经常会有哦也的赞叹,这样的乐趣相伴左右。
本文是nodebeginner对应的中文版的阅读笔记。可是在实验代码的过程当中,也顺手加入了些本身的一些文字与代码的风味:
-简洁:行文简化,代码也作了重构。而且表意也直接(总以为别人啰嗦)。还忽略和模块等和主题不太相关的内容。
-也有些个人想法。好比curl替代browser作响应验证
通过这个工做,我更好的学习了原文,体会到node的精要之处。因此感谢nodebeginer做者的创造和译者的工做。
说说我和js的交往吧。
过去N年,我一直是一家企业的技术团队管理者,同时也是MS技术的开发者。我采用c#作b/s 企业应用。其中涉及到的javascript不多,有的话,基本也就是数据核对。或者玩点动画之类的动态内容。一直认为js很简单,故而也谈不上作稍微深刻的研究。
而后ajax技术告诉我,这个看起来很小的玩意其实能够很强大。
接着,出现了Node,服务端的JavaScript,以及火热的NPM模块仓库。一块儿来的,还有不太熟悉的面孔,像是事件驱动的,非阻塞等等。
这几年社区明显的火起来。在github上算得上第一语言,即便MS也在为她作工具(Node tool,Visual studio code ),甚至创造了一门(再一个)能够编译到js的语言:TypeScript。
我(一路大跌眼镜)[http://1000copy.farbox.com/post/crossing-eye-s-hell],一次次的修正本身的认识,因而我真心的想要花点气力研究,以便充分的今后语言中获益。
不管如何,js是b/s编程的一个必选项。反正都要选,若是还能够同时完成后端的代码,只是想一想也会感到很棒。