建立型別表單

此子文章展示了我們如何定義頁面來建立Genre物件(這是一個很好的起點,因為Genre只有一個欄位,即name,並且沒有依賴項)。與任何其他頁面一樣,我們需要設定路由、控制器和檢視。

匯入驗證和清理方法

為了在我們的控制器中使用express-validator,我們必須從'express-validator'模組中引入我們想要使用的函式。

開啟/controllers/genreController.js,並在檔案頂部新增以下行,位於任何路由處理函式之前。

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

注意:這種語法允許我們使用bodyvalidationResult作為關聯的中件件函式,如下面的post路由部分所示。它等價於

js
const validator = require("express-validator");
const body = validator.body;
const validationResult = validator.validationResult;

控制器 - 獲取路由

找到匯出的genre_create_get()控制器方法,並將其替換為以下程式碼。這將渲染genre_form.pug檢視,並傳遞一個標題變數。

js
// Display Genre create form on GET.
exports.genre_create_get = (req, res, next) => {
  res.render("genre_form", { title: "Create Genre" });
};

請注意,這將替換我們在Express教程第4部分:路由和控制器中新增的佔位符非同步處理程式,替換為“普通”的Express路由處理程式函式。我們不需要為此路由使用asyncHandler()包裝器,因為它不包含任何可能丟擲異常的程式碼。

控制器 - 釋出路由

找到匯出的genre_create_post()控制器方法,並將其替換為以下程式碼。

js
// Handle Genre create on POST.
exports.genre_create_post = [
  // Validate and sanitize the name field.
  body("name", "Genre name must contain at least 3 characters")
    .trim()
    .isLength({ min: 3 })
    .escape(),

  // Process request after validation and sanitization.
  asyncHandler(async (req, res, next) => {
    // Extract the validation errors from a request.
    const errors = validationResult(req);

    // Create a genre object with escaped and trimmed data.
    const genre = new Genre({ name: req.body.name });

    if (!errors.isEmpty()) {
      // There are errors. Render the form again with sanitized values/error messages.
      res.render("genre_form", {
        title: "Create Genre",
        genre: genre,
        errors: errors.array(),
      });
      return;
    } else {
      // Data from form is valid.
      // Check if Genre with same name already exists.
      const genreExists = await Genre.findOne({ name: req.body.name })
        .collation({ locale: "en", strength: 2 })
        .exec();
      if (genreExists) {
        // Genre exists, redirect to its detail page.
        res.redirect(genreExists.url);
      } else {
        await genre.save();
        // New genre saved. Redirect to genre detail page.
        res.redirect(genre.url);
      }
    }
  }),
];

首先要注意的是,控制器指定了一箇中件件函式的陣列,而不是單箇中件件函式(帶引數(req, res, next))。該陣列傳遞給路由器函式,並且每個方法按順序呼叫。

注意:需要這種方法,因為驗證器是中件件函式。

陣列中的第一個方法定義了一個主體驗證器(body()),它驗證並清理欄位。它使用trim()刪除任何尾隨/前導空格,檢查name欄位是否為空,然後使用escape()刪除任何危險的HTML字元。

js
[
  // Validate that the name field is not empty.
  body("name", "Genre name must contain at least 3 characters")
    .trim()
    .isLength({ min: 3 })
    .escape(),
  // …
];

在指定驗證器之後,我們建立一箇中件件函式來提取任何驗證錯誤。我們使用isEmpty()檢查驗證結果中是否存在任何錯誤。如果存在,則我們再次渲染表單,傳入我們清理後的genre物件和錯誤訊息陣列(errors.array())。

js
// Process request after validation and sanitization.
asyncHandler(async (req, res, next) => {
  // Extract the validation errors from a request.
  const errors = validationResult(req);

  // Create a genre object with escaped and trimmed data.
  const genre = new Genre({ name: req.body.name });

  if (!errors.isEmpty()) {
    // There are errors. Render the form again with sanitized values/error messages.
    res.render("genre_form", {
      title: "Create Genre",
      genre: genre,
      errors: errors.array(),
    });
    return;
  } else {
    // Data from form is valid.
    // …
  }
});

如果genre名稱資料有效,則我們執行不區分大小寫的搜尋,以檢視是否存在名稱相同的Genre(因為我們不想建立僅在字母大小寫方面有所不同的重複或近似重複記錄,例如:“Fantasy”,“fantasy”,“FaNtAsY”等)。為了在搜尋時忽略字母大小寫和重音符號,我們連結collation()方法,指定'en'的區域設定和強度為2(有關更多資訊,請參閱MongoDB的排序規則主題)。

