校驗就是對輸入條件的約束,避免無效的輸入引發異常。Web 系統的用戶輸入主要爲編輯與提交各種表單,一方面校驗要作在編輯表單字段與提交的時候,另外一方面接收表單的接口也要作足校驗行爲,經過先後端共同控制輸入條件。然而,當前端比後端校驗嚴格時,會直接提升用戶編輯信息的門檻。反之,當後端比前端校驗嚴格時,會讓辛苦填寫的表單仍沒法順利提交。這兩種狀況都會嚴重打擊用戶的信心,其中的關鍵在於校驗規則的先後端一致。css
基於上述思考,值得期待的校驗模塊應該具有如下特色:html
綜合比較以後,選擇 yup 做爲校驗模塊,如今以上一章已完成的工程 host1-tech/nodejs-server-examples - 01-api-and-layering 着手改造,在工程根目錄安裝 yup:前端
$ yarn add yup # 本地安裝 yup # ... info Direct dependencies └─ yup@0.29.1 # ...
悉心的讀者會發現當前的店鋪管理功能對輸入是沒有限制的,好比設置店鋪名爲空也會提交成功。如今加上後端校驗彌補這一不足:node
$ 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(); };
這樣一來,不規範的輸入就被有效的阻止了,效果以下:git
如今前端也加上校驗爲用戶有效提供錯誤信息,先借助 rollup 將 yup 搬上瀏覽器:github
$ 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
而後補充前端校驗邏輯:express
// 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(); }
看一下效果:json
host1-tech/nodejs-server-examples - 02-validatebootstrap
從零搭建 Node.js 企業級 Web 服務器(零):靜態服務
從零搭建 Node.js 企業級 Web 服務器(一):接口與分層
從零搭建 Node.js 企業級 Web 服務器(二):校驗segmentfault