如果您正在构建一个 Web 应用程序,您可能会在第一天就遇到构建 HTML 表单的需求。它们是网络体验的重要组成部分,而且可能很复杂。

通常,表单处理过程包括:

  • 显示一个空的 HTML 表单以响应初始GET请求
  • POST用户在请求中提交带有数据的表单
  • 在客户端和服务器上进行验证
  • 如果无效,则重新显示填充了转义数据和错误消息的表单
  • 如果全部有效,则对服务器上的已清理数据执行某些操作
  • 处理数据后重定向用户或显示成功消息。

处理表单数据还带有额外的安全考虑。

我们将介绍所有这些并解释如何使用 Node.js 和Express(最流行的 Node 网络框架)构建它们。首先,我们将构建一个简单的联系表单,人们可以在其中安全地发送消息和电子邮件地址,然后查看处理文件上传所涉及的内容。

与以往一样,可以在我们的GitHub 存储库中找到完整的代码。

设置

确保您已安装最新版本的 Node.js。node -v应该返回8.9.0或更高。

使用 Git 从这里下载起始代码:

git clone -b starter https://github.com/sitepoint-editors/node-forms.git node-forms-starter
cd node-forms-starter
npm install
npm start

注意:repo 有两个分支,starter和master. 该starter分支包含遵循本文所需的最低设置。该master分支包含一个完整的工作演示(上面的链接)。

里面没有太多代码。它只是一个简单的 Express 设置,带有EJS 模板和错误处理程序:

// server.js
const path = require('path');
const express = require('express');
const layout = require('express-layout');

const routes = require('./routes');
const app = express();

app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

const middlewares = [
  layout(),
  express.static(path.join(__dirname, 'public')),
];
app.use(middlewares);

app.use('/', routes);

app.use((req, res, next) => {
  res.status(404).send("Sorry can't find that!");
});

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});

app.listen(3000, () => {
  console.log('App running at http://localhost:3000');
});

根 url/只是呈现index.ejs视图:

// routes.js
const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {
  res.render('index');
});

module.exports = router;

显示表格

当人们向 GET 请求时/contact,我们想要呈现一个新视图contact.ejs:

// routes.js
router.get('/contact', (req, res) => {
  res.render('contact');
});

联系表格将让他们向我们发送消息和他们的电子邮件地址:

<!-- views/contact.ejs -->
<div class="form-header">
  <h2>Send us a message</h2>
</div>
<form method="post" action="/contact" novalidate>
  <div class="form-field">
    <label for="message">Message</label>
    <textarea class="input" id="message" name="message" rows="4" autofocus></textarea>
  </div>
  <div class="form-field">
    <label for="email">Email</label>
    <input class="input" id="email" name="email" type="email" value="" />
  </div>
  <div class="form-actions">
    <button class="btn" type="submit">Send</button>
  </div>
</form>

看看它的样子http://localhost:3000/contact。

表格提交

要在 Express 中接收 POST 值,您首先需要包含中间件,该中间件在您的路由处理程序body-parser中公开提交的表单值。req.body将其添加到middlewares数组的末尾:

// server.js
const bodyParser = require('body-parser');

const middlewares = [
  // ...
  bodyParser.urlencoded({ extended: true }),
];

表单将数据 POST 回与初始 GET 请求中使用的相同 URL 是一种常见约定。让我们在这里执行此操作并处理 POST/contact以处理用户输入。

我们先来看看无效提交。如果无效,我们需要将提交的值连同我们想要显示的任何错误消息一起传回视图(因此用户不需要重新输入它们):

router.get('/contact', (req, res) => {
  res.render('contact', {
    data: {},
    errors: {}
  });
});

router.post('/contact', (req, res) => {
  res.render('contact', {
    data: req.body, // { message, email }
    errors: {
      message: {
        msg: 'A message is required'
      },
      email: {
        msg: 'That email doesn‘t look right'
      }
    }
  });
});

如果有任何验证错误,我们将执行以下操作:

  • 在表单顶部显示错误
  • 将输入值设置为提交给服务器的值
  • 在输入下方显示内联错误
  • form-field-invalid向有错误的字段添加一个类。
<!-- views/contact.ejs -->
<div class="form-header">
  <% if (Object.keys(errors).length === 0) { %>
    <h2>Send us a message</h2>
  <% } else { %>
    <h2 class="errors-heading">Oops, please correct the following:</h2>
    <ul class="errors-list">
      <% Object.values(errors).forEach(error => { %>
        <li><%= error.msg %></li>
      <% }) %>
    </ul>
  <% } %>
</div>

<form method="post" action="/contact" novalidate>
  <div class="form-field <%= errors.message ? 'form-field-invalid' : '' %>">
    <label for="message">Message</label>
    <textarea class="input" id="message" name="message" rows="4" autofocus><%= data.message %></textarea>
    <% if (errors.message) { %>
      <div class="error"><%= errors.message.msg %></div>
    <% } %>
  </div>
  <div class="form-field <%= errors.email ? 'form-field-invalid' : '' %>">
    <label for="email">Email</label>
    <input class="input" id="email" name="email" type="email" value="<%= data.email %>" />
    <% if (errors.email) { %>
      <div class="error"><%= errors.email.msg %></div>
    <% } %>
  </div>
  <div class="form-actions">
    <button class="btn" type="submit">Send</button>
  </div>
