从零搭建 Node.js 企业级 Web 服务器(二):校验

理想中的校验

校验就是对输入条件的约束,避免无效的输入引起异常。Web 系统的用户输入主要为编辑与提交各类表单,一方面校验要做在编辑表单字段与提交的时候,另一方面接收表单的接口也要做足校验行为,通过前后端共同控制输入条件。然而,当前端比后端校验严格时,会直接提高用户编辑信息的门槛。反之,当后端比前端校验严格时,会让辛苦填写的表单仍无法顺利提交。这两种情况都会严重打击用户的信心,其中的关键在于校验规则的前后端一致。

80cf3c924e0ec7744800883aad9e2a9c7b19d80c.jpg

选择校验模块

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

  1. 逻辑可以跨端复用
  2. 精巧,包大小有限
  3. 语义清晰
  4. 功能全面
  5. 足够稳定

综合比较之后,选择 yup 作为校验模块,现在以上一章已完成的工程 host1-tech/nodejs-server-examples - 01-api-and-layering 着手改造,在工程根目录安装 yup:

$ yarn add yup  # 本地安装 yup

# ...

info Direct dependencies

└─ [email protected]

# ...

加上后端校验

悉心的读者会发现当前的店铺管理功能对输入是没有限制的,比如设置店铺名为空也会提交成功。现在加上后端校验弥补这一不足:

$ 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();

};

这样一来,不规范的输入就被有效的阻止了,效果如下:

d197d23f063b97d51b2dbbfcc1a663dfe8e54fc0.gif

加上前端校验

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

$ yarn add -D rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs rollup-plugin-terser # 本地安装 rollup 及其插件

# ...

info Direct dependencies

├─ @rollup/[email protected]

├─ @rollup/plugin-[email protected]

├─ [email protected]

└─ [email protected]

# ...

// 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="https://segmentfault.com/a/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 }) {

+ e.target.parentElement.querySelector('.error').innerHTML = message;

+ return;

+ }

+

await fetch(`/api/shop/${shopId}?name=${encodeURIComponent(name)}`, {

method: 'PUT',

});

await refreshShopList();

}

看一下效果:

357aae2f495522b2191c47ee9deeaa3ff18e4e09.gif

本章源码

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

更多阅读

从零搭建 Node.js 企业级 Web 服务器(零):静态服务
从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
从零搭建 Node.js 企业级 Web 服务器(二):校验
从零搭建 Node.js 企业级 Web 服务器(三):中间件
从零搭建 Node.js 企业级 Web 服务器(四):异常处理
从零搭建 Node.js 企业级 Web 服务器(五):数据库访问
从零搭建 Node.js 企业级 Web 服务器(六):会话
从零搭建 Node.js 企业级 Web 服务器(七):认证登录
从零搭建 Node.js 企业级 Web 服务器(八):网络安全
从零搭建 Node.js 企业级 Web 服务器(九):配置项
从零搭建 Node.js 企业级 Web 服务器(十):日志
从零搭建 Node.js 企业级 Web 服务器(十一):定时任务
从零搭建 Node.js 企业级 Web 服务器(十二):远程调用
从零搭建 Node.js 企业级 Web 服务器(十三):断点调试与性能分析
从零搭建 Node.js 企业级 Web 服务器(十四):自动化测试
从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望

以上是 从零搭建 Node.js 企业级 Web 服务器(二):校验 的全部内容, 来源链接: utcz.com/a/48403.html

回到顶部