更有用的場景

2018-02-24 15:25 更新

到目前為止,我們做的已經(jīng)很好了,但是,我們的應(yīng)用沒有實(shí)際用途。

服務(wù)器,請求路由以及請求處理程序都已經(jīng)完成了,下面讓我們按照此前的用例給網(wǎng)站添加交互:用戶選擇一個(gè)文件,上傳該文件,然后在瀏覽器中看到上傳的文件。 為了保持簡單,我們假設(shè)用戶只會(huì)上傳圖片,然后我們應(yīng)用將該圖片顯示到瀏覽器中。

好,下面就一步步來實(shí)現(xiàn),鑒于此前已經(jīng)對(duì)JavaScript原理性技術(shù)性的內(nèi)容做過大量介紹了,這次我們加快點(diǎn)速度。

要實(shí)現(xiàn)該功能,分為如下兩步: 首先,讓我們來看看如何處理POST請求(非文件上傳),之后,我們使用Node.js的一個(gè)用于文件上傳的外部模塊。之所以采用這種實(shí)現(xiàn)方式有兩個(gè)理由。

第一,盡管在Node.js中處理基礎(chǔ)的POST請求相對(duì)比較簡單,但在這過程中還是能學(xué)到很多。?
第二,用Node.js來處理文件上傳(multipart POST請求)是比較復(fù)雜的,它_不_在本書的范疇,但,如何使用外部模塊卻是在本書涉獵內(nèi)容之內(nèi)。

處理POST請求

考慮這樣一個(gè)簡單的例子:我們顯示一個(gè)文本區(qū)(textarea)供用戶輸入內(nèi)容,然后通過POST請求提交給服務(wù)器。最后,服務(wù)器接受到請求,通過處理程序?qū)⑤斎氲膬?nèi)容展示到瀏覽器中。

_/start_請求處理程序用于生成帶文本區(qū)的表單,因此,我們將_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;

好了,現(xiàn)在我們的應(yīng)用已經(jīng)很完善了,都可以獲得威比獎(jiǎng)(Webby Awards)了,哈哈。(譯者注:威比獎(jiǎng)是由國際數(shù)字藝術(shù)與科學(xué)學(xué)院主辦的評(píng)選全球最佳網(wǎng)站的獎(jiǎng)項(xiàng),具體參見詳細(xì)說明)通過在瀏覽器中訪問http://localhost:8888/start就可以看到簡單的表單了,要記得重啟服務(wù)器哦!

你可能會(huì)說:這種直接將視覺元素放在請求處理程序中的方式太丑陋了。說的沒錯(cuò),但是,我并不想在本書中介紹諸如MVC之類的模式,因?yàn)檫@對(duì)于你了解JavaScript或者Node.js環(huán)境來說沒多大關(guān)系。

余下的篇幅,我們來探討一個(gè)更有趣的問題: 當(dāng)用戶提交表單時(shí),觸發(fā)_/upload_請求處理程序處理POST請求的問題。

現(xiàn)在,我們已經(jīng)是新手中的專家了,很自然會(huì)想到采用異步回調(diào)來實(shí)現(xiàn)非阻塞地處理POST請求的數(shù)據(jù)。

這里采用非阻塞方式處理是明智的,因?yàn)镻OST請求一般都比較“重” —— 用戶可能會(huì)輸入大量的內(nèi)容。用阻塞的方式處理大數(shù)據(jù)量的請求必然會(huì)導(dǎo)致用戶操作的阻塞。

為了使整個(gè)過程非阻塞,Node.js會(huì)將POST數(shù)據(jù)拆分成很多小的數(shù)據(jù)塊,然后通過觸發(fā)特定的事件,將這些小數(shù)據(jù)塊傳遞給回調(diào)函數(shù)。這里的特定的事件有_data_事件(表示新的小數(shù)據(jù)塊到達(dá)了)以及_end_事件(表示所有的數(shù)據(jù)都已經(jīng)接收完畢)。

我們需要告訴Node.js當(dāng)這些事件觸發(fā)的時(shí)候,回調(diào)哪些函數(shù)。怎么告訴呢? 我們通過在_request_對(duì)象上注冊監(jiān)聽器(listener) 來實(shí)現(xiàn)。這里的request對(duì)象是每次接收到HTTP請求時(shí)候,都會(huì)把該對(duì)象傳遞給_onRequest_回調(diào)函數(shù)。

如下所示:

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
});

問題來了,這部分邏輯寫在哪里呢? 我們現(xiàn)在只是在服務(wù)器中獲取到了_request_對(duì)象 —— 我們并沒有像之前_response_對(duì)象那樣,把 request 對(duì)象傳遞給請求路由和請求處理程序。

在我看來,獲取所有來自請求的數(shù)據(jù),然后將這些數(shù)據(jù)給應(yīng)用層處理,應(yīng)該是HTTP服務(wù)器要做的事情。因此,我建議,我們直接在服務(wù)器中處理POST數(shù)據(jù),然后將最終的數(shù)據(jù)傳遞給請求路由和請求處理器,讓他們來進(jìn)行進(jìn)一步的處理。