如果已存在名稱匹配的Genre,則我們重定向到其詳細資訊頁面。否則,我們儲存新的Genre並重定向到其詳細資訊頁面。請注意,這裡我們在資料庫查詢的結果上使用await,遵循與其他路由處理程式相同的模式。

js
// Check if Genre with same name already exists.
const genreExists = await Genre.findOne({ name: req.body.name })
  .collation({ locale: "en", strength: 2 })
  .exec();
if (genreExists) {
  // Genre exists, redirect to its detail page.
  res.redirect(genreExists.url);
} else {
  await genre.save();
  // New genre saved. Redirect to genre detail page.
  res.redirect(genre.url);
}

所有post控制器都使用相同的模式:我們執行驗證器(帶清理器),然後檢查錯誤,並重新渲染帶有錯誤資訊的表單或儲存資料。

檢視

當我們建立新的Genre時,在GETPOST控制器/路由中都會渲染相同的檢視(稍後在更新Genre時也會使用它)。在GET情況下,表單為空,我們只傳遞一個標題變數。在POST情況下,使用者之前輸入了無效資料——在genre變數中,我們傳回輸入資料的清理版本,在errors變數中,我們傳回錯誤訊息陣列。以下程式碼顯示了在兩種情況下渲染模板的控制器程式碼。

js
// Render the GET route
res.render("genre_form", { title: "Create Genre" });

// Render the POST route
res.render("genre_form", {
  title: "Create Genre",
  genre,
  errors: errors.array(),
});

建立/views/genre_form.pug並將下面的文字複製到其中。

pug
extends layout

block content

  h1 #{title}

  form(method='POST')
    div.form-group
      label(for='name') Genre:
      input#name.form-control(type='text', placeholder='Fantasy, Poetry etc.' name='name' required value=(undefined===genre ? '' : genre.name) )
    button.btn.btn-primary(type='submit') Submit

  if errors
    ul
      for error in errors
        li!= error.msg

此模板的很多內容在我們之前的教程中都很熟悉。首先,我們擴充套件了layout.pug基本模板並覆蓋了名為'content'的block。然後,我們有一個標題,其中包含我們從控制器(透過render()方法)傳入的title

接下來,我們有用於HTML表單的pug程式碼,該程式碼使用method="POST"將資料傳送到伺服器,並且由於action是一個空字串,因此會將資料傳送到與頁面相同的URL。

表單定義了一個名為“name”的型別為“text”的單個必填欄位。欄位的預設取決於genre變數是否已定義。如果從GET路由呼叫,則它將為空,因為這是一個新表單。如果從POST路由呼叫,則它將包含使用者最初輸入的(無效)值。

頁面的最後一部分是錯誤程式碼。如果已定義錯誤變數,則會列印錯誤列表(換句話說,當在GET路由上渲染模板時,此部分將不會出現)。

注意:這只是渲染錯誤的一種方法。您還可以從錯誤變數中獲取受影響欄位的名稱,並使用它們來控制錯誤訊息的渲染位置,是否應用自定義CSS等。

它是什麼樣子的?

執行應用程式,在瀏覽器中開啟https://:3000/,然後選擇建立新型別連結。如果一切設定正確,您的網站應該看起來像下面的螢幕截圖。輸入值後,它應該被儲存,並且您將被帶到型別詳細資訊頁面。

Genre Create Page - Express Local Library site

我們僅在伺服器端驗證的錯誤是型別欄位必須至少包含三個字元。下面的螢幕截圖顯示瞭如果提供僅包含一兩個字元的型別(以黃色突出顯示)時錯誤列表的外觀。

The Create Genre section of the Local library application. The left column has a vertical navigation bar. The right section is the create a new Genre from with a heading that reads 'Create Genre'. There is one input field labeled 'Genre'. There is a submit button at the bottom. There is an error message that reads 'Genre name required' directly below the Submit button. The error message was highlighted by the author of this article. There is no visual indication in the form that the genre is required nor that the error message only appears on error.

注意:我們的驗證使用trim()來確保不接受空格作為型別名稱。我們還在客戶端驗證欄位是否為空,方法是在表單中的欄位定義中新增布林屬性required

pug
input#name.form-control(type='text', placeholder='Fantasy, Poetry etc.' name='name' required value=(undefined===genre ? '' : genre.name) )

後續步驟

  1. 返回Express教程第6部分:使用表單。
  2. 繼續第6部分的下一篇文章:建立作者表單