也許,你已經高頻屢次聽到了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編程的一個必選項。反正都要選,若是還能夠同時完成後端的代碼,只是想一想也會感到很棒。