创建类型表单

此子文章展示了我们如何定义页面来创建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://127.0.0.1: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部分的下一篇文章:创建作者表单