Node.js 系列 - 搭建路由 & 處理表單提交

罐裝汽水_Garrik發表於2018-10-29

作為還在漫漫前端學習路上的一位自學者。我以學習分享的方式來整理自己對於知識的理解,同時也希望能夠給大家作為一份參考。希望能夠和大家共同進步,如有任何紕漏的話,希望大家多多指正。感謝萬分!


之前, 我們搭建了靜態檔案伺服器. 使用者通過在瀏覽器搜尋欄輸入 URL 來請求儲存在伺服器的指定檔案. 但是除了提供靜態檔案, 伺服器能做的還有很多很多. 在這一篇, 我們要學會用 Node.js 處理從前端頁面的 HTML 表單中提交的資訊.

搭建路由

在日常, 我們訪問一個站點的不同地址時, 通常頁面內容也隨之改變. 這是因為伺服器為了實現更多的功能, 其會根據請求 URL 的不同而做出不同的處理, 這被稱作 "路由"

『 路由 』簡單來說就是 請求和請求處理程式碼之間的對映關係. 當伺服器為一個特定 URL 掛在了請求處理程式碼時, 所有針對於這個特定 URL 的請求都會交由其處理.

假設我們要做一個用於自我介紹的個人網頁, 其包含: "主頁". "專案介紹頁面", "關於我頁面".

那麼我們可以像下面程式碼中那樣來搭建路由規則:

// 引入相關模組
var http = require('http');
var url = require('url');

// 搭建 HTTP 伺服器
var server = http.createServer(function(req, res) {
    // 獲取請求 URL, 根據 URL 中的 pathname 來匹配對應的處理方法.
    var urlObj = url.parse(req.url);
    var urlPathname = urlObj.pathname;

    switch (urlPathname) {
        case "/main":
            // 因為返回內容中有中文, 所以別忘了指定編碼方式
            res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
            res.write("主頁頁面");
            res.end();
            break;
        case "/aboutme":
            res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
            res.write("關於我頁面");
            res.end();
            break;
        case "/projects":
            res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
            res.write("專案介紹頁面");
            res.end();
            break;
        // 如果都不匹配就返回 404 
        default:
            res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
            res.write("404 - Not Found");
            res.end();
            break;
    }
});

// 在 3000 埠監聽請求
server.listen(3000, function() {
    console.log("伺服器執行中.");
    console.log("正在監聽 3000 埠:")
})
複製程式碼

上面程式碼根據請求 URL 的不同, 而將請求交給不同的處理程式碼. 你可以嘗試執行伺服器, 然後用瀏覽器去請求相應的 URL, 來看看得到的響應是什麼.

123


獲取 GET 表單提交

在學習了路由相關知識之後, 我們再來了解一下如何獲取從客戶端發過來的 表單提交.

我們先介紹用 GET 方法提交的表單. 通過 GET 提交的表單內容會組裝成『 查詢字串 』嵌入在請求 URL 裡. 例如下面這段:

https://www.zhihu.com/search?type=content&q=罐裝汽水Garrik
複製程式碼

? 問號開始就是這段 URL 的查詢字串; 引數之間用 & 分開; = 等號前面的是引數名, 後面的是引數值.

上面這段 URL 的查詢字串如何解析成 JSON 的話就是:

{
    "type": "content",
    "q": "罐裝汽水Garrik"
}
複製程式碼

那麼再簡單瞭解了基礎知識之後呢, 就讓我們趕快來寫程式碼吧!

首先讓我們來寫一個有 HTML 表單的頁面, 然後命名為 login.html (當然你也可以按照你的想法寫程式碼和命名)

這個表單我想用來提交登入資訊, form 元素的 action 屬性我定義為 login, 意思是將請求傳送到 login 這段路徑下. method 屬性我定義為 get, 意思是以 GET 方法提交表單.

<body>
    <form action="login" method="get">
        賬戶: <input type="text" name="username" />
        <br /> 
        密碼: <input type="text" name="password" />
        <br />
        <input type="submit" value="提交">
    </form>
</body>
複製程式碼

