摘要:處理程序在的匿名回調函數中做請求響應的操作,而處理程序仍然是簡單的回復,只是這次是使用對象而已。為了使整個過程非阻塞,會將數據拆分成很多小的數據塊,然后通過觸發特定的事件,將這些小數據塊傳遞給回調函數。
新手node入門,用這個小項目練練手,寫這篇文章也是為了自己鞏固下知識。
先看效果圖:先是讓用戶輸入名字 get yourself a nickname :)
輸入好了之后進入,然后隨便說點什么,
可以多個人在線聊天,
也可以上傳圖片:
更改字體顏色:
文件分布:入口文件server.js
本項目的全代碼 https://github.com/FengNianya...(我的github倉庫)
講解:服務器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() 中做這件事情。不過就像我說過的,我們應該加入一些抽象的元素,讓我們的例子變得更有趣一點兒。
處理不同的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 = ""+ ""+ ""+ ""+ ""+ ""+ ""+ ""; 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 = ""+ ""+ ""+ ""+ ""+ ""+ ""+ ""; 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 = ""+ ""+ ""+ ""+ ""+ ""+ ""+ ""; 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 = ""+ ""+ ""+ ""+ ""+ ""+ ""+ ""; 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 + " "); 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; 還沒寫完,有空繼續更新,也可以看下載個 》
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/89579.html
摘要:知乎日報代理首先感謝提供的分析使用詳情請參考他提供的參數和地址代理轉發的使用為前綴進入代理路由啟動界面圖像獲取后為圖像分辨率,接受任意的格式,為任意非負整數,返回值均相同返回值示例最新消息等具體參考提供的分析中的使用方式以及參數含義。 項目說明 這是一個基于express的node后端API服務,當時只是想抓取字幕組網站的下載資源,以備以后通過nas的方式去自動下載關注的美劇。不過后來...
摘要:本文源碼簡介之前剛入門并做好了一個簡而全的純全家桶的項目,數據都是本地模擬請求的詳情請移步這里為了真正做到數據庫的真實存取,于是又開始入門了并以此來為之前的頁面寫后臺數據接口。 本文源碼:Github 簡介: 之前剛入門vue并做好了一個簡而全的純vue2全家桶的項目,數據都是本地 json 模擬請求的;詳情請移步這里:vue-proj-demo 為了真正做到數據庫的真實存取,于是又...
摘要:沒有耐心閱讀的同學,可以直接前往學習全棧最后一公里。我下面會羅列一些,我自己錄制過的一些項目,或者其他的我覺得可以按照這個路線繼續深入學習的項目資源。 showImg(https://segmentfault.com/img/bVMlke?w=833&h=410); 本文技術軟文,閱讀需謹慎,長約 7000 字,通讀需 5 分鐘 大家好,我是 Scott,本文通過提供給大家學習的方法,...
摘要:項目簡介主要是通過做一個多人在線多房間群聊的小項目來練手全棧技術的結合運用。編譯運行開啟服務,新建命令行窗口啟動服務端,新建命令行窗口啟動前端頁面然后在瀏覽器多個窗口打開,注冊不同賬號并登錄即可進行多用戶多房間在線聊天。 項目簡介 主要是通過做一個多人在線多房間群聊的小項目、來練手全棧技術的結合運用。 項目源碼:chat-vue-node 主要技術: vue2全家桶 + socket....
閱讀 2344·2021-11-23 09:51
閱讀 1999·2021-10-14 09:43
閱讀 2760·2021-09-27 13:35
閱讀 1144·2021-09-22 15:54
閱讀 2495·2021-09-13 10:36
閱讀 3785·2019-08-30 15:56
閱讀 3404·2019-08-30 14:09
閱讀 1711·2019-08-30 12:57