Express 教學 6: 使用表單

在此教程中,我們會教你如何使用 Express ,並且結合 Pug 來實現 HTML 表單,並且如何從數據庫中創建、更新、和刪除文檔。

前提條件: 完成前面所有的教程,包括 Express 教程第5章: 展示圖書館數據。
目標: 了解如何編寫表單獲取用戶信息,並且將這些數據更新到數據庫中。

概覽

HTML 表單是網頁中由一個、或多個字段/小工具形成的一個組合,它被用來收集用戶的信息,並將信息上傳到服務器。表單作為一種用來收集用戶的機制,非常的靈活,因為有各種合適的輸入框,來接受各種類型的數據——文本框,複選框,單選按鈕,時間選擇器等。表單和服務器交互數據也相對安全,因為它使用POST請求發送數據,保護不受跨站點請求偽造攻擊(cross-site request forgery)的威脅。

但是表單同樣也很複雜!開發者需要編寫給表單編寫 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 標籤,指定(請參閱上面的「輸入名稱」 "Enter name"),其中 for 字段,包含 input 相關輸入的 id值。

提交輸入(submit)將顯示為按鈕(默認情況下) - 用戶可以按此按鈕,將其他輸入元素包含的數據,上傳到服務器(在本例中,只有 team_name)。表單屬性,定義用於發送數據的 HTTP method方法,和服務器上數據的目標(action):

  • action: 提交表單時,要發送數據以進行處理的資源/ URL。如果未設置(或設置為空字符串),則表單將提交回當前頁面 URL。
  • method: 用於發送數據的 HTTP 方法:POSTGET
    • POST 方法。如果數據將導致服務器數據庫的更改,則始終應該使用 POST方法,因為這更加可以抵抗跨站點偽造請求攻擊。
    • GET 方法只應用於不更改用戶數據的表單(例如,搜索表單)。當你希望能夠為 URL 添加書籤或共享時,建議使用此選項。

表單處理流程

表單處理使用的技術,與我們學習過、用來顯示有關模型的信息的所有技術,是相同的:路由將我們的請求發送到控制器函數,該函數執行所需的任何數據庫操作,包括從模型中讀取數據,然後生成並返回 HTML 頁面。使事情變得更複雜的是,服務器還需要能夠處理用戶提供的數據,並在出現任何問題時,重新顯示帶有錯誤信息的表單。

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

如上圖所示,構成處理代碼所需要做的主要是:

  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/check'和'express-validator/filter'模塊中,導入我們想要使用的函數,如下所示:

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

有許多可用的功能,允許你一次檢查和清理請求參數,正文,標頭,cookie 等數據,或所有數據。對於本教程,我們主要使用 bodysanitizeBody,和 validationResult(如上面 required 導入的 )。

