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 的良好概述。我們建議您閱讀它以瞭解其所有功能(包括使用模式驗證建立自定義驗證器)。下面我們只介紹了對LocalLibrary有用的一個子集。

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

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

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

這些函式定義如下

  • body(fields, message):指定請求正文(POST引數)中的一組欄位來驗證和/或清理,以及可選的錯誤訊息,如果驗證失敗,可以顯示該訊息。驗證和清理條件串聯到body()方法。例如,下面的行首先定義我們正在檢查“name”欄位,並且驗證錯誤將設定錯誤訊息“空名稱”。然後我們呼叫清理方法trim()來刪除字串開頭和結尾的空白字元,然後呼叫isLength()來檢查結果字串是否為空。最後,我們呼叫escape()來從可能用於 JavaScript 跨站點指令碼攻擊的變數中刪除 HTML 字元。
    js
    [
      // …
      body("name", "Empty name").trim().isLength({ min: 1 }).escape(),
      // …
    ];
    
    此測試檢查年齡欄位是否為有效日期,並使用optional()指定空值和空字串不會導致驗證失敗。
    js
    [
      // …
      body("age", "Invalid age")
        .optional({ values: "falsy" })
        .isISO8601()
        .toDate(),
      // …
    ];
    
    您還可以串聯不同的驗證器,並新增在前面的驗證器為假時顯示的訊息。
    js
    [
      // …
      body("name")
        .trim()
        .isLength({ min: 1 })
        .withMessage("Name empty.")
        .isAlpha()
        .withMessage("Name must be alphabet letters."),
      // …
    ];
    
  • validationResult(req):執行驗證,以validation結果物件的形式提供錯誤。這將在單獨的回撥中呼叫,如下所示
    js
    asyncHandler(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例項)。
  • 如果物件未被其他物件引用,則刪除該物件(因此,例如,您無法刪除Book,除非所有關聯的BookInstance物件都被刪除)。

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

路由

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

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

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 表單——定義一個頁面來建立Genre物件。
  2. 建立作者表單 — 定義一個頁面來建立Author物件。
  3. 建立書籍表單 — 定義一個頁面/表單來建立Book物件。
  4. 建立書籍例項表單 — 定義一個頁面/表單來建立BookInstance物件。
  5. 刪除作者表單 — 定義一個頁面來刪除Author物件。
  6. 更新書籍表單 — 定義一個頁面來更新Book物件。

挑戰自己

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

  • 如果其他物件引用了該物件,則應顯示這些其他物件,並附帶一條說明,指出只有在刪除列出的物件後才能刪除該記錄。
  • 如果沒有其他物件引用該物件,則該檢視應提示刪除該物件。如果使用者按下刪除按鈕,則應刪除該記錄。

一些提示

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

實現BookInstanceAuthorGenre模型的更新頁面,並從關聯的詳細資訊頁面連結到這些頁面,與我們的書籍更新頁面相同。

一些提示

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

總結

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

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

另請參閱