node練手小項目:在線聊天系統

新手node入門,用這個小項目練練手,寫這篇文章也是爲了本身鞏固下知識。html

先看效果圖:

先是讓用戶輸入名字 get yourself a nickname :)node

clipboard.png

輸入好了以後進入,而後隨便說點什麼,git

clipboard.png
能夠多我的在線聊天,github

clipboard.png
也能夠上傳圖片:npm

clipboard.png
更改字體顏色:編程

clipboard.png

製做過程

  • 文件分佈:clipboard.png入口文件server.js
    clipboard.png

講解:

  • 服務器server.js
    先舉一個例子:咱們把咱們的服務器腳本放到一個叫作 start 的函數裏,而後咱們會導出這個函數。
var http = require("http");

function start() {
  function onRequest(request, response) {
    console.log("Request received.");
    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;

這樣,咱們如今就能夠建立咱們的主文件 index.js 並在其中啓動咱們的HTTP了,雖然服務器的代碼還在 server.js 中。數組

建立 index.js 文件並寫入如下內容:瀏覽器

var server = require("./server");

server.start();

正如你所看到的,咱們能夠像使用任何其餘的內置模塊同樣使用server模塊:請求這個文件並把它指向一個變量,其中已導出的函數就能夠被咱們使用了。咱們如今能夠把咱們的應用的不一樣部分放入不一樣的文件裏,而且經過生成模塊的方式把它們鏈接到一塊兒了。緩存

  • 路由:
    對於不一樣的URL請求,服務器應該有不一樣的反應。

對於一個很是簡單的應用來講,你能夠直接在回調函數 onRequest() 中作這件事情。不過就像我說過的,咱們應該加入一些抽象的元素,讓咱們的例子變得更有趣一點兒。bash

處理不一樣的HTTP請求在咱們的代碼中是一個不一樣的部分,叫作「路由選擇」——那麼,咱們接下來就創造一個叫作 路由 的模塊吧。
咱們要爲路由提供請求的URL和其餘須要的GET及POST參數,隨後路由須要根據這些數據來執行相應的代碼

咱們須要的全部數據都會包含在request對象中,該對象做爲onRequest()回調函數的第一個參數傳遞。可是爲了解析這些數據,咱們須要額外的Node.JS模塊,它們分別是url和querystring模塊。
如今咱們來給onRequest()函數加上一些邏輯,用來找出瀏覽器請求的URL路徑:

var http = require("http");
var url = require("url");

function start() {
  function onRequest(request, response) {
    var pathname = url.parse(request.url).pathname;
    console.log("Request for " + pathname + " received.");
    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路徑來區別不一樣請求了--這使咱們得以使用路由(還未完成)來將請求以URL路徑爲基準映射處處理程序上。

在咱們所要構建的應用中,這意味着來自/start和/upload的請求可使用不一樣的代碼來處理。
router.js:

function route(pathname) {
  console.log("About to route a request for " + pathname);
}

exports.route = route;

如你所見,這段代碼什麼也沒幹,不過對於如今來講這是應該的。在添加更多的邏輯之前,咱們先來看看如何把路由和服務器整合起來。
咱們的服務器應當知道路由的存在並加以有效利用。咱們固然能夠經過硬編碼的方式將這一依賴項綁定到服務器上,可是其它語言的編程經驗告訴咱們這會是一件很是痛苦的事,所以咱們將使用依賴注入的方式較鬆散地添加路由模塊
來擴展一下服務器的start()函數,以便將路由函數做爲參數傳遞過去:

var http = require("http");
var url = require("url");

function start(route) {
  function onRequest(request, response) {
    var pathname = url.parse(request.url).pathname;
    console.log("Request for " + pathname + " received.");

    route(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;

同時,咱們會相應擴展index.js,使得路由函數能夠被注入到服務器中:

var server = require("./server");
var router = require("./router");

server.start(router.route);
bash$ node index.js
Request for /foo received.
About to route a request for /foo

你將會看到應用輸出相應的信息,這代表咱們的HTTP服務器已經在使用路由模塊了,並會將請求的路徑傳遞給路由

  • 處理函數requestHandlers的模塊

並對於每個請求處理程序,添加一個佔位用函數,隨後將這些函數做爲模塊的方法導出:

function start() {
  console.log("Request handler 'start' was called.");
}

function upload() {
  console.log("Request handler 'upload' was called.");
}

exports.start = start;
exports.upload = upload;

這樣咱們就能夠把請求處理程序和路由模塊鏈接起來,讓路由「有路可尋」。
仔細想一想,有一大堆東西,每一個都要映射到一個字符串(就是請求的URL)上?彷佛關聯數組(associative array)能完美勝任。

引用:不過結果有點使人失望,JavaScript沒提供關聯數組 -- 也能夠說它提供了?事實上,在JavaScript中,真正能提供此類功能的是它的對象。
在C++或C#中,當咱們談到對象,指的是類或者結構體的實例。對象根據他們實例化的模板(就是所謂的類),會擁有不一樣的屬性和方法。但在JavaScript裏對象不是這個概念。在JavaScript中,對象就是一個鍵/值對的集合 -- 你能夠把JavaScript的對象想象成一個鍵爲字符串類型的字典。

好了,最後再回到代碼上來。如今咱們已經肯定將一系列請求處理程序經過一個對象來傳遞,而且須要使用鬆耦合的方式將這個對象注入到route()函數中。

咱們先將這個對象引入到主文件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);

正如所見,將不一樣的URL映射到相同的請求處理程序上是很容易的:只要在對象中添加一個鍵爲"/"的屬性,對應requestHandlers.start便可,這樣咱們就能夠乾淨簡潔地配置/start和/的請求都交由start這一處理程序處理。

在完成了對象的定義後,咱們把它做爲額外的參數傳遞給服務器,爲此將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.");

    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;

這樣咱們就在start()函數裏添加了handle參數,而且把handle對象做爲第一個參數傳遞給了route()回調函數。
而後咱們相應地在route.js文件中修改route()函數:

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;

經過以上代碼,咱們首先檢查給定的路徑對應的請求處理程序是否存在,若是存在的話直接調用相應的函數。咱們能夠用從關聯數組中獲取元素同樣的方式從傳遞的對象中獲取請求處理函數,所以就有了簡潔流暢的形如handle[pathname]();的表達式

有了這些,咱們就把服務器、路由和請求處理程序在一塊兒了

  • 處理程序與服務器
    咱們採用以下這種新的實現方式:相對採用將內容傳遞給服務器的方式,咱們此次採用將服務器「傳遞」給內容的方式。 從實踐角度來講,就是將response對象(從服務器的回調函數onRequest()獲取)經過請求路由傳遞給請求處理程序。 隨後,處理程序就能夠採用該對象上的函數來對請求做出響應。

原理就是如此,接下來讓咱們來一步步實現這種方案。

先從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.");

    route(handle, pathname, response);
  }

  http.createServer(onRequest).listen(8888);
  console.log("Server has started.");
}

exports.start = start;

相對此前從route()函數獲取返回值的作法,此次咱們將response對象做爲第三個參數傳遞給route()函數,而且,咱們將onRequest()處理程序中全部有關response的函數調都移除,由於咱們但願這部分工做讓route()函數來完成。

下面就來看看咱們的router.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);
    response.writeHead(404, {"Content-Type": "text/plain"});
    response.write("404 Not found");
    response.end();
  }
}

exports.route = route;

一樣的模式:相對此前從請求處理程序中獲取返回值,此次取而代之的是直接傳遞response對象。

若是沒有對應的請求處理器處理,咱們就直接返回「404」錯誤。

最後,咱們將requestHandler.js修改成以下形式:

var exec = require("child_process").exec;

function start(response) {
  console.log("Request handler 'start' was called.");

  exec("ls -lah", 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;

咱們的處理程序函數須要接收response參數,爲了對請求做出直接的響應。

start處理程序在exec()的匿名回調函數中作請求響應的操做,而upload處理程序仍然是簡單的回覆「Hello World」,只是此次是使用response對象而已。

這時再次咱們啓動應用(node index.js),一切都會工做的很好。

  • 處理POST請求
    考慮這樣一個簡單的例子:咱們顯示一個文本區(textarea)供用戶輸入內容,而後經過POST請求提交給服務器。最後,服務器接受到請求,經過處理程序將輸入的內容展現到瀏覽器中。

/start請求處理程序用於生成帶文本區的表單,所以,咱們將requestHandlers.js修改成以下形式:

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" 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) {
  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;

這裏採用非阻塞方式處理是明智的,由於POST請求通常都比較「重」 —— 用戶可能會輸入大量的內容。用阻塞的方式處理大數據量的請求必然會致使用戶操做的阻塞。

爲了使整個過程非阻塞,Node.js會將POST數據拆分紅不少小的數據塊,而後經過觸發特定的事件,將這些小數據塊傳遞給回調函數。這裏的特定的事件有data事件(表示新的小數據塊到達了)以及end事件(表示全部的數據都已經接收完畢)。

咱們須要告訴Node.js當這些事件觸發的時候,回調哪些函數。怎麼告訴呢? 咱們經過在request對象上註冊監聽器(listener) 來實現。這裏的request對象是每次接收到HTTP請求時候,都會把該對象傳遞給onRequest回調函數。
以下所示:

request.addListener("data", function(chunk) {
  // called when a new chunk of data was received
});

request.addListener("end", function() {
  // called when all chunks of data have been received
});

問題來了,這部分邏輯寫在哪裏呢? 咱們如今只是在服務器中獲取到了request對象 —— 咱們並無像以前response對象那樣,把 request 對象傳遞給請求路由和請求處理程序。

在我看來,獲取全部來自請求的數據,而後將這些數據給應用層處理,應該是HTTP服務器要作的事情。所以,我建議,咱們直接在服務器中處理POST數據,而後將最終的數據傳遞給請求路由和請求處理器,讓他們來進行進一步的處理。

所以,實現思路就是: 將data和end事件的回調函數直接放在服務器中,在data事件回調中收集全部的POST數據,當接收到全部數據,觸發end事件後,其回調函數調用請求路由,並將數據傳遞給它,而後,請求路由再將該數據傳遞給請求處理程序。

還等什麼,立刻來實現。先從server.js開始:

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 += 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;

上述代碼作了三件事情: 首先,咱們設置了接收數據的編碼格式爲UTF-8,而後註冊了「data」事件的監聽器,用於收集每次接收到的新數據塊,並將其賦值給postData 變量,最後,咱們將請求路由的調用移到end事件處理程序中,以確保它只會當全部數據接收完畢後才觸發,而且只觸發一次。咱們同時還把POST數據傳遞給請求路由,由於這些數據,請求處理程序會用到。

上述代碼在每一個數據塊到達的時候輸出了日誌,這對於最終生產環境來講,是很很差的(數據量可能會很大,還記得吧?),可是,在開發階段是頗有用的,有助於讓咱們看到發生了什麼。

我建議能夠嘗試下,嘗試着去輸入一小段文本,以及大段內容,當大段內容的時候,就會發現data事件會觸發屢次。
再來點酷的。咱們接下來在/upload頁面,展現用戶輸入的內容。要實現該功能,咱們須要將postData傳遞給請求處理程序,修改router.js爲以下形式:

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中,咱們將數據包含在對upload請求的響應中:

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: " + postData);
  response.end();
}

exports.start = start;
exports.upload = upload;

好了,咱們如今能夠接收POST數據並在請求處理程序中處理該數據了。

咱們最後要作的是: 當前咱們是把請求的整個消息體傳遞給了請求路由和請求處理程序。咱們應該只把POST數據中,咱們感興趣的部分傳遞給請求路由和請求處理程序。在咱們這個例子中,咱們感興趣的其實只是text字段。

咱們可使用此前介紹過的querystring模塊來實現:

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 the text: "+
  querystring.parse(postData).text);
  response.end();
}