功能定義如下:

  • body(fields[, message]): 指定請求本文中的一組字段(POST參數)以及可選的錯誤消息,如果測試失敗,則可以顯示該字段。驗證標準以菊花鏈形式連接到 body()方法。例如,下面的第一個檢查測試,「name」字段不為空,如果不是,則設置錯誤消息「Empty name」。第二個測試,檢查 age 字段是否為有效日期,並使用optional()指定 null 和空字符串不會驗證失敗。
    js
    body('name', 'Empty name').isLength({ min: 1 }),
    body('age', 'Invalid age').optional({ checkFalsy: true }).isISO8601(),
    
    你還可以用菊花鍊式連接不同的驗證器,並添加前面驗證器為真時顯示的消息。
    js
    body('name').isLength({ min: 1 }).trim().withMessage('Name empty.')
        .isAlpha().withMessage('Name must be alphabet letters.'),
    

    備註: 你還可以添加內聯清理器,如trim(),如上所示。但是,此處應用清理器,僅適用於驗證步驟。如果要對最終輸出進行消毒,則需要使用單獨的清理器方法,如下所示。

  • sanitizeBody(fields): 指定一個正文要清理的字段。然後將清理操作,以菊花鏈形式連接到此方法。例如,下面的 escape()清理操作,會從名稱變量中,刪除可能在 JavaScript 跨站點腳本攻擊中使用的 HTML 字符。
    js
    sanitizeBody('name').trim().escape(),
    sanitizeBody('date').toDate(),
    
  • validationResult(req): 運行驗證,以 validation 驗證結果對象的形式,提供錯誤。這是在單獨的回調中調用的,如下所示:
    js
    (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() 方法,來獲取錯誤消息集合。有關更多信息,請參閱驗證結果的 Validation Result API

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

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

表單設計

圖書館中的許多模型都是相關/依賴的 - 例如,一本書需要一個作者,也可能有一個或多個種類。這提出了一個問題,即我們應該如何處理用戶希望的情況:

  • 在其相關對象尚不存在時,創建對象(例如,尚未定義作者對象的書)。
  • 刪除另一個對象仍在使用的對象(例如,刪除仍有書本 Book 使用的種類 Genre)。

在這個項目,我們為了簡化實作,將聲明表單只能:

  • 使用已存在的對象創建對象(因此用戶在嘗試創建任何 Book對象之前,必須創建任何所需的 AuthorGenre實例)。
  • 如果對象未被其他對象引用,則刪除該對象(例如,在刪除所有關聯的 BookInstance對象之前,你將無法刪除該書)。

備註: 更「牢固」的實現,可能允許你在創建新對象時,創建依賴對象,並隨時刪除任何對象(例如,通過刪除依賴對象,或從數據庫中,刪除對已刪除對象的引用) 。

路由

為了實現我們的表單處理代碼,我們需要兩個具有相同 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. 創建種類表單 (en-US) — 定義我們的頁面以創建種類對象 Genre
  2. 創建作者表單 (en-US) — 定義用於創建作者對象 Author 的頁面。
  3. 創建書本表單 (en-US) — 定義頁面/表單以創建書本對象 Book
  4. 創建書本實例表單 (en-US) — 定義頁面/表單以創建書本實例對象 BookInstance
  5. 刪除作者表單 (en-US) — 定義要刪除作者對象 Author的頁面。
  6. 更新書本表單 (en-US) — 定義頁面以更新書本對象 Book

挑戰自我

實現 Book, BookInstance, 和 Genre模型的刪除頁面,用跟我們的作者刪除頁面相同的方式,將它們與關聯的詳細信息頁面,鏈接起來。頁面應遵循相同的設計方法:

  • 如果有來自其他對象的、對於對象的引用,則應顯示註釋,列出這些對象,並說明在刪除列出的對象之前,無法刪除此記錄。
  • 如果沒有對該對象的其他引用,則視圖應提示刪除它。如果用戶按下「刪除」按鈕 Delete,則應刪除該記錄。

一些提示:

  • 刪除種類 Genre就像刪除作者Author一樣,因為兩個對像都是 Book的依賴項(因此在這兩種情況下,只有在刪除相關書本時,才能刪除對象)。
  • 刪除書本 Book也很相似,但你需要檢查是否沒有關聯的書本實例 BookInstances
  • 刪除書本實例 BookInstances是最簡單的,因為沒有依賴對象。在這種情況下,你只需找到相關記錄並將其刪除即可。

實現 BookInstance, Author, 和 Genre模型的更新頁面,以與我們的書本更新頁面相同的方式,將它們與關聯的詳細信息頁面,鏈接起來。

一些提示:

  • 我們剛剛實施的圖書更新頁面是最難的!相同的模式可用於其他對象的更新頁面。
  • 作者Author的死亡日期和出生日期字段,以及書本實例BookInstance 的 due_date 字段,是輸入到表單上日期輸入字段的錯誤格式(它需要 「YYYY-MM-DD」 形式的數據)。解決此問題的最簡單方法,是為適當格式化的日期,定義新的虛擬屬性,然後在關聯的視圖模板中,使用此字段。
  • 如果你遇到困難,此處示例中的更新頁面有一些示例的連結。

總結

Express, node, 與 NPM 上面的第三方套件,提供你需要的每樣東西 ,可用於新增表單到你的網站上。在本文中,你學習如何使用 Pug 創建表單,使用 express-validator 驗證和清理輸入,以及添加,刪除和修改數據庫中的記錄。

你現在應該了解如何新增基本表單,以及表單處理代碼到你的 node 網站!

請參閱