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

理想中的校验

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

80cf3c924e0ec7744800883aad9e2a9c7b19d80c.jpg

选择校验模块

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

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

综合比较之后,选择 yup 作为校验模块,现在以上一章已完成的工程 licg9999/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();
};

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

d197d23f063b97d51b2dbbfcc1a663dfe8e54fc0.gif

加上前端校验

现在前端也加上校验为用户有效提供错误信息,先借助 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 }) {
+    e.target.parentElement.querySelector('.error').innerHTML = message;
+    return;
+  }
+
  await fetch(`/api/shop/${shopId}?name=${encodeURIComponent(name)}`, {
    method: 'PUT',
  });
  await refreshShopList();
}

看一下效果:

357aae2f495522b2191c47ee9deeaa3ff18e4e09.gif

本章源码

licg9999/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 服务器(十五):总结与展望

认真写点好代码。

2.1k 声望
1k 粉丝
0 条评论
推荐阅读
Transpile Webpack Plugin:让 Webpack 按照源文件的目录结构输出
作为 Web 开发者,你是否也纠结过如何用 Webpack 做文件转译?就像 Babel CLI 转译文件那样按照源文件的目录结构输出?如果有,那么这篇文章就是为你而写,我们一起瞧一瞧怎么做吧。

乌柏木5阅读 718

从零搭建 Node.js 企业级 Web 服务器(零):静态服务
过去 5 年,我前后在菜鸟网络和蚂蚁金服做开发工作,一方面支撑业务团队开发各类业务系统,另一方面在自己的技术团队做基础技术建设。期间借着 Node.js 的锋芒做了不少 Web 系统,有的至今生气蓬勃、有的早已夭折...

乌柏木148阅读 12.2k评论 10

JavaScript有用的代码片段和trick
平时工作过程中可以用到的实用代码集棉。判断对象否为空 {代码...} 浮点数取整 {代码...} 注意:前三种方法只适用于32个位整数,对于负数的处理上和Math.floor是不同的。 {代码...} 生成6位数字验证码 {代码...} ...

jenemy46阅读 5.9k评论 12

从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望
总结截止到本章 “从零搭建 Node.js 企业级 Web 服务器” 主题共计 16 章内容就更新完毕了,回顾第零章曾写道:搭建一个 Node.js 企业级 Web 服务器并非难事,只是必须做好几个关键事项这几件必须做好的关键事项就...

乌柏木66阅读 6.1k评论 16

再也不学AJAX了!(二)使用AJAX ① XMLHttpRequest
「再也不学 AJAX 了」是一个以 AJAX 为主题的系列文章,希望读者通过阅读本系列文章,能够对 AJAX 技术有更加深入的认识和理解,从此能够再也不用专门学习 AJAX。本篇文章为该系列的第二篇,最近更新于 2023 年 1...

libinfs39阅读 6.3k评论 12

封面图
从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
分层规范从本章起,正式进入企业级 Web 服务器核心内容。通常,一块完整的业务逻辑是由视图层、控制层、服务层、模型层共同定义与实现的,如下图:从上至下,抽象层次逐渐加深。从下至上,业务细节逐渐清晰。视图...

乌柏木43阅读 7.3k评论 6

CSS 绘制一只思否猫
欢迎关注我的公众号:前端侦探练习 CSS 有一个比较有趣的方式,就是发挥想象,绘制各式各样的图案,比如来绘制一只思否猫?思否猫,SegmentFault 思否的吉祥物,是一只独一无二、特立独行、热爱自由的(&gt;^ω^&lt...

XboxYan43阅读 2.9k评论 14

封面图

认真写点好代码。

2.1k 声望
1k 粉丝
宣传栏