exports.start = start;
exports.upload = upload;
  • 處理文件上傳
    這裏咱們要用到的外部模塊是Felix Geisendörfer開發的node-formidable模塊。它對解析上傳的文件數據作了很好的抽象。 其實說白了,處理文件上傳「就是」處理POST數據 —— 可是,麻煩的是在具體的處理細節,因此,這裏採用現成的方案更合適點。

使用該模塊,首先須要安裝該模塊。Node.js有它本身的包管理器,叫NPM。它可讓安裝Node.js的外部模塊變得很是方便。經過以下一條命令就能夠完成該模塊的安裝:

npm install formidable
npm info build Success: formidable@1.0.9
npm ok
var querystring = require("querystring"),
    fs = require("fs");

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 the text: "+
  querystring.parse(postData).text);
  response.end();
}

function show(response, postData) {
  console.log("Request handler 'show' was called.");
  fs.readFile("/tmp/test.png", "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;
exports.show = show;

咱們還須要將這新的請求處理程序,添加到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;
handle["/show"] = requestHandlers.show;

server.start(router.route, handle);

好,最後咱們要的就是:

在/start表單中添加一個文件上傳元素
將node-formidable整合到咱們的upload請求處理程序中,用於將上傳的圖片保存到/tmp/test.png
將上傳的圖片內嵌到/uploadURL輸出的HTML中
須要在upload處理程序中對上傳的文件進行處理,這樣的話,咱們就須要將request對象傳遞給node-formidable的form.parse函數。

可是,咱們有的只是response對象和postData數組。看樣子,咱們只能不得不將request對象從服務器開始一路經過請求路由,再傳遞給請求處理程序。 或許還有更好的方案,可是,無論怎麼說,目前這樣作能夠知足咱們的需求。

到這裏,咱們能夠將postData從服務器以及請求處理程序中移除了 —— 一方面,對於咱們處理文件上傳來講已經不須要了,另一方面,它甚至可能會引起這樣一個問題: 咱們已經「消耗」了request對象中的數據,這意味着,對於form.parse來講,當它想要獲取數據的時候就什麼也獲取不到了。(由於Node.js不會對數據作緩存)

咱們從server.js開始 —— 移除對postData的處理以及request.setEncoding (這部分node-formidable自身會處理),轉而採用將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 —— 咱們再也不須要傳遞postData了,此次要傳遞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/html"});
    response.write("404 Not found");
    response.end();
  }
}

exports.route = route;
還沒寫完,有空繼續更新,也能夠看下載個
 》
相關文章
相關標籤/搜索