因此,實(shí)現(xiàn)思路就是: 將_data_和_end_事件的回調(diào)函數(shù)直接放在服務(wù)器中,在_data_事件回調(diào)中收集所有的POST數(shù)據(jù),當(dāng)接收到所有數(shù)據(jù),觸發(fā)_end_事件后,其回調(diào)函數(shù)調(diào)用請求路由,并將數(shù)據(jù)傳遞給它,然后,請求路由再將該數(shù)據(jù)傳遞給請求處理程序。

還等什么,馬上來實(shí)現(xiàn)。先從_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;

上述代碼做了三件事情: 首先,我們設(shè)置了接收數(shù)據(jù)的編碼格式為UTF-8,然后注冊了“data”事件的監(jiān)聽器,用于收集每次接收到的新數(shù)據(jù)塊,并將其賦值給postData?變量,最后,我們將請求路由的調(diào)用移到_end_事件處理程序中,以確保它只會(huì)當(dāng)所有數(shù)據(jù)接收完畢后才觸發(fā),并且只觸發(fā)一次。我們同時(shí)還把POST數(shù)據(jù)傳遞給請求路由,因?yàn)檫@些數(shù)據(jù),請求處理程序會(huì)用到。

上述代碼在每個(gè)數(shù)據(jù)塊到達(dá)的時(shí)候輸出了日志,這對(duì)于最終生產(chǎn)環(huán)境來說,是很不好的(數(shù)據(jù)量可能會(huì)很大,還記得吧?),但是,在開發(fā)階段是很有用的,有助于讓我們看到發(fā)生了什么。

我建議可以嘗試下,嘗試著去輸入一小段文本,以及大段內(nèi)容,當(dāng)大段內(nèi)容的時(shí)候,就會(huì)發(fā)現(xiàn)_data_事件會(huì)觸發(fā)多次。

再來點(diǎn)酷的。我們接下來在/upload頁面,展示用戶輸入的內(nèi)容。要實(shí)現(xiàn)該功能,我們需要將_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_中,我們將數(shù)據(jù)包含在對(duì)_upload_請求的響應(yīng)中:

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;

好了,我們現(xiàn)在可以接收POST數(shù)據(jù)并在請求處理程序中處理該數(shù)據(jù)了。

我們最后要做的是: 當(dāng)前我們是把請求的整個(gè)消息體傳遞給了請求路由和請求處理程序。我們應(yīng)該只把POST數(shù)據(jù)中,我們感興趣的部分傳遞給請求路由和請求處理程序。在我們這個(gè)例子中,我們感興趣的其實(shí)只是_text_字段。

我們可以使用此前介紹過的_querystring_模塊來實(shí)現(xiàn):

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;

好了,以上就是關(guān)于處理POST數(shù)據(jù)的全部內(nèi)容。

處理文件上傳

最后,我們來實(shí)現(xiàn)我們最終的用例:允許用戶上傳圖片,并將該圖片在瀏覽器中顯示出來。

回到90年代,這個(gè)用例完全可以滿足用于IPO的商業(yè)模型了,如今,我們通過它能學(xué)到這樣兩件事情: 如何安裝外部Node.js模塊,以及如何將它們應(yīng)用到我們的應(yīng)用中。

這里我們要用到的外部模塊是Felix Geisend?rfer開發(fā)的_node-formidable模塊。它對(duì)解析上傳的文件數(shù)據(jù)做了很好的抽象。 其實(shí)說白了,處理文件上傳“就是”_處理POST數(shù)據(jù) —— 但是,麻煩的是在具體的處理細(xì)節(jié),所以,這里采用現(xiàn)成的方案更合適點(diǎn)。

使用該模塊,首先需要安裝該模塊。Node.js有它自己的包管理器,叫NPM。它可以讓安裝Node.js的外部模塊變得非常方便。通過如下一條命令就可以完成該模塊的安裝:

npm install formidable

如果終端輸出如下內(nèi)容:

npm info build Success: formidable@1.0.9
npm ok

就說明模塊已經(jīng)安裝成功了。

現(xiàn)在我們就可以用_formidable_模塊了——使用外部模塊與內(nèi)部模塊類似,用require語句將其引入即可:

var formidable = require("formidable");

這里該模塊做的就是將通過HTTP POST請求提交的表單,在Node.js中可以被解析。我們要做的就是創(chuàng)建一個(gè)新的IncomingForm,它是對(duì)提交表單的抽象表示,之后,就可以用它解析request對(duì)象,獲取表單中需要的數(shù)據(jù)字段。

node-formidable官方的例子展示了這兩部分是如何融合在一起工作的:

var formidable = require('formidable'),
    http = require('http'),
    util = 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(util.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);

如果我們將上述代碼,保存到一個(gè)文件中,并通過_node_來執(zhí)行,就可以進(jìn)行簡單的表單提交了,包括文件上傳。然后,可以看到通過調(diào)用_form.parse_傳遞給回調(diào)函數(shù)的_files_對(duì)象的內(nèi)容,如下所示:

received upload:

{ fields: { title: 'Hello World' },
  files:
   { upload:
      { size: 1558,
        path: '/tmp/1c747974a27a6292743669e91f29350b',
        name: 'us-flag.png',
        type: 'image/png',
        lastModifiedDate: Tue, 21 Jun 2011 07:02:41 GMT,
        _writeStream: [Object],
        length: [Getter],
        filename: [Getter],
        mime: [Getter] } } }

為了實(shí)現(xiàn)我們的功能,我們需要將上述代碼應(yīng)用到我們的應(yīng)用中,另外,我們還要考慮如何將上傳文件的內(nèi)容(保存在_/tmp_目錄中)顯示到瀏覽器中。

我們先來解決后面那個(gè)問題: 對(duì)于保存在本地硬盤中的文件,如何才能在瀏覽器中看到呢?

顯然,我們需要將該文件讀取到我們的服務(wù)器中,使用一個(gè)叫_fs_的模塊。

我們來添加_/showURL的請求處理程序,該處理程序直接硬編碼將文件/tmp/test.png_內(nèi)容展示到瀏覽器中。當(dāng)然了,首先需要將該圖片保存到這個(gè)位置才行。

將_requestHandlers.js_修改為如下形式:

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

重啟服務(wù)器之后,通過訪問http://localhost:8888/show,就可以看到保存在_/tmp/test.png_的圖片了。

好,最后我們要的就是:

  • 在_/start_表單中添加一個(gè)文件上傳元素
  • 將node-formidable整合到我們的_upload請求處理程序中,用于將上傳的圖片保存到/tmp/test.png_
  • 將上傳的圖片內(nèi)嵌到_/upload_URL輸出的HTML中

第一項(xiàng)很簡單。只需要在HTML表單中,添加一個(gè)_multipart/form-data_的編碼類型,移除此前的文本區(qū),添加一個(gè)文件上傳組件,并將提交按鈕的文案改為“Upload file”即可。 如下_requestHandler.js_所示:

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" 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, 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;

很好。下一步相對(duì)比較復(fù)雜。這里有這樣一個(gè)問題: 我們需要在_upload_處理程序中對(duì)上傳的文件進(jìn)行處理,這樣的話,我們就需要將_request_對(duì)象傳遞給node-formidable的_form.parse_函數(shù)。

但是,我們有的只是_response_對(duì)象和_postData_數(shù)組??礃幼?,我們只能不得不將_request_對(duì)象從服務(wù)器開始一路通過請求路由,再傳遞給請求處理程序。 或許還有更好的方案,但是,不管怎么說,目前這樣做可以滿足我們的需求。

到這里,我們可以將_postData_從服務(wù)器以及請求處理程序中移除了 —— 一方面,對(duì)于我們處理文件上傳來說已經(jīng)不需要了,另外一方面,它甚至可能會(huì)引發(fā)這樣一個(gè)問題: 我們已經(jīng)“消耗”了_request_對(duì)象中的數(shù)據(jù),這意味著,對(duì)于_form.parse_來說,當(dāng)它想要獲取數(shù)據(jù)的時(shí)候就什么也獲取不到了。(因?yàn)镹ode.js不會(huì)對(duì)數(shù)據(jù)做緩存)

我們從_server.js_開始 —— 移除對(duì)postData的處理以及request.setEncoding?(這部分node-formidable自身會(huì)處理),轉(zhuǎn)而采用將_request_對(duì)象傳遞給請求路由的方式:

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_對(duì)象:

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;

現(xiàn)在,_request_對(duì)象就可以在我們的_upload請求處理程序中使用了。node-formidable會(huì)處理將上傳的文件保存到本地/tmp目錄中,而我們需要做的是確保該文件保存成/tmp/test.png_。 沒錯(cuò),我們保持簡單,并假設(shè)只允許上傳PNG圖片。

這里采用_fs.renameSync(path1,path2)_來實(shí)現(xiàn)。要注意的是,正如其名,該方法是同步執(zhí)行的, 也就是說,如果該重命名的操作很耗時(shí)的話會(huì)阻塞。 這塊我們先不考慮。

接下來,我們把處理文件上傳以及重命名的操作放到一起,如下_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" multiple="multiple">'+
    '<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) {
  console.log("Request handler 'upload' was called.");

  var form = new formidable.IncomingForm();
  console.log("about to parse");
  form.parse(request, function(error, fields, files) {
    console.log("parsing done");
    fs.renameSync(files.upload.path, "/tmp/test.png");
    response.writeHead(200, {"Content-Type": "text/html"});
    response.write("received image:<br/>");
    response.write("<img src='/show' />");
    response.end();
  });
}

function show(response) {
  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;

好了,重啟服務(wù)器,我們應(yīng)用所有的功能就可以用了。選擇一張本地圖片,將其上傳到服務(wù)器,然后瀏覽器就會(huì)顯示該圖片。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)