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 网站中添加基本表单和表单处理代码了!

另见