Express 教程第 6 部分:使用表單

在本教程中,我們將向您展示如何使用 Pug 在 Express 中處理 HTML 表單。特別是,我們將討論如何編寫表單來從網站資料庫中建立、更新和刪除文件。

預備知識 完成所有之前的教程主題,包括Express 教程第 5 部分:顯示圖書館資料
目標 瞭解如何編寫表單以從使用者獲取資料,並使用此資料更新資料庫。

概述

HTML 表單是網頁上一個或多個欄位/小部件的集合,可用於從使用者那裡收集資訊以提交到伺服器。表單是收集使用者輸入的靈活機制,因為有適合輸入多種不同型別資料(文字框、複選框、單選按鈕、日期選擇器等)的表單輸入。表單也是一種與伺服器共享資料的相對安全的方式,因為它們允許我們使用跨站請求偽造保護在 POST 請求中傳送資料。

使用表單可能很複雜!開發人員需要編寫表單的 HTML,在伺服器(可能也在瀏覽器)上驗證和正確清理輸入的資料,用錯誤訊息重新發布表單以通知使用者任何無效欄位,在成功提交資料後處理資料,最後以某種方式響應使用者以指示成功。

在本教程中,我們將向您展示如何在 Express 中執行上述操作。在此過程中,我們將擴充套件 LocalLibrary 網站,允許使用者從圖書館建立、編輯和刪除專案。

注意:我們尚未研究如何限制特定路由給經過身份驗證或授權的使用者,因此此時,任何使用者都將能夠對資料庫進行更改。

HTML 表單

首先簡要概述HTML 表單。考慮一個簡單的 HTML 表單,其中包含一個用於輸入某個“團隊”名稱的文字欄位及其關聯的標籤。

Simple name field example in HTML form

表單在 HTML 中定義為 <form>…</form> 標籤內的一組元素,其中至少包含一個 type="submit"input 元素。

html
<form action="/team_name_url/" method="post">
  <label for="team_name">Enter name: </label>
  <input
    id="team_name"
    type="text"
    name="name_field"
    value="Default name for team." />
  <input type="submit" value="OK" />
</form>

雖然這裡只包含一個(文字)欄位用於輸入團隊名稱,但表單*可以*包含任意數量的其他輸入元素及其關聯的標籤。欄位的 type 屬性定義將顯示哪種小部件。欄位的 nameid 用於在 JavaScript/CSS/HTML 中標識該欄位,而 value 定義了該欄位首次顯示時的初始值。匹配的團隊標籤使用 label 標籤指定(參見上面的“輸入名稱”),其中 for 欄位包含關聯 inputid 值。

submit 輸入將顯示為一個按鈕(預設情況下)——使用者可以按下此按鈕將其他輸入元素(在本例中,只是 team_name)包含的資料上傳到伺服器。表單屬性定義用於傳送資料的 HTTP method 和伺服器上資料的目的地(action)。

  • action:當表單提交時,資料將傳送到此資源/URL 進行處理。如果未設定(或設定為空字串),則表單將提交回當前頁面 URL。
  • method:用於傳送資料的 HTTP 方法:POSTGET
    • 如果資料將導致伺服器資料庫發生更改,則應始終使用 POST 方法,因為這樣可以更好地抵抗跨站偽造請求攻擊。
    • GET 方法只應用於不更改使用者資料的表單(例如,搜尋表單)。建議在您希望能夠書籤或共享 URL 時使用它。

表單處理過程

表單處理使用了我們學習顯示模型資訊的所有相同技術:路由將我們的請求傳送到控制器函式,該函式執行所有必需的資料庫操作,包括從模型中讀取資料,然後生成並返回 HTML 頁面。使事情更復雜的是,伺服器還需要能夠處理使用者提供的資料,並在出現任何問題時重新顯示帶有錯誤資訊的表單。

下面顯示了處理表單請求的過程流程圖,從請求包含表單的頁面(顯示為綠色)開始。

Web server form request processing flowchart. Browser requests for the page containing the form by sending an HTTP GET request. The server creates an empty default form and returns it to the user. The user populates or updates the form, submitting it via HTTP POST with form data. The server validates the received form data. If the user-provided data is invalid, the server recreates the form with the user-entered data and error messages and sends it back to the user for the user to update and resubmits via HTTP Post, and it validates again. If the data is valid, the server performs actions on the valid data and redirects the user to the success URL.

