现实中的校验

校验就是对输出条件的束缚,防止有效的输出引起异样。Web 零碎的用户输出次要为编辑与提交各类表单,一方面校验要做在编辑表单字段与提交的时候,另一方面接管表单的接口也要做足校验行为,通过前后端独特管制输出条件。然而,以后端比后端校验严格时,会间接进步用户编辑信息的门槛。反之,当后端比前端校验严格时,会让辛苦填写的表单仍无奈顺利提交。这两种状况都会重大打击用户的信念,其中的关键在于校验规定的前后端统一。

抉择校验模块

基于上述思考,值得期待的校验模块应该具备以下特点:

  1. 逻辑能够跨端复用
  2. 精美,包大小无限
  3. 语义清晰
  4. 性能全面
  5. 足够稳固

综合比拟之后,抉择 yup 作为校验模块,当初以上一章已实现的工程 host1-tech/nodejs-server-examples - 01-api-and-layering 着手革新,在工程根目录装置 yup:

$ yarn add yup  # 本地装置 yup# ...info Direct dependencies└─ yup@0.29.1# ...

加上后端校验

悉心的读者会发现以后的店铺治理性能对输出是没有限度的,比方设置店铺名为空也会提交胜利。当初加上后端校验补救这一有余:

$ mkdir src/moulds            # 新建 src/moulds 目录寄存校验 schema$ tree -L 2 -I node_modules   # 展现除了 node_modules 之外的目录内容构造.├── Dockerfile├── package.json├── public│   ├── index.html│   └── index.js├── src│   ├── controllers│   ├── moulds│   ├── server.js│   └── services└── yarn.lock
// src/moulds/ShopForm.jsconst Yup = require('yup');exports.createShopFormSchema = () =>  Yup.object({    name: Yup.string()      .required('店铺名不能为空')      .min(3, '店铺名至多 3 个字符')      .max(20, '店铺名不可超过 20 字'),  });
// src/controllers/shop.jsconst { Router } = require('express');const shopService = require('../services/shop');+const { createShopFormSchema } = require('../moulds/ShopForm');class ShopController {  // ...  put = async (req, res) => {    const { shopId } = req.params;    const { name } = req.query;++    try {+      await createShopFormSchema().validate({ name });+    } catch (e) {+      res.status(400).send({ success: false, message: e.message });+      return;+    }+    const shopInfo = await this.shopService.modify({      id: shopId,      values: { name },    });    if (shopInfo) {      res.send({ success: true, data: shopInfo });    } else {      res.status(404).send({ success: false, data: null });    }  };  // ...}module.exports = async () => {  const c = new ShopController();  return await c.init();};

这样一来,不标准的输出就被无效的阻止了,成果如下:

加上前端校验

当初前端也加上校验为用户无效提供错误信息,先借助 rollup 将 yup 搬上浏览器:

$ yarn add -D rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs rollup-plugin-terser # 本地装置 rollup 及其插件# ...info Direct dependencies├─ @rollup/plugin-commonjs@14.0.0├─ @rollup/plugin-node-resolve@8.4.0├─ rollup-plugin-terser@6.1.0└─ rollup@2.22.2# ...
// package.json{  "name": "02-validate",  "version": "1.0.0",  "scripts": {-    "start": "node src/server.js"+    "start": "node src/server.js",+    "build:yup": "rollup node_modules/yup -o src/moulds/yup.js -p @rollup/plugin-node-resolve,@rollup/plugin-commonjs,rollup-plugin-terser -f umd -n 'yup'"  }  // ...}
$ yarn build:yup# ...created src/moulds/yup.js in 1.9s

而后补充前端校验逻辑:

// src/server.jsconst express = require('express');const { resolve } = require('path');const { promisify } = require('util');const initControllers = require('./controllers');const server = express();const port = parseInt(process.env.PORT || '9000');const publicDir = resolve('public');+const mouldsDir = resolve('src/moulds');async function bootstrap() {  server.use(express.static(publicDir));+  server.use('/moulds', express.static(mouldsDir));  server.use(await initControllers());  await promisify(server.listen.bind(server, port))();  console.log(`> Started on port ${port}`);}bootstrap();
// public/glue.jsimport './moulds/yup.js';window.require = (k) => window[k];window.exports = window.moulds = {};
/* public/index.css */.error {  color: red;  font-size: 14px;}
<!-- public/index.html --><html>  <head>    <meta charset="utf-8" />+    <link rel="stylesheet" href="./index.css" />  </head>  <body>    <div id="root"></div>    <script type="module">+      import './glue.js';      import { refreshShopList, bindShopInfoEvents } from './index.js';      async function bootstrap() {        await refreshShopList();        await bindShopInfoEvents();      }      bootstrap();    </script>  </body></html>
// public/index.js+import './moulds/ShopForm.js';+const { createShopFormSchema } = window.moulds;+export async function refreshShopList() {  const res = await fetch('/api/shop');  const { data: shopList } = await res.json();  const htmlItems = shopList.map(    ({ id, name }) => `<li data-shop-id="${id}">  <div data-type="text">${name}</div>  <input type="text" placeholder="输出新的店铺名称" />  <a href="#" data-type="modify">确认批改</a>  <a href="#" data-type="remove">删除店铺</a>+  <div class="error"></div></li>`  );  document.querySelector('#root').innerHTML = `<h1>店铺列表:</h1><ul class="shop-list">${htmlItems.join('')}</ul>`;}// ...export async function modifyShopInfo(e) {  const shopId = e.target.parentElement.dataset.shopId;  const name = e.target.parentElement.querySelector('input').value;++  try {+    await createShopFormSchema().validate({ name });+  } catch ({ message }) {+    console.log(message);+    e.target.parentElement.querySelector('.error').innerHTML = message;+    return;+  }+  await fetch(`/api/shop/${shopId}?name=${encodeURIComponent(name)}`, {    method: 'PUT',  });  await refreshShopList();}

看一下成果:

本章源码

host1-tech/nodejs-server-examples - 02-validate

更多浏览

从零搭建 Node.js 企业级 Web 服务器(零):动态服务
从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
从零搭建 Node.js 企业级 Web 服务器(二):校验