Express 教程第 6 部分:使用表单
在本教程中,我们将向您展示如何使用 Pug 在 Express 中处理 HTML 表单。特别是,我们将讨论如何编写表单来从网站数据库中创建、更新和删除文档。
预备知识 | 完成所有之前的教程主题,包括Express 教程第 5 部分:显示图书馆数据 |
---|---|
目标 | 了解如何编写表单以从用户获取数据,并使用此数据更新数据库。 |
概述
HTML 表单是网页上一个或多个字段/小部件的集合,可用于从用户那里收集信息以提交到服务器。表单是收集用户输入的灵活机制,因为有适合输入多种不同类型数据(文本框、复选框、单选按钮、日期选择器等)的表单输入。表单也是一种与服务器共享数据的相对安全的方式,因为它们允许我们使用跨站请求伪造保护在 POST
请求中发送数据。
使用表单可能很复杂!开发人员需要编写表单的 HTML,在服务器(可能也在浏览器)上验证和正确清理输入的数据,用错误消息重新发布表单以通知用户任何无效字段,在成功提交数据后处理数据,最后以某种方式响应用户以指示成功。
在本教程中,我们将向您展示如何在 Express 中执行上述操作。在此过程中,我们将扩展 LocalLibrary 网站,允许用户从图书馆创建、编辑和删除项目。
注意:我们尚未研究如何限制特定路由给经过身份验证或授权的用户,因此此时,任何用户都将能够对数据库进行更改。
HTML 表单
首先简要概述HTML 表单。考虑一个简单的 HTML 表单,其中包含一个用于输入某个“团队”名称的文本字段及其关联的标签。
表单在 HTML 中定义为 <form>…</form>
标签内的一组元素,其中至少包含一个 type="submit"
的 input
元素。
<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
属性定义将显示哪种小部件。字段的 name
和 id
用于在 JavaScript/CSS/HTML 中标识该字段,而 value
定义了该字段首次显示时的初始值。匹配的团队标签使用 label
标签指定(参见上面的“输入名称”),其中 for
字段包含关联 input
的 id
值。
submit
输入将显示为一个按钮(默认情况下)——用户可以按下此按钮将其他输入元素(在本例中,只是 team_name
)包含的数据上传到服务器。表单属性定义用于发送数据的 HTTP method
和服务器上数据的目的地(action
)。
action
:当表单提交时,数据将发送到此资源/URL 进行处理。如果未设置(或设置为空字符串),则表单将提交回当前页面 URL。method
:用于发送数据的 HTTP 方法:POST
或GET
。- 如果数据将导致服务器数据库发生更改,则应始终使用
POST
方法,因为这样可以更好地抵抗跨站伪造请求攻击。 GET
方法只应用于不更改用户数据的表单(例如,搜索表单)。建议在您希望能够书签或共享 URL 时使用它。
- 如果数据将导致服务器数据库发生更改,则应始终使用
表单处理过程
表单处理使用了我们学习显示模型信息的所有相同技术:路由将我们的请求发送到控制器函数,该函数执行所有必需的数据库操作,包括从模型中读取数据,然后生成并返回 HTML 页面。使事情更复杂的是,服务器还需要能够处理用户提供的数据,并在出现任何问题时重新显示带有错误信息的表单。
下面显示了处理表单请求的过程流程图,从请求包含表单的页面(显示为绿色)开始。
如上图所示,表单处理代码主要需要做的事情是:
-
用户首次请求时显示默认表单。
- 表单可能包含空白字段(例如,如果您正在创建新记录),或者可能预填充了初始值(例如,如果您正在更改记录,或者具有有用的默认初始值)。
-
接收用户提交的数据,通常通过 HTTP
POST
请求。 -
验证并清理数据。
-
如果任何数据无效,重新显示表单——这次显示用户填写的值和问题字段的错误消息。
-
如果所有数据都有效,执行所需操作(例如,将数据保存到数据库中,发送通知电子邮件,返回搜索结果,上传文件等)。
-
所有操作完成后,将用户重定向到另一个页面。
通常,表单处理代码使用 GET
路由用于表单的初始显示,以及指向相同路径的 POST
路由用于处理表单数据的验证和处理。这就是本教程将使用的方法。
Express 本身不提供任何特定的表单处理操作支持,但它可以使用中间件来处理表单中的 POST
和 GET
参数,并验证/清理它们的值。
验证与清理
在存储表单数据之前,必须对其进行验证和清理。
- 验证检查输入的值是否适合每个字段(是否在正确的范围、格式等),并且所有必填字段都已提供值。
- 清理删除/替换数据中可能用于向服务器发送恶意内容的字符。
在本教程中,我们将使用流行的 express-validator 模块来对我们的表单数据执行验证和清理。
安装
通过在项目根目录中运行以下命令来安装模块。
npm install express-validator
使用 express-validator
注意: GitHub 上的 express-validator 指南提供了 API 的良好概述。我们建议您阅读该指南以了解其所有功能(包括使用 schema validation 和 创建自定义验证器)。下面我们仅介绍对 LocalLibrary 有用的一部分。
要在控制器中使用验证器,我们需要从 express-validator 模块中导入我们想要使用的特定函数,如下所示:
const { body, validationResult } = require("express-validator");
有许多可用的函数,允许您检查和清理来自请求参数、正文、标头、cookie 等的数据,或一次性处理所有这些数据。在本教程中,我们将主要使用 body
和 validationResult
(如上所示为“必需”)。
这些函数定义如下:
-
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
结果对象的形式提供错误。这在单独的回调中调用,如下所示:jsasync (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
对象之前创建任何必需的Author
和Genre
实例)。 - 如果一个对象没有被其他对象引用,则删除该对象(例如,在所有关联的
BookInstance
对象被删除之前,您将无法删除Book
)。
注意:更灵活的实现可能允许您在创建新对象时创建依赖对象,并随时删除任何对象(例如,通过删除依赖对象,或从数据库中删除对已删除对象的引用)。
路由
为了实现我们的表单处理代码,我们将需要两个具有相同 URL 模式的路由。第一个(GET
)路由用于显示一个新的空表单以创建对象。第二个(POST
)路由用于验证用户输入的数据,然后保存信息并重定向到详细信息页面(如果数据有效)或重新显示带有错误的表单(如果数据无效)。
我们已经在 /routes/catalog.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 表单子文章
以下子文章将引导我们完成向示例应用程序添加所需表单的过程。您需要逐一阅读并完成每个表单,然后才能进入下一个表单。
挑战自我
实现 Book
、BookInstance
和 Genre
模型的删除页面,并以与我们的 *Author 删除*页面相同的方式从关联的详细信息页面链接它们。这些页面应遵循相同的设计方法:
- 如果对象被其他对象引用,则应显示这些其他对象以及一条注释,说明在删除列出的对象之前无法删除此记录。
- 如果没有其他对象引用该对象,则视图应提示删除它。如果用户按下 **Delete** 按钮,则应删除该记录。
一些小贴士
- 删除一个
Genre
就像删除一个Author
,因为这两个对象都是Book
的依赖项(因此在这两种情况下,只有当关联的书籍被删除时,您才能删除该对象)。 - 删除
Book
也很类似,您需要先检查没有关联的BookInstances
。 - 删除
BookInstance
是所有操作中最简单的,因为没有依赖对象。在这种情况下,您只需找到相关的记录并将其删除。
实现 BookInstance
、Author
和 Genre
模型的更新页面,并以与我们的 Book 更新页面相同的方式从关联的详细信息页面链接它们。
一些小贴士
- 我们刚刚实现的 图书更新页面 是最难的!相同的模式可用于其他对象的更新页面。
Author
的死亡日期和出生日期字段以及BookInstance
的到期日期字段的格式不适合在表单的日期输入字段中输入(它需要“YYYY-MM-DD”格式的数据)。解决此问题的最简单方法是为日期定义一个新的虚拟属性,该属性以适当的格式格式化日期,然后在关联的视图模板中使用此字段。- 如果您遇到困难,可以在此处的示例中找到更新页面的示例。
总结
Express、Node 和 npm 上的第三方包提供了您将表单添加到网站所需的一切。在本文中,您学习了如何使用 Pug 创建表单,使用 express-validator 验证和清理输入,以及在数据库中添加、删除和修改记录。
现在您应该了解如何在自己的 Node 网站中添加基本表单和表单处理代码了!
另见
- express-validator (npm 文档)。