现实中的校验
校验就是对输出条件的束缚,防止有效的输出引起异样。Web 零碎的用户输出次要为编辑与提交各类表单,一方面校验要做在编辑表单字段与提交的时候,另一方面接管表单的接口也要做足校验行为,通过前后端独特管制输出条件。然而,以后端比后端校验严格时,会间接进步用户编辑信息的门槛。反之,当后端比前端校验严格时,会让辛苦填写的表单仍无奈顺利提交。这两种状况都会重大打击用户的信念,其中的关键在于校验规定的前后端统一。
抉择校验模块
基于上述思考,值得期待的校验模块应该具备以下特点:
- 逻辑能够跨端复用
- 精美,包大小无限
- 语义清晰
- 性能全面
- 足够稳固
综合比拟之后,抉择 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.js
const Yup = require('yup');
exports.createShopFormSchema = () =>
Yup.object({name: Yup.string()
.required('店铺名不能为空')
.min(3, '店铺名至多 3 个字符')
.max(20, '店铺名不可超过 20 字'),
});
// src/controllers/shop.js
const {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.js
const 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.js
import './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 服务器(二):校验