之後再讓我們來寫伺服器程式碼. 通過前面的介紹, 你知道我們需要解析 URL 的查詢字串. 做到這點很簡單, 只需要在呼叫 url.parse 函式解析請求 URL 時為其傳入第二個引數 true. 這個函式就會自動幫你把 URL 的查詢字串解析成一個 JavaScript 物件了, 儲存在函式返回物件的 query 屬性中. 如果沒有查詢的話屬性值就是 null

我們可以用路由去匹配路徑, 當請求 URL 的路徑和表單傳送的路徑相匹配時, 將請求交給特定程式碼去處理.

var server = http.createServer(function(req, res) {
    // 解析請求 URL
    var urlObj = url.parse(req.url, true);
    // 獲取請求 URL 的路徑
    var urlPathname = urlObj.pathname;
    // 獲取請求 URL 的查詢字串解析成的物件
    var queryObj = urlObj.query;
    
    // 路由
    switch (urlPathname) {
        // 響應 login 頁面
        case "/":
        case "":
            // 我用了靜態伺服器那篇的模組, 不瞭解的地方可以去那篇參考
            readStaticFile(res, "./login.html");
            break;
        // 響應查詢物件的 JSON 形式到瀏覽器 
        case "/login":
            res.writeHead(200, { "Content-Type": "text/plain" });
            res.write(JSON.stringify(queryObj));
            res.end();
            break;
        // 錯誤處理
        default:
            readStaticFile(res, "./404.html");
    }
});
複製程式碼

當執行起伺服器之後, 訪問 login 頁面, 提交表單你看到的應該像是下面這樣:

Screen Shot 2018-10-09 at 12.49.09 AM

Screen Shot 2018-10-09 at 12.49.20 AM


獲取 POST 表單提交

說完 GET, 我們再來說說用 POST 方法提交表單. 不同於用 GET 方法時, 提交的內容都包含在 URL 裡. POST 提交的內容全部的都在請求體中.

我們 HTTP 伺服器 http.createServer 接收的請求物件 req 並沒有一個屬性內容為請求體. 原因是 POST 請求體可能體積非常大, 如果每次接收請求都包含請求體的話會很耗時. 而且萬一遇到了惡意 POST 請求攻擊, 伺服器的資源就被大大地浪費了.

為了獲取 POST 請求體, 我們需要手動來操作. 因為 POST 請求資料量可能很大, 所以它被拆分成了很多個小資料塊 ( chunk ) 我們通過在伺服器監聽請求物件 req'data' 事件來一個個地接收這些資料塊, 並將其拼接在一起.

當請求傳輸完畢, 會觸發請求物件 req'end' 事件. 我們需要監聽它, 事件觸發後, 在其事件處理函式中解析 POST 的請求體.

var server = http.createServer(function(req, res) {
    var urlObj = url.parse(req.url, true);
    var urlPathname = urlObj.pathname;

    switch (urlPathname) {
        case "/":
        case "":
            readStaticFile(res, "./login.html");
            break;
        case "/login":
            // 當請求方法為 POST 時觸發
            if (req.method === 'POST') {
                // 用於儲存拼接後的請求體
                var post = '';
                // 'data' 事件觸發, 將接受的資料塊 chunk 拼接到 post 變數上
                req.on('data', function(chunk) {
                    post += chunk;
                });
                // 請求完畢, 'end' 事件觸發
                req.on('end', function() {
                    // querystring 是 Node.js 自帶模組, parse 方法用於將查詢字串解析成物件
                    var queryObj = querystring.parse(post);
                    // 將接收的 POST 請求體以 JSON 格式響應回客戶端
                    res.writeHead(200, { "Content-Type": "text/plain" });
                    res.write(JSON.stringify(queryObj));
                    res.end();
                });
            }
            break;
        default:
            readStaticFile(res, "./404.html");
    }
});
複製程式碼

對了, 最重要的一點, 別忘了將 login.html 檔案中的表單提交方法從 get 改成 post

<body>
    <form action="login" method="post">
        <!-- 省略了 -->
    </form>
</body>
複製程式碼

現在執行伺服器, 提交表單, 看看結果是什麼. 應該效果像下圖所示:

Screen Shot 2018-10-09 at 12.49.09 AM

Screen Shot 2018-10-09 at 10.43.14 AM


POST 檔案上傳

檔案上傳我們可以很方便的用第三方模組 formidable 來實現.