如上圖所示,表單處理程式碼主要需要做的事情是:

  1. 使用者首次請求時顯示預設表單。

    • 表單可能包含空白欄位(例如,如果您正在建立新記錄),或者可能預填充了初始值(例如,如果您正在更改記錄,或者具有有用的預設初始值)。
  2. 接收使用者提交的資料,通常透過 HTTP POST 請求。

  3. 驗證並清理資料。

  4. 如果任何資料無效,重新顯示錶單——這次顯示使用者填寫的值和問題欄位的錯誤訊息。

  5. 如果所有資料都有效,執行所需操作(例如,將資料儲存到資料庫中,傳送通知電子郵件,返回搜尋結果,上傳檔案等)。

  6. 所有操作完成後,將使用者重定向到另一個頁面。

通常,表單處理程式碼使用 GET 路由用於表單的初始顯示,以及指向相同路徑的 POST 路由用於處理表單資料的驗證和處理。這就是本教程將使用的方法。

Express 本身不提供任何特定的表單處理操作支援,但它可以使用中介軟體來處理表單中的 POSTGET 引數,並驗證/清理它們的值。

驗證與清理

在儲存表單資料之前,必須對其進行驗證和清理。

  • 驗證檢查輸入的值是否適合每個欄位(是否在正確的範圍、格式等),並且所有必填欄位都已提供值。
  • 清理刪除/替換資料中可能用於向伺服器傳送惡意內容的字元。

在本教程中,我們將使用流行的 express-validator 模組來對我們的表單資料執行驗證和清理。

安裝

透過在專案根目錄中執行以下命令來安裝模組。

bash
npm install express-validator

使用 express-validator

注意: GitHub 上的 express-validator 指南提供了 API 的良好概述。我們建議您閱讀該指南以瞭解其所有功能(包括使用 schema validation建立自定義驗證器)。下面我們僅介紹對 LocalLibrary 有用的一部分。

要在控制器中使用驗證器,我們需要從 express-validator 模組中匯入我們想要使用的特定函式,如下所示:

js
const { body, validationResult } = require("express-validator");

有許多可用的函式,允許您檢查和清理來自請求引數、正文、標頭、cookie 等的資料,或一次性處理所有這些資料。在本教程中,我們將主要使用 bodyvalidationResult(如上所示為“必需”)。

這些函式定義如下:

  • body(fields, message):指定請求正文中的一組欄位(POST 引數)以進行驗證和/或清理,以及一個可選的錯誤訊息,如果測試失敗,則可以顯示該訊息。驗證和清理條件會鏈式地附加到 body() 方法。

    例如,下面這行程式碼首先定義我們要檢查“name”欄位,如果驗證失敗,將設定錯誤訊息“Empty name”。然後我們呼叫清理方法 trim() 來刪除字串開頭和結尾的空格,然後呼叫 isLength() 來檢查結果字串是否為空。最後,我們呼叫 escape() 來刪除可能在 JavaScript 跨站指令碼攻擊中使用的 HTML 字元。

    js
    [
      // …
      body("name", "Empty name").trim().isLength({ min: 1 }).escape(),
      // …
    ];
    

    此測試檢查年齡欄位是否為有效日期,並使用 optional() 指定 null 和空字串不會導致驗證失敗。

    js
    [
      // …
      body("age", "Invalid age")
        .optional({ values: "falsy" })
        .isISO8601()
        .toDate(),
      // …
    ];
    

    您還可以鏈式使用不同的驗證器,並新增在之前的驗證器為 false 時顯示的訊息。

    js
    [
      // …
      body("name")
        .trim()
        .isLength({ min: 1 })
        .withMessage("Name empty.")
        .isAlpha()
        .withMessage("Name must be alphabet letters."),
      // …
    ];
    
  • validationResult(req):執行驗證,以 validation 結果物件的形式提供錯誤。這在單獨的回撥中呼叫,如下所示:

    js
    async (req, res, next) => {
      // Extract the validation errors from a request.
      const errors = validationResult(req);
    
      if (!errors.isEmpty()) {
        // There are errors. Render form again with sanitized values/errors messages.
        // Error messages can be returned in an array using `errors.array()`.
      } else {
        // Data from form is valid.
      }
    };
    

    我們使用驗證結果的 isEmpty() 方法來檢查是否存在錯誤,並使用其 array() 方法來獲取錯誤訊息集。有關更多資訊,請參閱處理驗證部分

驗證和清理鏈是應該傳遞給 Express 路由處理程式的中介軟體(我們透過控制器間接執行此操作)。當中間件執行時,每個驗證器/清理器都按指定的順序執行。

當我們在下面實現 LocalLibrary 表單時,我們將介紹一些真實的例子。

表單設計

