diff --git a/files/zh-cn/learn/server-side/express_nodejs/forms/create_author_form/index.md b/files/zh-cn/learn/server-side/express_nodejs/forms/create_author_form/index.md index 4820a3bf71782b..625da9701d80a3 100644 --- a/files/zh-cn/learn/server-side/express_nodejs/forms/create_author_form/index.md +++ b/files/zh-cn/learn/server-side/express_nodejs/forms/create_author_form/index.md @@ -3,168 +3,178 @@ title: 创建作者表单 slug: Learn/Server-side/Express_Nodejs/forms/Create_author_form --- -本章节演示,如何为创建作者对象`Author`定义一个页面。 +{{LearnSidebar}} -## 导入验证和清理方法 +本章节将演示如何定义一个用于创建 `Author` 对象的页面。 -为了在种类表单使用 express 验证器,我们必须用 require 导入我们想用的函式。 +## 导入验证和修整方法 -打开 **/controllers/authorController.js**,并在档案最上方加入底下几行: +与[创建种类表单](/zh-CN/docs/Learn/Server-side/Express_Nodejs/forms/Create_genre_form) 一样,要使用 _express-validator_,我们必须 _require_ 我们想要使用的函数。 + +打开 **/controllers/authorController.js**,并将以下代码添加到文件顶部(路由函数上方): ```js -const { body, validationResult } = require("express-validator/check"); -const { sanitizeBody } = require("express-validator/filter"); +const { body, validationResult } = require("express-validator"); ``` -## 控制器—get 路由 +## 控制器——get 路由 -找到导出的 `author_create_get()`控制器方法,并替换为底下代码。这里单纯呈现 **author_form.pug** 视图,传送 `title` 变数。 +找到导出的 `author_create_get()` 控制器方法并用以下代码替换。此方法会将 `title` 变量传入 `author_form.pug` 视图并渲染。 ```js -// Display Author create form on GET. -exports.author_create_get = function (req, res, next) { +// 展示 GET 方法获取的创建作者表单 +exports.author_create_get = (req, res, next) => { res.render("author_form", { title: "Create Author" }); }; ``` -## 控制器—post 路由 +## 控制器——post 路由 -找到导出的 `author_create_post()` 控制器方法,并替换为底下代码。 +找到导出的 `author_create_post()` 控制器方法,并将其替换为以下代码。 ```js -// Handle Author create on POST. +// 处理 POST 方法提交的创建作者表单 exports.author_create_post = [ - // Validate fields. + // 验证并且修整字段 body("first_name") - .isLength({ min: 1 }) .trim() + .isLength({ min: 1 }) + .escape() .withMessage("First name must be specified.") .isAlphanumeric() .withMessage("First name has non-alphanumeric characters."), body("family_name") - .isLength({ min: 1 }) .trim() + .isLength({ min: 1 }) + .escape() .withMessage("Family name must be specified.") .isAlphanumeric() .withMessage("Family name has non-alphanumeric characters."), body("date_of_birth", "Invalid date of birth") - .optional({ checkFalsy: true }) - .isISO8601(), + .optional({ values: "falsy" }) + .isISO8601() + .toDate(), body("date_of_death", "Invalid date of death") - .optional({ checkFalsy: true }) - .isISO8601(), - - // Sanitize fields. - sanitizeBody("first_name").trim().escape(), - sanitizeBody("family_name").trim().escape(), - sanitizeBody("date_of_birth").toDate(), - sanitizeBody("date_of_death").toDate(), - - // Process request after validation and sanitization. - (req, res, next) => { - // Extract the validation errors from a request. + .optional({ values: "falsy" }) + .isISO8601() + .toDate(), + + // 在验证和修整完字段后处理请求 + asyncHandler(async (req, res, next) => { + // 从请求中提取验证错误 const errors = validationResult(req); + // 使用经过 trim() 和 escape() 处理过的数据创建作者对象 + const author = new Author({ + first_name: req.body.first_name, + family_name: req.body.family_name, + date_of_birth: req.body.date_of_birth, + date_of_death: req.body.date_of_death, + }); + if (!errors.isEmpty()) { - // There are errors. Render form again with sanitized values/errors messages. + // 出现了错误,那么就用修整过的值和错误信息再次渲染一遍表单 res.render("author_form", { title: "Create Author", - author: req.body, + author: author, errors: errors.array(), }); return; } else { - // Data from form is valid. - - // Create an Author object with escaped and trimmed data. - var author = new Author({ - first_name: req.body.first_name, - family_name: req.body.family_name, - date_of_birth: req.body.date_of_birth, - date_of_death: req.body.date_of_death, - }); - author.save(function (err) { - if (err) { - return next(err); - } - // Successful - redirect to new author record. - res.redirect(author.url); - }); + // 表格中的数据有效 + + // 保存作者信息 + await author.save(); + // Redirect to new author record. + // 重定向到新的作者记录 + res.redirect(author.url); } - }, + }), ]; ``` -此代码的结构和行为,几乎与创建`Genre`对象完全相同。首先,我们验证并清理数据。如果数据无效,那么我们将重新显示表单,以及用户最初输入的数据,和错误消息列表。如果数据有效,那么我们保存新的作者记录,并将用户重定向到作者详细信息页面。 +> [!WARNING] +> 切勿使用 `isAlphanumeric()` 来验证 _name_(正如上面代码所写的那样),因为有许多名字会使用其他字符集。我们在这里这样做是为了演示如何使用验证器,以及如何将其与其他验证器和错误报告进行链式调用。 -> [!NOTE] -> 与`Genre` post 处理程序不同,我们不会在保存之前,检查`Author`对象是否已存在。可以说,我们应该这样做,尽管现在我们可以有多个具有相同名称的作者。 +此代码的结构和行为几乎与创建 `Genre` 对象一致。首先,我们验证并修整数据。如果数据无效,我们将重新显示表单以及用户最初输入的数据和错误消息列表。如果数据有效,那么我们将保存新的作者记录并将用户重定向到作者详情页面。 + +与 `Genre` 的 post 处理程序不同,我们不会在保存 `Author` 对象之前检查其是否已经存在。从某种程度上说我们应该这样做,但目前我们可能会有多个同名作者。 验证代码演示了几个新功能: -- 我们可以用菊花链式连接验证器,使用`withMessage()`指定在前一个验证方法失败时,显示的错误消息。这使得在没有大量代码重复的情况下,提供特定的错误消息变得非常容易。 +- 我们可以链式调用验证器,使用 `withMessage()` 指定在先前的验证方法失败时需要显示的错误消息。这使得在没有大量代码重复的情况下,提供特定的错误消息变得非常容易。 ```js - // Validate fields. - body('first_name').isLength({ min: 1 }).trim().withMessage('First name must be specified.') - .isAlphanumeric().withMessage('First name has non-alphanumeric characters.'), + [ + // 验证并修整字段 + body("first_name") + .trim() + .isLength({ min: 1 }) + .escape() + .withMessage("First name must be specified.") + .isAlphanumeric() + .withMessage("First name has non-alphanumeric characters."), + // … + ]; ``` -- 我们可以使用`optional()`函数,仅在输入字段时运行后续验证(这允许我们验证可选字段)。例如,下面我们检查可选的出生日期是否符合 ISO8601 标准(`checkFalsy` 旗标,表示我们接受空字符串或`null`作为空值)。 +- 我们可以使用 `optional()` 函数来保证仅当有字段输入时才去运行后续的验证(这允许我们验证可选字段)。例如,下面我们检查可选的出生日期是否符合 ISO8601 标准(传递的 `{ values: "falsy" }` 对象意味着我们将接受空字符串或 `null` 作为空值)。 ```js - body('date_of_birth', 'Invalid date of birth').optional({ checkFalsy: true }).isISO8601(), + [ + body("date_of_birth", "Invalid date of birth") + .optional({ values: "falsy" }) + .isISO8601() + .toDate(), + ]; ``` -- 参数从请求中作为字符串接收。我们可以使用`toDate()`(或`toBoolean()`等)将这些转换为正确的 JavaScript 类型。 - - ```js - sanitizeBody('date_of_birth').toDate(), - ``` +- 参数以字符串形式从请求中接收。我们可以使用 `toDate()`(或 `toBoolean()`)将它们转换为正确的 JavaScript 类型(如上方验证器链末尾所示)。 ## 视图 -创建 **/views/author_form.pug** 并复制贴上以下文字。 +创建 **/views/author_form.pug** 并复制下方文本。 -```plain +```pug extends layout block content h1=title - form(method='POST' action='') + form(method='POST') div.form-group label(for='first_name') First Name: - input#first_name.form-control(type='text' placeholder='First name (Christian) last' name='first_name' required='true' value=(undefined===author ? '' : author.first_name) ) + input#first_name.form-control(type='text', placeholder='First name (Christian)' name='first_name' required value=(undefined===author ? '' : author.first_name) ) label(for='family_name') Family Name: - input#family_name.form-control(type='text' placeholder='Family name (surname)' name='family_name' required='true' value=(undefined===author ? '' : author.family_name)) + input#family_name.form-control(type='text', placeholder='Family name (Surname)' name='family_name' required value=(undefined===author ? '' : author.family_name)) div.form-group label(for='date_of_birth') Date of birth: input#date_of_birth.form-control(type='date' name='date_of_birth' value=(undefined===author ? '' : author.date_of_birth) ) button.btn.btn-primary(type='submit') Submit + if errors ul for error in errors li!= error.msg ``` -此视图的结构和行为与**genre_form.pug**模板完全相同,因此我们不再对其进行描述。 +该视图的结构和行为与 `genre_form.pug` 模板完全相同,因此我们不会再次描述它。 > [!NOTE] -> 某些浏览器不支持 input `type=“date”`,因此你不会获得日期选取部件或默认的*`dd/mm/yyyy`*占位符,而是获取一个空的纯文本字段。一种解决方法,是明确添加属性`placeholder='dd/mm/yyyy'`,以便在功能较少的浏览器上,仍然可以获得有关所需文本格式的信息。 +> 某些浏览器不支持 input 标签的 `type=“date”` 属性,因此你不会获得日期选择器小组件或默认的 `dd/mm/yyyy` 占位符,而是获取一个空的纯文本字段。一种解决方法是显式添加属性 `placeholder='dd/mm/yyyy'`,以便在功能较差的浏览器上仍然可以获得有关所需文本格式的信息。 -### 自我挑战:加入死亡日期 +### 自我挑战:添加死亡日期 -上面的模板少了一个输入字段 `date_of_death` 。依照跟生日表单同样的模式,创建此字段! +上面的模板缺少用于输入死亡日期 `date_of_death` 的字段。按照与出生日期表单组相同的模式创建字段! ## 它看起來像是? -运行本应用,打开浏览器访问网址,然后点击创建新作者 Create new author 连结。如果每个东西都设定正确了,你的网站看起应该像底下的截图。在你输入一个值之后,它应该会被储存,并且你将被带到作者详细信息页面。 +运行本应用,打开浏览器访问网址 `http://localhost:3000/`,然后点击 `Create new author` 链接。如果一切设置正确,你的网站应类似于下方截图。在你输入一个值后,它应该会被保存,并且进入作者详情页面。 -![Author Create Page - Express Local Library site](locallibary_express_author_create_empty.png) +![作者创建页面——Express 本地图书馆网站](locallibary_express_author_create_empty.png) > [!NOTE] -> 如果你尝试使用日期的各种输入格式,你可能会发现格式`yyyy-mm-dd`行为不正常。这是因为 JavaScript 将日期字符串,视为包含 0 小时的时间,但另外将该格式的日期字符串(ISO 8601 标准)视为包括 0 小时 UTC 时间,而不是本地时间。如果你的时区在 UTC 以西,则日期显示(即本地)将在你输入的日期之前一天。这是我们在这里没有解决的几个复杂问题之一(例如多字姓和有多个作者的书本)。 +> 如果你尝试使用各种日期输入格式,你可能会发现格式 `yyyy-mm-dd` 行为不恰当。这是因为 JavaScript 中的日期字符串包含了 0 小时这个时间,而且还将该格式的日期字符串(ISO 8601 标准)视为包括 0 小时的 UTC 时间,而不是本地时间。如果你的时区在 UTC 以西,则本地日期显示将会是你输入的日期的前一天。这是我们在此没有解决的几个复杂问题(例如多字姓氏和多作者书籍)之一。 ## 下一步 diff --git a/files/zh-cn/learn/server-side/express_nodejs/forms/create_genre_form/index.md b/files/zh-cn/learn/server-side/express_nodejs/forms/create_genre_form/index.md index 8d6fbfee733623..489c416d1ba347 100644 --- a/files/zh-cn/learn/server-side/express_nodejs/forms/create_genre_form/index.md +++ b/files/zh-cn/learn/server-side/express_nodejs/forms/create_genre_form/index.md @@ -3,53 +3,65 @@ title: 创建种类表单 slug: Learn/Server-side/Express_Nodejs/forms/Create_genre_form --- -本章节演示如何定义我们的页面,创建`Genre` 物件(这是一个很好的起点,因为类型只有一个字段,它的名称`name`,没有依赖项)。像任何其他页面一样,我们需要设置路由,控制器和视图。 +{{LearnSidebar}} -## 引入验证与无害化方法 +本章节演示如何定义页面来创建 `Genre` 对象(这是一个很好的起点,因为 `Genre` 只有一个字段,即它的名称 `name`,并且没有依赖项)。与任何其他页面一样,我们需要设置路由,控制器和视图。 -在我们的控制器中使用 _express-validator_ 验证器,我們必須导入我们想要从 **'express-validator/check**' 和 **'express-validator/filter**' 模块中使用的函数。 +## 导入验证与修整方法 -打开**/controllers/genreController.js**,并在文件顶部添加以下行: +要控制器中使用 _express-validator_,我們必須从 `'express-validator'` 模块中 _require_ 我们想使用的函数。 + +打开 **/controllers/genreController.js**,在文件顶部、路由处理函数之前添加下方代码: ```js -const { body, validationResult } = require("express-validator/check"); -const { sanitizeBody } = require("express-validator/filter"); +const { body, validationResult } = require("express-validator"); ``` -## 控制器—get 路由 +> [!NOTE] +> 此语法允许我们使用 `body` 和 `validationResult` 作为关联的中间件函数,正如你将在下面的 post 路由部分中看到的那样。它相当于: +> +> ```js +> const validator = require("express-validator"); +> const body = validator.body; +> const validationResult = validator.validationResult; +> ``` + +## 控制器——get 路由 -找到导出的`genre_create_get()` 控制器方法,并将其替换为以下代码。这只是渲染**genre_form.pug**视图,传递一个 title 变量。 +找到导出的 `genre_create_get()` 控制器方法,并将其替换为以下代码。这将渲染 **genre_form.pug** 视图,传递一个标题变量。 ```js -// Display Genre create form on GET. -exports.genre_create_get = function (req, res, next) { +// 呈现 GET 方法获取的种类表格 +exports.genre_create_get = (req, res, next) => { res.render("genre_form", { title: "Create Genre" }); }; ``` -## 控制器—post 路由 +请注意,这里我们将使用一个“普通”的函数替换我们在 [Express 教程 4:路由和控制器](/zh-CN/docs/Learn/Server-side/Express_Nodejs/routes) 中添加的占位 asynchronous handler 函数。我们不需要该路由的 `asyncHandler()` 函数的包装,因为它不包含任何可能引发异常的代码。 + +## 控制器——post 路由 -找到导出的`genre_create_post()`控制器方法,并将其替换为以下代码。 +找到导出的 `genre_create_post()` 控制器方法,并将其替换为以下代码。 ```js -// Handle Genre create on POST. +// 处理 POST 方法创建的 Genre 表单 exports.genre_create_post = [ - // Validate that the name field is not empty. - body("name", "Genre name required").isLength({ min: 1 }).trim(), - - // Sanitize (trim and escape) the name field. - sanitizeBody("name").trim().escape(), - - // Process request after validation and sanitization. - (req, res, next) => { - // Extract the validation errors from a request. + // 验证及修整名字字段 + body("name", "Genre name must contain at least 3 characters") + .trim() + .isLength({ min: 3 }) + .escape(), + + // 处理验证及修整过后的请求 + asyncHandler(async (req, res, next) => { + // 从请求中提取验证时产生的错误信息 const errors = validationResult(req); - // Create a genre object with escaped and trimmed data. - var genre = new Genre({ name: req.body.name }); + // 使用经过 trim() 和 escape() 处理过的数据创建一个种类对象 + 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, @@ -57,131 +69,118 @@ exports.genre_create_post = [ }); return; } else { - // Data from form is valid. - // Check if Genre with same name already exists. - Genre.findOne({ name: req.body.name }).exec(function (err, found_genre) { - if (err) { - return next(err); - } - - if (found_genre) { - // Genre exists, redirect to its detail page. - res.redirect(found_genre.url); - } else { - genre.save(function (err) { - if (err) { - return next(err); - } - // Genre saved. Redirect to genre detail page. - res.redirect(genre.url); - }); - } - }); + // 表格中的数据有效 + // 检查是否存在同名的 Genre + const genreExists = await Genre.findOne({ name: req.body.name }) + .collation({ locale: "en", strength: 2 }) + .exec(); + if (genreExists) { + // 存在同名的 Genre,则重定向到详情页面 + res.redirect(genreExists.url); + } else { + await genre.save(); + // 保存新创建的 Genre,然后重定向到详情页面 + res.redirect(genre.url); + } } - }, + }), ]; ``` -首先要注意的是,控制器不是单个中间件函数(带参数(`req, res, next`)),而是指定一组中间件函数。数组传递给路由器函数,并按顺序调用每个方法。 +首先需要注意的是,控制器不是单个中间件函数(带参数`(req, res, next)`),而是指定了中间件函数*数组*。该数组传递给路由器函数并依次执行各个方法。 > [!NOTE] -> 这种方法是必需的,因为消毒/验证器是中间件功能。 - -数组中的第一个方法定义了一个验证器(`body`),来检查 name 字段是否为空(在执行验证之前调用`trim()`,以删除任何尾随/前导空格)。 +> 这种方法是必要的,因为验证器是中间件函数。 -数组中的第二个方法(`sanitizeBody()`),创建一个清理程序来调用`trim()`修剪名称字段和调用`escape()`转义任何危险的 HTML 字符。 +数组中的第一个方法定义了一个 body 验证器(`body()`),用于验证和修整字段。这个方法使用 `trim()` 删除所有的首部/尾部空白,检查 _name_ 字段是否为空,然后使用 `escape()` 删除任何危险的 HTML 字符。 ```js -// Validate that the name field is not empty. -body('name', 'Genre name required').isLength({ min: 1 }).trim(), - -// Sanitize (trim and escape) the name field. -sanitizeBody('name').trim().escape(), +[ + // 检验 name 字段不为空 + body("name", "Genre name must contain at least 3 characters") + .trim() + .isLength({ min: 3 }) + .escape(), + // … +]; ``` -> [!NOTE] -> 验证期间运行的清洁器不会修改请求。这就是为什么我们必须在上面的两个步骤中调用`trim()`! - -在指定验证器和清理器之后,我们创建了一个中间件函数,来提取任何验证错误。我们使用`isEmpty()` 来检查验证结果中,是否有任何错误。如果有,那么我们再次渲染表单,传入我们的已清理种类对象和错误消息的数组(`errors.array()`)。 +指定验证器后,我们创建一个中间件函数来提取任何验证错误。我们使用 `isEmpty()` 来检查验证结果是否有错误。如果有,我们就再次渲染表单,传入经过修整的种类对象和错误消息数组(`errors.array()`)。 ```js -// Process request after validation and sanitization. -(req, res, next) => { - - // Extract the validation errors from a request. - const errors = validationResult(req); - - // Create a genre object with escaped and trimmed data. - var 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()}); +// 处理验证和修整之后的请求 +asyncHandler(async (req, res, next) => { + // 从请求中提取验证错误 + const errors = validationResult(req); + + // 使用经过 trim() 和 escape() 处理过的数据创建一个种类对象 + const genre = new Genre({ name: req.body.name }); + + if (!errors.isEmpty()) { + // 出现错误,并使用修整过的数据/错误信息重新渲染表单 + res.render("genre_form", { + title: "Create Genre", + genre: genre, + errors: errors.array(), + }); return; - } - else { - // Data from form is valid. - ... ... - } -} + } else { + // 表格中的数据有效 + // … + } +}); ``` -如果种类名称数据有效,那么我们检查,是否已存在具有相同名称的种类`Genre`(因为我们不想创建重复项)。 +如果种类名称数据有效,那么我们执行不区分大小写的搜索,以查看是否存在具有相同名称的种类 `Genre`(因为我们不想创建仅字母大小写不同的重复或过于近似的记录,例如“Fantasy”,“fantasy”,“FaNtAsY”等等)。为了在搜索时忽略掉大小写和重音,我们链式调用了 [`collation()`]() 方法,指定“en”的区域设置和 2 的强度(更多信息请参阅 MongoDB 的 [Collation](https://www.mongodb.com/docs/manual/reference/collation/)主题)。 -如果是,我们会重定向到现有种类的详细信息页面。如果没有,我们保存新种类,并重定向到其详细信息页面。 +如果匹配的种类 `Genre` 已经存在,我们将重定向到其详情页面。如果不存在,我们则保存新种类并重定向到其详情页面。请注意,这里我们 `await` 数据库的查询结果,遵循与其他路由处理程序相同的模式。 ```js -// Check if Genre with same name already exists. -Genre.findOne({ name: req.body.name }).exec(function (err, found_genre) { - if (err) { - return next(err); - } - if (found_genre) { - // Genre exists, redirect to its detail page. - res.redirect(found_genre.url); - } else { - genre.save(function (err) { - if (err) { - return next(err); - } - // Genre saved. Redirect to genre detail page. - res.redirect(genre.url); - }); - } -}); +// 检查是否存在同名的 Genre +const genreExists = await Genre.findOne({ name: req.body.name }) + .collation({ locale: "en", strength: 2 }) + .exec(); +if (genreExists) { + // 存在同名的 Genre,则重定向到详情页面 + res.redirect(genreExists.url); +} else { + await genre.save(); + // 保存新创建的 Genre,然后重定向到详情页面 + res.redirect(genre.url); +} ``` -在我们所有的 `POST`控制器中,都使用了相同的模式:我们运行验证器,然后运行消毒器,然后检查错误,并使用错误信息重新呈现表单,或保存数据。 +我们所有的 post 控制器中都使用了相同的模式:运行验证器(带有修整功能),然后检查错误并重新渲染带有错误信息的表单或保存数据。 ## 视图 -当我们创建一个新的种类`Genre`时,在`GET`和`POST`控制器/路由中,都会呈现相同的视图(稍后在我们更新种类`Genre`时也会使用它)。 - -在`GET`情况下,表单为空,我们只传递一个 title 变量。在`POST`情况下,用户先前输入了无效数据 - 在种类变量`genre`中,我们传回了输入数据的已清理版本,并且在`errors`变量中,我们传回了一组错误消息。 +当我们创建新的种类 `Genre` 时,相同的视图会在 `GET` 和 `POST` 控制器/路由中呈现(稍后当我们*更新*种类时也会使用它),在 `GET` 情况下,表单为空,我们只传递一个标题变量。在 `POST` 情况下,用户之前输入了无效数据——对于 `genre` 变量,我们回传经过修整后的输入数据,对于错误变量,则回传一组错误消息。下面的代码显示了在两种情况下渲染模板的控制器代码。 ```js +// 渲染 GET 方法获取的视图 res.render("genre_form", { title: "Create Genre" }); + +// 渲染 POST 方法使用的视图 res.render("genre_form", { title: "Create Genre", - genre: genre, + genre, errors: errors.array(), }); ``` -创建 **/views/genre_form.pug**,并复制下面的文本。 +创建 **/views/genre_form.pug**,并复制下方的代码。 -```plain +```pug extends layout block content + h1 #{title} - form(method='POST' action='') + form(method='POST') div.form-group label(for='name') Genre: - input#name.form-control(type='text', placeholder='Fantasy, Poetry etc.' name='name' value=(undefined===genre ? '' : genre.name)) + 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 @@ -190,32 +189,32 @@ block content li!= error.msg ``` -从我们之前的教程中,可以很好地理解这个模板的大部分内容。首先,我们扩展 **layout.pug**基本模板,并覆盖名为“**content**”的块`block`。然后我们有一个标题,我们从控制器传入的标题`title`(通过`render()` 方法)。 +从我们之前的教程中可以很好地理解这个模板的大部分内容。首先,我们扩展 **layout.pug** 基本模板并覆盖名为 **content** 的模块 `block`。然后我们就创建了网页头部,其包含了我们从控制器传入的标题 `title`(通过 `render()` 方法)。 -接下来,我们有 HTML 表单的 Pug 代码,它使用`POST`方法将数据发送到服务器,并且因为操作`action`是空字符串,所以将数据发送到与页面相同的 URL。 +接下来,pug 代码中的 HTML 表单部分则会使用 `method="POST"` 方法将数据发送到服务器,并且由于 `action` 是空字符串,因此会将数据发送到与页面相同的 URL。 -表单定义了一个名为“name”的“text”类型的必填字段。字段的默认值,取决于是否定义了种类变量`genre`。如果从`GET`路由调用,它将为空,因为这是一个新表单。如果从`POST`路由调用,它将包含用户最初输入的(无效)值。 +该表单定义了一个名为“name”的“text”类型的必填字段。该字段的默认值取决于是否定义了种类 `genre` 变量。如果从 `GET` 路由调用,它将为空,因为这是一个新表单。如果从 `POST` 路由调用,它将包含用户最初输入的(无效)值。 -页面的最后一部分是错误代码。如果已定义错误变量,则只会打印错误列表(换句话说,当模板在`GET`路由上呈现时,此部分不会出现)。 +该页面的最后一部分是错误代码。如果已定义错误变量,则只会打印错误列表(换句话说,当模板在 `GET` 路由上呈现时,此部分将不会出现)。 > [!NOTE] -> 这只是呈现错误的一种方法。你还可以从错误变量中,获取受影响字段的名称,并使用这些,来控制错误消息的呈现位置,以及是否应用自定义 CSS 等。 +> 这只是呈现错误的一种方法。你还可以从错误变量中获取受影响字段的名称,并使用它们来控制错误消息的呈现位置以及是否应用自定义 CSS 等。 ## 它看起來像是? -运行应用程序,打开浏览器到,然后选择 Create new genre 链接。如果一切设置正确,你的网站应该类似于以下屏幕截图。输入值后,应保存该值,你将进入种类详细信息页面。 +运行应用程序,打开浏览器到 `http://localhost:3000/`,然后选择 _Create new genre_ 链接。如果一切设置正确,你的网站应该类似于下方的屏幕截图。输入值后,应将其保存,并且你将进入种类详情页面。 -![Genre Create Page - Express Local Library site](locallibary_express_genre_create_empty.png) +![种类创建页面——Express 本地图书馆网站](locallibary_express_genre_create_empty.png) -我们针对服务器端,验证的唯一错误是种类字段不能为空。下面的屏幕截图,显示了如果你没有提供种类(以红色突出显示),错误列表会是什么样子。 +我们在服务器端验证的唯一错误是种类字段必须至少包含三个字符。下面的屏幕截图显示了如果你提供仅包含一个或两个字符的类型(以黄色突出显示),错误列表会是什么样子。 -![](locallibary_express_genre_create_error.png) +![本地图书馆应用的创建种类部分。左栏有一个垂直导航栏。右侧部分是创建一个新种类,标题为“创建种类”。有一个标有“种类”的输入字段。底部有一个提交按钮。“提交”按钮正下方有一条错误消息,上面写着“需要类型名称”。本文作者强调了该错误消息。表格中没有视觉指示表明类型是必需的,也没有错误消息仅在出现错误时出现。](locallibary_express_genre_create_error.png) > [!NOTE] -> 我们的验证使用`trim()`,来确保不接受空格作为种类名称。我们还可以在表单中 的字段定义中,添加值`required='true'`,来验证客户端字段不为空: +> 我们的验证使用 `trim()` 来确保不接受空格作为种类名称。我们还对表单中​​字段定义添加 `required` ![布尔属性](/zh-CN/docs/Glossary/Boolean/HTML)来验证客户端上的字段不为空: > -> ```js -> input#name.form-control(type='text', placeholder='Fantasy, Poetry etc.' name='name' value=(undefined===genre ? '' : genre.name), required='true' ) +> ```pug +> input#name.form-control(type='text', placeholder='Fantasy, Poetry etc.' name='name' required value=(undefined===genre ? '' : genre.name) ) > ``` ## 下一步