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

另请参阅