庫中的許多模型是相關/依賴的——例如,Book 需要一個 Author,並且可能還具有一個或多個 Genres。這就提出了一個問題,即我們應該如何處理使用者希望進行以下操作的情況:

  • 在相關物件尚不存在時建立物件(例如,作者物件尚未定義的書籍)。
  • 刪除仍被其他物件使用的物件(例如,刪除仍被 Book 使用的 Genre)。

對於本專案,我們將透過宣告一個表單只能執行以下操作來簡化實現:

  • 使用已存在的物件建立物件(因此使用者必須在嘗試建立任何 Book 物件之前建立任何必需的 AuthorGenre 例項)。
  • 如果一個物件沒有被其他物件引用,則刪除該物件(例如,在所有關聯的 BookInstance 物件被刪除之前,您將無法刪除 Book)。

注意:更靈活的實現可能允許您在建立新物件時建立依賴物件,並隨時刪除任何物件(例如,透過刪除依賴物件,或從資料庫中刪除對已刪除物件的引用)。

路由

為了實現我們的表單處理程式碼,我們將需要兩個具有相同 URL 模式的路由。第一個(GET)路由用於顯示一個新的空表單以建立物件。第二個(POST)路由用於驗證使用者輸入的資料,然後儲存資訊並重定向到詳細資訊頁面(如果資料有效)或重新顯示帶有錯誤的表單(如果資料無效)。

我們已經在 /routes/catalog.js 中為所有模型的建立頁面建立了路由(在之前的教程中)。例如,流派路由如下所示:

js
// GET request for creating a Genre. NOTE This must come before route that displays Genre (uses id).
router.get("/genre/create", genre_controller.genre_create_get);

// POST request for creating Genre.
router.post("/genre/create", genre_controller.genre_create_post);

Express 表單子文章

以下子文章將引導我們完成向示例應用程式新增所需表單的過程。您需要逐一閱讀並完成每個表單,然後才能進入下一個表單。

  1. 建立流派表單 — 定義一個頁面來建立 Genre 物件。
  2. 建立作者表單 — 定義一個頁面來建立 Author 物件。
  3. 建立書籍表單 — 定義一個頁面/表單來建立 Book 物件。
  4. 建立圖書副本表單 — 定義一個頁面/表單來建立 BookInstance 物件。
  5. 刪除作者表單 — 定義一個頁面來刪除 Author 物件。
  6. 更新圖書表單 — 定義一個頁面來更新 Book 物件。

挑戰自我

實現 BookBookInstanceGenre 模型的刪除頁面,並以與我們的 *Author 刪除*頁面相同的方式從關聯的詳細資訊頁面連結它們。這些頁面應遵循相同的設計方法:

  • 如果物件被其他物件引用,則應顯示這些其他物件以及一條註釋,說明在刪除列出的物件之前無法刪除此記錄。
  • 如果沒有其他物件引用該物件,則檢視應提示刪除它。如果使用者按下 **Delete** 按鈕,則應刪除該記錄。

一些小貼士

  • 刪除一個 Genre 就像刪除一個 Author,因為這兩個物件都是 Book 的依賴項(因此在這兩種情況下,只有當關聯的書籍被刪除時,您才能刪除該物件)。
  • 刪除 Book 也很類似,您需要先檢查沒有關聯的 BookInstances
  • 刪除 BookInstance 是所有操作中最簡單的,因為沒有依賴物件。在這種情況下,您只需找到相關的記錄並將其刪除。

實現 BookInstanceAuthorGenre 模型的更新頁面,並以與我們的 Book 更新頁面相同的方式從關聯的詳細資訊頁面連結它們。

一些小貼士

  • 我們剛剛實現的 圖書更新頁面 是最難的!相同的模式可用於其他物件的更新頁面。
  • Author 的死亡日期和出生日期欄位以及 BookInstance 的到期日期欄位的格式不適合在表單的日期輸入欄位中輸入(它需要“YYYY-MM-DD”格式的資料)。解決此問題的最簡單方法是為日期定義一個新的虛擬屬性,該屬性以適當的格式格式化日期,然後在關聯的檢視模板中使用此欄位。
  • 如果您遇到困難,可以在此處的示例中找到更新頁面的示例。

總結

Express、Node 和 npm 上的第三方包提供了您將表單新增到網站所需的一切。在本文中,您學習瞭如何使用 Pug 建立表單,使用 express-validator 驗證和清理輸入,以及在資料庫中新增、刪除和修改記錄。

現在您應該瞭解如何在自己的 Node 網站中新增基本表單和表單處理程式碼了!

另見