</form>

提交表格http://localhost:3000/contact以查看实际情况。这就是我们在视图方面需要的一切。

验证和消毒

有一个名为express-validator的方便的中间件,用于使用validator.js库验证和清理数据。让我们将它添加到我们的应用程序中。

验证

使用提供的验证器,我们可以轻松检查是否提供了消息和有效的电子邮件地址:

// routes.js
const { check, validationResult, matchedData } = require('express-validator');

router.post('/contact', [
  check('message')
    .isLength({ min: 1 })
    .withMessage('Message is required'),
  check('email')
    .isEmail()
    .withMessage('That email doesn‘t look right')
], (req, res) => {
  const errors = validationResult(req);
  res.render('contact', {
    data: req.body,
    errors: errors.mapped()
  });
});

消毒

使用提供的消毒剂,我们可以从值的开头和结尾修剪空白,并将电子邮件地址规范化为一致的模式。这可以帮助删除由稍微不同的输入创建的重复联系人。例如,' Mark@gmail.com'and'mark@gmail.com '都将被清理为'mark@gmail.com'.

消毒剂可以简单地链接到验证器的末尾:

// routes.js
router.post('/contact', [
  check('message')
    .isLength({ min: 1 })
    .withMessage('Message is required')
    .trim(),
  check('email')
    .isEmail()
    .withMessage('That email doesn‘t look right')
    .bail()
    .trim()
    .normalizeEmail()
], (req, res) => {
  const errors = validationResult(req);
  res.render('contact', {
    data: req.body,
    errors: errors.mapped()
  });

  const data = matchedData(req);
  console.log('Sanitized:', data);
});

该matchedData函数在我们的输入上返回消毒剂的输出。

另外,请注意我们使用了bail方法,如果之前的任何验证失败,它就会停止运行验证。我们需要这个,因为如果用户在没有在电子邮件字段中输入值的情况下提交表单,normalizeEmail则会尝试规范化一个空字符串并将其转换为@. 当我们重新呈现表单时,这将被插入到我们的电子邮件字段中。

有效表格

如果有错误,我们需要重新渲染视图。如果没有,我们需要对数据做一些有用的事情,然后显示提交成功。通常,此人会被重定向到成功页面并显示一条消息。

HTTP 是无状态的,因此您无法重定向到另一个页面并在没有会话 cookie的帮助下传递消息以在 HTTP 请求之间保留该消息。“flash message”是这种一次性消息的名称,我们希望在重定向中持续存在然后消失。

我们需要包含三个中间件来连接它:

// server.js
const cookieParser = require('cookie-parser');
const session = require('express-session');
const flash = require('express-flash');

const middlewares = [
  // ...
  cookieParser(),
  session({
    secret: 'super-secret-key',
    key: 'super-secret-cookie',
    resave: false,
    saveUninitialized: false,
    cookie: { maxAge: 60000 }
  }),
  flash(),
];

express-flash中间件添加了req.flash(type, message),我们可以在路由处理程序中使用它:

// routes
router.post('/contact', [
  // validation ...
], (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.render('contact', {
      data: req.body,
      errors: errors.mapped()
    });
  }

  const data = matchedData(req);
  console.log('Sanitized: ', data);
  // Homework: send sanitized data in an email or persist to a db

  req.flash('success', 'Thanks for the message! I‘ll be in touch :)');
  res.redirect('/');
});

express-flash中间件添加messages了req.locals所有视图都可以访问的:

<!-- views/index.ejs -->
<% if (messages.success) { %>
  <div class="flash flash-success"><%= messages.success %></div>
<% } %>

<h1>Working With Forms in Node.js</h1>

您现在应该被重定向到index视图,并在表单使用有效数据提交时看到成功消息。嘘!我们现在可以将其部署到生产环境并由尼日利亚王子发送消息。

使用节点发送电子邮件

您可能已经注意到,邮件的实际发送是留给读者作为家庭作业的。这并不像听起来那么困难,并且可以使用Nodemailer 包来完成。您可以在此处找到有关如何进行设置的基本说明,或在此处找到更深入的教程。

安全注意事项

如果您在 Internet 上使用表单和会话,则需要了解 Web 应用程序中的常见安全漏洞。我得到的最好的安全建议是“永远不要相信客户!”

基于 HTTPS 的 TLS

在处理表单时始终使用 TLS 加密https://,以便提交的数据在通过 Internet 发送时被加密。如果您通过 发送表单数据http://,它会以纯文本形式发送,并且在这些数据包通过 Web 传输时,任何窃听这些数据包的人都可以看到这些数据。

