第二章簡單的 Node.js 框架——2.1 ​HTTP 服務器

字號+ 編輯: Snake 修訂: 种花家 來源: 《为PHP开发者写的Node.js学习指南》 2023-09-09 我要說兩句(0)

在之前的章節,我介紹了一個用於 PHP 到Node.js 轉換的開發環境,以及如何使用它進行轉換。在本章,我們將開始使用這個開發環境並進行實際的轉換。

在 PHP 中,一個 PHP 文档代表一個 HTML 頁面。一個 Web 服務器,比如 Apache, 當請求一個 PHP 頁面時,Web 服務器會運行 PHP。但是在 Node.js 裡,Node.js 的main 文档代表了整個服務器。Node.js 並不是運行在類似 apache 這樣的 Web 服務器中,而是取代 apache. 因此,我們需要一些啓動代碼來讓 Web 服務器正常工作。

我們來看看上一個章中作爲例子的 httpsvr.njs,下面是 httpsvr.njs 中的 Node.js 代碼:

var http s require('http');
var static = require('node-static');
var file = new static.Server();
http.createServer(function (req, res) {
file.serve(req, res);
}).listen(l337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/')

它是如何工作的呢?

如同在前面章節中提到的,require()函數可以加載一個模塊並使用該模塊。前兩行代碼分別展示了加載一個内建模塊和外部模塊:

var http = require('http'); //built-in module
var static = require('node-static');// external module

如果你按照上一章的内容安裝了 Node.js 並且實現了所有的例子,那麽應該已經安裝了包含 node-static 外部模塊的 node-static npm 包。如果沒有,你可以執行下面的npm 命令進行安裝:

npm install node-static

第三行代碼有一點技巧:

var file = new static.Server();

non-static 模塊想要在Node.js 服務器提供多個文档服務對象給客戶耑使用而不僅僅是被限制在單個文档服務對象上。因此,它沒有使用模塊本身作爲文档服務對象, 而是通過調用構造函數來創建一個文档服務對象。構造函數被設計成跟 new 關鍵字一起使用。使用 new 關鍵字調用構造函數就可以創建一個新的對象。

在這個例子裡,模塊對象被命名爲 static。在這個模塊對象内部有一個鍵值對(key-value),key  是  Server,value  是一個構造函數。點操作符(.)指明了這種關係,使得 new 關鍵字正確作用在構造函數上。創建一個新的文档服務對象並存在file 變量裡。

文档服務對象的構造函數接受零個或一個參數。當沒有提供參數的時候,文档服務對象會使用當前的目錄(文档夾)作爲 HTTP 服務器的頂級目錄。例如,如果httpsvr.njs  是在  ferris  目錄下運行的,那麽當瀏覽器如  Google  Chrome  訪問http://127.0.0.1:1337/hello.html 時,文档服務對象會在 ferris 目錄下尋找 hello.html文档。當瀏覽器訪問 http://127.0.0.1:1337/exit/goodbye.html 時,文档服務對象會在ferris 目錄下的 exit 目錄裡尋找 goodbye.html 文档。

然而,當使用有參的構造函數時,文档服務對象會到參數所指定的目錄内查詢文档。例如,當“..”字符串作爲構造函數的參數時,被創建出來的文档服務對象會去當前目錄的父目錄裡查詢文档。require()函數隻接受一個參數,即要加載的模塊的名字。這裡並沒有提供更便捷的方式在用來在模塊加載過程中傳遞額外參數。雖然它需要指定加載文档的目錄作爲參數,但是最好還是把加載模塊和指定從哪裡加載文档的操作完全分開。

在文档服務對象創建好以後,HTTP 服務器對象就可以接受 HTTP 請求並把文档返回給客戶耑,例如 Web 瀏覽器:

http.createServer(function(req,res){
file.serve(req,res);
}).listen(1337,'127.0.0.1');

上面的代碼可以改寫成下面三條語句,更容易理解:

var handleReq (function(req, res) {
file.serve(req, res);
};
var svr = http.createServer(handle'leReq);
svr.listen(1337, '127.0.0.1');

第一條語句由三行代碼組成。它定義了一個叫 handleReq 的變量,這個變量的值不是類似字符串或數字這樣“一般”的值,而是一個函數。在 Node.js 裡面,可以將函數賦值給一個變量,像字符串和數字一樣。當一個函數被賦值給一個變量時,這個函數被叫做回調函數,並爲了方便起見,被賦值的變量被叫做回調變量。回調變量的定義和常槼函數定義基本相同,不同的是回調函數不需要命名可以通過賦值給變量來使用。

在這個例子裡,回調函數需要兩個變量。第一個變量 req,包含進行 HTTP 請求的所有數據。第二個變量,res 包含 HTTP 響應的所有數據。在這種實現裡,文档服務對象 file 負責解析 HTTP 請求,在磁盤上找到對應的文档,然後把數據寫入到HTTP 響應中,文档就這樣被返回到瀏覽器中。Node-static 模塊就是依據這種思想來設計的,僅需一行代碼文档服務對象就能返回磁盤文档。

第四行代碼創建了一個 HTTP 服務器和一個處理 HTTP 請求的循環,它會一直接收新的 HTTP 請求並使用 handleReq 回調函數來執行這些請求:

var svr = http.createServer(handleReq);

在 createServer()函數内部,handleReq 變量會按如下方式被調用:

function createServer(handleReq){
white (true){
//wait for HTTP request
var req = decodeHttppequest(); // somehow decode the request
var res = createHttpRequest(); // somehow create a response object handleReq(req, res); // invoke handleReq()
// send WTTP response back to client based on "res" object

回調變量與一般的函數一樣(但不同於其他類型的變量)可以調用它所包含的函數。正如你可以看到的,調用 handleReq 回調函數的參數與調用一般的函數是相同的。事實就是這樣的,即使 handleReq 不是函數的名字僅僅是回調變量或參數的名字。回調變量可以作爲參數傳遞給其他函數,就像其他類型的變量一樣。

爲什麽不直接將 file.serve()調用直接硬編碼到 createServer()函數中呢?難道提供文档不是一個 Web 服務器該做的事情嗎?

function createServer(handleReq){
  while (true){
    // wait for HTTP request
    var res = decodeHttpRequest(); // somehow decode the request
    var res = createHttpRequest(); // somehow create a request object
    file.serve(req,res); // hardcode a reference to the "node static" module
    // send HTTP response back to the client based on "res" object
    }
}

這樣也是可以的,但是把回調函數傳遞給 createServer 函數會更加靈活。記住:http模塊是  node.js  内建的,而  node  static  模塊是一個獨立安裝的  npm  包。如果將file.serve()調用寫到 createServer()函數中,當我們使用另一個模塊替代 node static 模塊或者添加一些自定義的處理 HTTP 請求的代碼時,就需要複制粘貼整個createServer()函數才能調整其中一行代碼。因此,它使用回調函數。所以,你仔細想想,回調函數是一種調用代碼的時將自己的代碼插入到被調用的函數中的方法。它是在不修改函數本身代碼的情況下改變函數的行爲的方法。在本例中被調用的函數 createServer()必須期望和支持回調函數,但是在編寫程序的時候就考慮到回調函數的話,那麽調用者可以創建一個匹配它的期望的回調函數,這樣函數就可以使用整個回調而不用調用代碼的任何詳細内容。回調函數使得兩段代碼能在一起工作, 即使它們是不同人在不同時間編寫的。

在這個例子中,通過傳遞作爲參數的回調函數,調用者可以以適合它的任何方式來處理 HTTP 請求。但是,在大多數應用場景中,作爲參數傳遞進去的回調函數會在異步請求完成後被調用。在下一個章節中,將會詳細講解回調函數的這類應用。

第五行代碼使用 svr 對象監聽‘127.0.0.1’計算機,又名‘localhost’計算機(即運行Node.js 服務器的計算機)上的 1337 耑口:

svr.listen(1337,'127.0.0.1');

指出的是, 處理 HTTP 請求的循環更可能在 listen() 函數裡而不是createServer()函數。但是這裡的目的是解釋回調函數,所以這不是大問題。

因爲 svr 變量和 handleReq 變量僅被使用了一次因此可以使用更簡潔的代碼來替換他們,這樣三條語句可以合並成一條:

http.createServer(function(req,res){
    file.serve(req,res);
}).listen(1337,'127.0.0.1');

httpsvr.njs 的最後一行代碼會向控制台中輸出一條信息,這樣當某人啓動 HTTP 服務器時,他可以知道該如何訪問它:

console.log('Server running at http://127.0.0.1/');

httpsvr.njs 文档構建了一個基本的 Node.js HTTP 服務器。現在我們將組成 Web 應用程序的所有的文档從 PHP 服務器中移動到 httpsvr.njs 所在的文档夾中。當 httpsvr.njs 啓動後,所有這些文档包括 HTML、CSS、客戶耑 JavaScript、圖片文档(如 PNG 文档),以及其他相關的文档都可以被傳送給客戶耑了,可能是瀏覽器,它們就會像之前那樣正常工作。客戶耑僅需要指向正確的耑口(例如,1337)就可以從 Node.js創建的服務器加載文档了。現在 Web 應用程序會出問題的唯一原因是它仍然是用PHP 寫的,但是因爲 HTML,CSS,客戶耑 JavaScript 和圖片文档都是完全在客戶耑處理和執行的,所以除非需要 PHP 文档的支持,它們都能正常工作。.php 文档也可以被移動到 Node.js 服務器,但是因爲 Node.js 不能解釋.php 文档,所以它們並不能正常工作。

.php 文档和其他文档的最大的不同點是.php 文档會被服務器解釋,解釋結果會作爲HTTP 響應返回給客戶耑。而對於其他文档,服務器會讀取它們的内容並直接寫入到 HTTP 響應中。如果.php 文档沒經過解析,那麽客戶耑會接收到 PHP 源碼,而它並不知道如何使用這些源碼。客戶耑需要這些源碼的解析結果才能工作,而不是源碼本身。PHP 到 Node.js 的轉換過程歸結來說就是,使用 Node.js 代碼來産生 PHP 代碼完全相同的輸出結果。這看起來相當簡單,但是工作量很大。

首先,需要爲每一個.php 文档創建一個本地模塊。對於本書來說,一個本地模塊就是一個本地的.njs 文档,main 函數文档可以通過 require()函數來加載這個模塊。爲了給一個.php 文档創建一個 Node.js 模塊,我們可以在.php 文档相同的文档夾創建一個相同文档名的.njs 空文档。例如,對於 admin/index.php,創建一個空的 admin/index.njs 文档。

對於這些轉換工作,你需要自己做出判斷。在某些情況下,有相當多的.php 文档, 創建對應的.njs 文档會做很多無用功,因此開始的時候只創建一小部分會比較好。但是在其他情況,只有很少的.php 文档需要轉換,所以一次性創建所有的對應的空文档會比較有效率。

一旦你創建好了對應的.njs 文档後,選擇一個.php 文档並修改它對應的.njs 文档:

ecports.serve = function(req,res){
    res,writHead(200,{'Content-Type':'text/plain'});
res.end('admin/index.njs');
};

把這些代碼寫到.njs 文档中並且修改 res.end()函數調用的路徑(在這個例子中是admin/index.njs)指向正在被修改的這個.njs 文档。

通過這段簡單的代碼,這個.njs 文档就是一個實現了 exports.serve()這個存根函數的Node.js 模塊了。存根實現是指那些使用簡單代碼作爲佔位符,稍後再用一個更可靠的實現來替換。exports.serve()存根函數有兩個參數,它們對應於 httpsvr.njs 中傳遞給 http.createServer()的回調函數的兩個參數。Req 參數是 HTTP request 對象,res 參數是 HTTP response 對象。

當 exports.serve()函數被調用的時候,存根實現會返回一個 HTTP response,它的一個 HTTP  頭的 Content-Type 被設置爲“text/plain”,内容則是正在被調用的文档。通過自定義的存根實現使得後期更容易調試,以免一個編碼錯誤導致請求一個文档的 HTTP request 意外結束在另一個文档中。

一旦你有了一個實現了 exports.serve 的.njs 文档,你應該在適當的時機修改httpsvr.njs 文档來調用 exports.serve 函數。但是首先,必須使 httpsvr.njs 能訪問這個模塊。通過使用 Node.js 中的 require()函數可以像加載内建模塊一樣加載本地模塊:

var admin_index = require('./admin/index.njs');

在加載本地模塊的時候,參數需要一個以./開頭的路徑名。./指明了一個路徑名是一個本地模塊而不是内建模塊或 npm 包。

在這個例子中,該本地模塊被賦值給 admin index 變量。使用那些包含了實現了.php文档同等功能的 Node.js 代碼的模塊文档的路徑名的變體作爲存儲模塊的變量的名字會帶來很大的便利並且不容易混淆,當然這取決這個 Web 應用程序中的.php 文档的數量(即對應的.njs 文档的數量)。

爲了將對某個 PHP 頁面的 HTTP request 傳遞到正確的 Node.js 本地模塊,我們需要修改傳遞給  http.createServer()的回調函數驗证並傳遞  HTTP  request  到適當的Node.js 本地模塊。通過在 if 語句中使用一個簡單比較,可以檢查該 HTTP request 是否正在請求一個特定的.php 文档,如果是,則把它傳遞到合適的 Node.js 本地模塊上,否則傳遞到 node static npm 包(例如,file 變量)。

http.createServer((req,rea){
    if (url.parse(req.url).pathname == '/admin/index.php'){
        admin_index.serve(req,res);
    } else {
        file.serve(req,res);
    }
}).listen(1337,'127.0.0.1');

這段源碼使用了 Node.js 内建的 http 和 url 模塊實現了一個簡單但是功能完整的 Node.js 服務器。如果需要的話,可以安裝使用一些更複雜的包,如 express Node.js 包。

在這個實現中,需要使用内建的 url 模塊來從 HTTP request 中解析 URL。下面的代碼使用 require()函數訪問内建的 url 模塊:

var url = require('url');

讓我們繼續前面的例子,當 HTTP request 正在請求/admin/index.php URL 資源,那麽本地模塊 admin index 會解釋這個請求並返回一個 HTTP response,而不是使用file 變量讀取一個文档並以靜態文档返回。

你應該已經注意到了 HTTP request 中 URL 是與/admin/index.php 進行對比的,而不是/admin/index.njs。因爲瀏覽器使用自定義的代碼處理對.php 文档的請求,它可以以任何方式來處理.php 文档的請求,包括忽略磁盤上.php 文档並使用 Node.js 代碼來代替運行。一個 HTTP request 是一個請求,而不是一個需求,Node.js 服務器可以不適用完全不適用  PHP  的情況下處理.php  文档的請求。在這段  Node.js  代碼中,.php  文档引用變成了進行一個特定操作的唯一標示符,.php  擴展符不再代表PHP。回調函數中 if-else 語句僅僅是將一個唯一的路徑匹配到一段 Node.js 代碼上。這個語義表明.php 擴展符已經被忽略了。

最有可能的是,客戶耑正在請求一個.php 文档或者是由於用戶點擊了一個 HTML頁面中的鏈接,又或者這是 JavaScript AJAX 調用的一部分。爲了改變這種情況,HTML 和客戶耑 JavaScript 中對.php 文档的引用需要改成.njs。這些改變取決你的實際情況。從有利的一方面來說,移除.php 文档擴展符可以消除訪問你代碼的程序員的困惑,他們可能會奇怪爲什麽在 Node.js 代碼庫裡會有 PHP 引用。從不利的方面來看,移除.php 文档引用會在 PHP 和Node.js 代碼庫之間産生一些不必要的技術上的差異。

在一步一步解釋完單個.php 文档後,讓我們把所有這些融合到一起來處理多個.php 文档:

var http = require('http');
var static = require('node-static');
var file = new static.Server();
var url = require('url');
var index = require('./index.njs');
var login = require('./login.njs');
var admin_index = require('./admin/index.njs');
var admin_login = require('./admin/login.njs');
http.createServer(function(req,res){
    if(url.parse(req.url).pathname == '/index.php'){
        index.serve(req,res);
    } else if (url.parse(req.url).pathname == '/login.php'){
        login.serve(req,res);
    } sele if (url.parse(req,res).pathname == '/admin/index.php'){
        admin_index.serve(req,res);
    } else if (url.parse(req,res).pathname == '/admin/login.php'){
        admin_login.serve(req,res);
    } sele {
        file.serve(req,res);
    }
    }).listen(1337,'127.0.0.1');
    console.log('Server running at http://127.0.0.1/');

這裡有兩個重要的修改:(1)添加了一組 require()函數調用,每一個 require()函數對應於一個.php 文档;(2)添加了一組 else-if 語句與每一個.php 文档一一對應。在這個例子中,這個修改過的 httpsvr.njs 函數會處理四個.php 文档:index.njs、login.njs、 admin/index.njs 和 admin/login.njs。這些文档裡面的代碼都相同,除了對指向那個文档的路徑名的修改:

exports.serve = function(req,res){
    res.writeHead(200,{'Content-Type':'text/plain'});
    res.end('admin/index.njs');
    };

每一個.njs 文档中的 exports.serve()函數中所包含的代碼完整地模擬了它對應的.php 文档的操作。我們將會移除存根實現中僅返回.njs 文档的名字作爲 HTTP response 的操作,而將對應的.php 文档中的 PHP 代碼複制粘貼到 exports.serve()函數中,並且通過一系列的變形和轉換的技巧將這些 PHP 代碼變成相同功能的 Node.js 代碼。但是我們還沒有準備好這樣做。

閲完此文,您的感想如何?
  • 有用

    0

  • 沒用

    0

  • 開心

    0

  • 憤怒

    0

  • 可憐

    0

1.如文章侵犯了您的版權,請發郵件通知本站,該文章將在24小時内刪除;
2.本站標注原創的文章,轉發時煩請注明來源;
3.交流群: 2702237 13835667

相關課文
  • JS如何防止父節點的事件運行

  • nodejs編寫一個簡單的http請求客戶耑代碼demo

  • 說一則爲什麽後耑開發人員不選擇node.js的原因

  • 使用Sublime Text3 開發React-Native的配置

我要說說
網上賓友點評