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 的良好概述。我们建议您阅读它以了解其所有功能(包括使用模式验证和创建自定义验证器)。下面我们只介绍了对LocalLibrary有用的一个子集。
要在控制器中使用验证器,我们需要指定要从express-validator模块中导入的特定函数,如下所示
const { body, validationResult } = require("express-validator");
有许多函数可用,允许您检查和清理来自请求参数、正文、标头、cookie 等的数据,或者一次性检查所有数据。在本教程中,我们将主要使用body
和validationResult
(如上面“必需”所示)。
这些函数定义如下
-
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
结果对象的形式提供错误。这将在单独的回调中调用,如下所示我们使用验证结果的jsasyncHandler(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
对象之前创建任何所需的Author
和Genre
实例)。 - 如果对象未被其他对象引用,则删除该对象(因此,例如,您无法删除
Book
,除非所有关联的BookInstance
对象都被删除)。
注意:更灵活的实现可能允许您在创建新对象时创建依赖对象,并随时删除任何对象(例如,通过删除依赖对象,或从数据库中删除对已删除对象的引用)。
路由
为了实现我们的表单处理代码,我们需要两个具有相同 URL 模式的路由。第一个(GET
)路由用于显示用于创建对象的新的空表单。第二个路由(POST
)用于验证用户输入的数据,然后保存信息并重定向到详细信息页面(如果数据有效)或重新显示包含错误的表单(如果数据无效)。
我们已经在/routes/catalog.js中为所有模型的创建页面创建了路由(在之前的教程中)。例如,下面显示了 genre 路由
// 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
模型的删除页面,并从关联的详细信息页面链接到这些页面,与我们的作者删除页面相同。这些页面应遵循相同的设计方法。
- 如果其他对象引用了该对象,则应显示这些其他对象,并附带一条说明,指出只有在删除列出的对象后才能删除该记录。
- 如果没有其他对象引用该对象,则该视图应提示删除该对象。如果用户按下删除按钮,则应删除该记录。
一些提示
- 删除
Genre
就像删除Author
一样,因为这两个对象都是Book
的依赖项(因此在这两种情况下,只有在删除相关书籍后才能删除该对象)。 - 删除
Book
也类似,因为您需要首先检查是否存在相关的BookInstances
。 - 删除
BookInstance
是最简单的,因为它没有依赖对象。在这种情况下,您只需找到关联的记录并将其删除。
实现BookInstance
、Author
和Genre
模型的更新页面,并从关联的详细信息页面链接到这些页面,与我们的书籍更新页面相同。
一些提示
- 我们刚刚实现的书籍更新页面是最难的!相同的模式可以用于其他对象的更新页面。
Author
的死亡日期和出生日期字段以及BookInstance
的到期日期字段的格式不适合输入到表单上的日期输入字段中(它需要以“YYYY-MM-DD”格式的数据)。解决此问题的最简单方法是为日期定义一个新的虚拟属性,该属性以适当的格式格式化日期,然后在关联的视图模板中使用此字段。- 如果您遇到困难,可以在此处的示例中找到更新页面的示例。
总结
Express、node 和 npm 上的第三方包为您提供了将表单添加到网站所需的一切。在本文中,您学习了如何使用Pug创建表单、使用express-validator验证和清理输入以及添加、删除和修改数据库中的记录。
您现在应该了解如何在自己的 node 网站中添加基本表单和表单处理代码!
另请参阅
- express-validator (npm 文档)。