首先用 npm 來安裝模組:

npm install formidable --save
複製程式碼

formidable 是用於是表單資料解析的模組, 非常適合用於檔案上傳的處理. 使用該模組時, 先要呼叫它的 IncomingForm 建構函式初始模組. 該函式返回一個 IncomingForm 例項用於解處理表單提交資料. 之後通過呼叫該例項的 parse 方法來解析資料.

當使用者使用表單提交資料時,表單中可能會包含兩類資料: 普通表單資料, 檔案資料. parse 方法解析時,會將這兩種資料分別放到fieldsfiles 這兩個回撥引數中.

那麼不多廢話直接上程式碼:

// 模組引入
var formidable = require('formidable');

var server = http.createServer(function(req, res) {
    var urlObj = url.parse(req.url, true);
    var urlPathname = urlObj.pathname;

    switch (urlPathname) {
        case "/":
        case "":
            readStaticFile(res, "./upload.html");
            break;
        // 路由為 '/upload'
        case "/upload":
            if (req.method === 'POST') {
                // 初始化 formidable 的 IncomingForm 例項
                var form = new formidable.IncomingForm();

                // uploadDir 設定上傳檔案時臨時檔案存放的位置
                form.uploadDir = "./uploads";
                // keepExtensions 屬性設定是否保留上傳檔案的副檔名, 預設為 false
                form.keepExtensions = true;
                
                // 開始解析
                form.parse(req, function(err, fields, files) {
                    if (err) {
                        var message = "檔案解析失敗";
                    } else {
                        var message = "檔案上傳成功";
                    }
                    res.writeHead(200, { "Content-Type": "text/plain;charset=utf-8" });
                    res.write(message);
                    res.end();
                })
            }
            break;
        default:
            readStaticFile(res, "./404.html");
    }
});
複製程式碼

伺服器程式碼寫完後, 讓我們寫 upload.html 檔案:

<body>
    <form action="upload" enctype="multipart/form-data" method="post">
        <input type="file" name="upload" />
        <br />
        <input type="submit" value="提交">
    </form>
</body>
複製程式碼

注意要設定的表單的編碼方式 enctype"multipart/form-data" 表單資料預設的編碼方式為 "application/x-www-form-urlencoded" 不可用於檔案上傳. 在使用包含檔案上傳控制元件的表單時,必須使用 "multipart/form-data" 這個值.

寫好後, 執行伺服器, 上傳一張你喜歡的照片, 看看結果是什麼. 以下是我的操作:

Screen Shot 2018-10-09 at 11.54.45 AM

Screen Shot 2018-10-09 at 11.55.17 AM

可以看到照片已經上傳到了 uploads 目錄下.

GET vs POST

前面分別用 GET 和 POST 方法提交了表單, 那麼這兩種方法到底區別是什麼呢?

先來看看 MDN 對這兩個方法的定義:

  • 『 HTTP GET 方法 』: 請求指定的資源. 使用 GET 的請求應該只用於獲取資料
  • 『 HTTP POST 方法 』: 傳送資料給伺服器

上面說的已經很簡潔, 當你想要請求伺服器上的資源時用 GET 方法. 傳送資料時用 POST 方法. 像我之前用 GET 方法提交登入資訊, 是不符合規範的. 實際開發中, 這種行為不允許出現.

說完定義, 讓我們再來看看這兩種方法在表現上有什麼不同.

  • 善於觀察的你一定已經發現, GET 提交的表單資料顯式地新增在了請求 URL 的查詢字串中. 而 POST 把提交的資料放置在了請求體中. 這也體現出為什麼 GET 不能用於傳輸資料, 你總不希望你的賬號和密碼這麼明顯地暴露在 URL 裡吧.

  • 因為瀏覽器對 URL 的長度都有限制, 所以 GET 方式提交的資料是有大小限制的, 一般不超過 1024 位元組. 理論上講, POST 提交資料時沒有大小限制的. 但出於效能考慮, 伺服器接收時可能對 POST 傳輸的資料大小進行限制.


? 好啦,今天的分享就告一段落啦。下一篇中,我會介紹 "模板引擎"

如果喜歡的話就點個關注吧!O(∩_∩)O 謝謝各位的支援❗️

相關文章