如果您想了解有关在 Node.js 中使用 SSL/TLS 的更多信息,请参阅这篇文章。

戴上你的头盔

有一个整洁的小中间件叫做安全帽,它增加了 HTTP 标头的一些安全性。最好将其包含在中间件的顶部,并且非常容易包含:

// server.js
const helmet = require('helmet');

middlewares = [
  helmet(),
  // ...
];

跨站请求伪造 (CSRF)

您可以通过在向用户呈现表单时生成唯一令牌并在处理 POST 数据之前验证该令牌来保护自己免受跨站点请求伪造。这里还有一个中间件可以帮助您:

// routes.js
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });

在 GET 请求中,我们生成一个令牌:

// routes.js
router.get('/contact', csrfProtection, (req, res) => {
  res.render('contact', {
    data: {},
    errors: {},
    csrfToken: req.csrfToken()
  });
});

并且在验证错误响应中:

router.post('/contact', csrfProtection, [
  // validations ...
], (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.render('contact', {
      data: req.body,
      errors: errors.mapped(),
      csrfToken: req.csrfToken()
    });
  }

  // ...
});

然后我们只需要将令牌包含在隐藏的输入中:

<!-- view/contact.ejs -->
<form method="post" action="/contact" novalidate>
  <input type="hidden" name="_csrf" value="<%= csrfToken %>">
  <!-- ... -->
</form>

这就是所有需要的。

我们不需要修改我们的 POST 请求处理程序,因为所有 POST 请求现在都需要csurf中间件的有效令牌。如果未提供有效的 CSRF 令牌,ForbiddenError则会抛出错误,该错误可由server.js.

您可以通过使用浏览器的开发人员工具从表单中编辑或删除令牌并提交来自己测试这一点。

跨站脚本 (XSS)

在 HTML 视图中显示用户提交的数据时需要小心,因为它可以打开跨站点脚本(XSS)。所有模板语言都提供了不同的方法来输出值。EJS<%= value %>输出HTML 转义值以保护您免受 XSS 攻击,而<%- value %>输出原始字符串。

<%= value %>处理用户提交的值时,始终使用转义输出。仅当您确定这样做是安全的时才使用原始输出。

文件上传

以 HTML 表单上传文件是一种特殊情况,需要编码类型为"multipart/form-data". 有关多部分表单提交的更多详细信息,请参阅MDN 的发送表单数据指南。

您需要额外的中间件来处理分段上传。我们将在这里使用一个名为multer的 Express 包:

// routes.js
const multer = require('multer');
const upload = multer({ storage: multer.memoryStorage() });

router.post('/contact', upload.single('photo'), csrfProtection, [
  // validation ...
], (req, res) => {
  // error handling ...

  if (req.file) {
    console.log('Uploaded: ', req.file);
    // Homework: Upload file to S3
  }

  req.flash('success', 'Thanks for the message! I’ll be in touch :)');
  res.redirect('/');
});

此代码指示multer将“照片”字段中的文件上传到内存中,并在 中公开File对象req.file,我们可以进一步检查或处理。

我们需要做的最后一件事是添加enctype属性和我们的文件输入:

<form method="post" action="/contact?_csrf=<%= csrfToken %>" novalidate enctype="multipart/form-data">
  <input type="hidden" name="_csrf" value="<%= csrfToken %>">
  <div class="form-field <%= errors.message ? 'form-field-invalid' : '' %>">
    <label for="message">Message</label>
    <textarea class="input" id="message" name="message" rows="4" autofocus><%= data.message %></textarea>
    <% if (errors.message) { %>
      <div class="error"><%= errors.message.msg %></div>
    <% } %>
  </div>
  <div class="form-field <%= errors.email ? 'form-field-invalid' : '' %>">
    <label for="email">Email</label>
    <input class="input" id="email" name="email" type="email" value="<%= data.email %>" />
    <% if (errors.email) { %>
      <div class="error"><%= errors.email.msg %></div>
    <% } %>
  </div>
  <div class="form-field">
    <label for="photo">Photo</label>
    <input class="input" id="photo" name="photo" type="file" />
  </div>
  <div class="form-actions">
    <button class="btn" type="submit">Send</button>
  </div>
</form>

尝试上传文件。您应该会File在控制台中看到记录的对象。

填充文件输入

如果出现验证错误,我们无法像处理文本输入那样重新填充文件输入(这是一个安全风险)。解决此问题的常用方法包括以下步骤:

  • 将文件上传到服务器上的临时位置
  • 显示附件的缩略图和文件名
  • 将 JavaScript 添加到表单以允许人们删除所选文件或上传新文件
  • 当一切都有效时,将文件移动到永久位置。

由于处理分段上传和文件上传的额外复杂性,它们通常以单独的形式保存。

使用节点上传文件

最后,您会注意到它留给了读者来实现实际的上传功能。这并不像听起来那么困难,并且可以使用各种包来完成,例如Formidable或express-fileupload。

加客服微信:qsiq17,开通VIP下载权限!VIP全站资源免费下载!!!