Доброго времени суток, друзья!
В этом небольшом туториале я хочу продемонстировать вам пример клиент-серверной валидации формы.
Клиент будет реализован на React, сервер на Express.
Мы не будем изобретать велосипеды, а воспользуемся готовыми решениями: для валидации формы на стороне клиента будет использоваться react-hook-form (+: используются хуки, русский язык), а на стороне сервера express-validator.
Для стилизации будет использоваться styled-components (CSS-in-JS или All-in-JS, учитывая JSX).
Исходный код примера находится здесь.
Поиграть с кодом можно здесь.
Без дальнейших предисловий.
Клиент
Создаем проект с помощью create-react-app:
yarn create react-app form-validation# илиnpm init react-app form-validation# илиnpx create-react-app form-validation
В дальнейшем для установки зависимостей и выполнения команд я буду использовать yarn.
Структура проекта после удаления лишних файлов:
public index.htmlsrc App.js index.js styles.jsserver.js...
Устанавливаем зависимости:
# для клиентаyarn add styled-components react-hook-form# для сервера (производственные зависимости)yarn add express express-validator cors# для сервера (зависимость для разработки)yarn add -D nodemon# для одновременного запуска серверовyarn add concurrently
Поскольку styled-components не умеет импотировать шрифты, нам придется добавить их в public/index.html:
<head> ... <link rel="preconnect" href="http://personeltest.ru/aways/fonts.gstatic.com" /> <link href="http://personeltest.ru/aways/fonts.googleapis.com/css2?family=Comfortaa&display=swap" rel="stylesheet" /></head>
Наша форма будет состоять из трех полей: имя пользователя, его адрес электронной почты и пароль. Условия, которым должны удовлетворять данные:
- Имя
- от 2 до 10 символов
- кириллица
- Email
- особых требований не предъявляется
- Пароль
- 8-12 символов
- латиница: буквы в любом регистре, цифры, нижнее подчеркивание и дефис
Начнем со стилизации (src/styles.js; для подстветки синтаксиса я использую расширение для VSCode vscode-styled-components):
// импорт инструментовimport styled, { createGlobalStyle } from 'styled-components'// глобальные стилиconst GlobalStyle = createGlobalStyle` body { margin: 0; min-height: 100vh; display: grid; place-items: center; background-color: #1c1c1c; font-family: 'Comfortaa', cursive; font-size: 14px; letter-spacing: 1px; color: #f0f0f0; }`// заголовокconst StyledTitle = styled.h1` margin: 1em; color: orange;`// формаconst StyledForm = styled.form` margin: 0 auto; width: 320px; font-size: 1.2em; text-align: center;`// подписьconst Label = styled.label` margin: 0.5em; display: grid; grid-template-columns: 1fr 2fr; align-items: center; text-align: left;`// проект поля для ввода данныхconst BaseInput = styled.input` padding: 0.5em 0.75em; font-family: inherit; font-size: 0.9em; letter-spacing: 1px; outline: none; border: none; border-radius: 4px;`// обычное полеconst RegularInput = styled(BaseInput)` background-color: #f0f0f0; box-shadow: inset 0 0 2px orange; &:focus { background-color: #1c1c1c; color: #f0f0f0; box-shadow: inset 0 0 4px yellow; }`// поле для отправки данных на серверconst SubmitInput = styled(BaseInput)` margin: 1em 0.5em; background-image: linear-gradient(yellow, orange); cursor: pointer; &:active { box-shadow: inset 0 1px 3px #1c1c1c; }`// проект сообщения с текстомconst BaseText = styled.p` font-size: 1.1em; text-align: center; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25);`// сообщение об ошибкеconst ErrorText = styled(BaseText)` font-size: ${(props) => (props.small ? '0.8em' : '1.1em')}; color: red;`// сообщение об успехеconst SuccessText = styled(BaseText)` color: green;`// экспорт стилизованных компонентовexport { GlobalStyle, StyledTitle, StyledForm, Label, RegularInput, SubmitInput, ErrorText, SuccessText}
Импортируем и подключаем глобальные стили в src/index.js:
import React from 'react'import ReactDOM from 'react-dom'// импортируем глобальные стилиimport { GlobalStyle } from './styles'import App from './App'ReactDOM.render( <React.StrictMode> {/* подключаем глобальные стили */} <GlobalStyle /> <App /> </React.StrictMode>, document.getElementById('root'))
Переходим к основному файлу клиента (src/App.js):
import { useState } from 'react'// импорт хука для валидации формыimport { useForm } from 'react-hook-form'// импорт стилизованных компонентовimport { StyledTitle, StyledForm, Label, RegularInput, SubmitInput, ErrorText, SuccessText} from './styles'// компонент заголовкаfunction Title() { return <StyledTitle>Валидация формы</StyledTitle>}// компонент формыfunction Form() { // инициализируем начальное состояние const [result, setResult] = useState({ message: '', success: false }) // извлекаем средства валидации: // регистрация проверяемого поля // ошибки и обработка отправки формы const { register, errors, handleSubmit } = useForm() // общие валидаторы const validators = { required: 'Не может быть пустым' } // функция отправки формы async function onSubmit(values) { console.log(values) const response = await fetch('http://localhost:5000/server', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(values) }) const result = await response.json() // обновляем состояние setResult({ message: result, success: response.ok }) } // нажатие кнопки сброса полей в исходное состояние приводит к перезагрузке страницы function onClick() { window.location.reload() } return ( <> <StyledForm onSubmit={handleSubmit(onSubmit)}> <Label> Имя: <RegularInput type='text' name='name' // поля являются неуправляемыми // это повышает производительность ref={register({ ...validators, minLength: { value: 2, message: 'Не менее двух букв' }, maxLength: { value: 10, message: 'Не более десяти букв' }, pattern: { value: /[А-ЯЁ]{2,10}/i, message: 'Только киррилица' } })} defaultValue='Иван' /> </Label> {/* ошибки */} <ErrorText small>{errors.name && errors.name.message}</ErrorText> <Label> Email: <RegularInput type='email' name='email' ref={register({ ...validators, pattern: { value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i, message: 'Неправильный адрес электронной почты' } })} defaultValue='email@example.com' /> </Label> <ErrorText small>{errors.email && errors.email.message}</ErrorText> <Label> Пароль: <RegularInput type='password' name='password' ref={register({ ...validators, pattern: { value: /^[A-Z0-9_-]{8,12}$/i, message: 'От 8 до 12 символов: латиница, цифры, нижнее подчеркивание и дефис' } })} defaultValue='password' /> </Label> <ErrorText small> {errors.password && errors.password.message} </ErrorText> <SubmitInput type='submit' defaultValue='Отправить' /> {/* обратите внимание на атрибут "as", он позволяет превратить "инпут" в кнопку с аналогичными стилями */} <SubmitInput as='button' onClick={onClick}> Сбросить </SubmitInput> </StyledForm> {/* результат отправки формы */} {result.success ? ( <SuccessText>{result.message}</SuccessText> ) : ( <ErrorText>{result.message}</ErrorText> )} </> )}export default function App() { return ( <> <Title /> <Form /> </> )}
Метод register() хука useForm() поддерживает все атрибуты тега input. Полный список таких атрибутов. В случае с именем, мы могли бы ограничиться регулярным выражением.
Запускаем сервер для клиента с помощью yarn start и тестируем форму:
Замечательно. Валидация на стороне клиента работает, как ожидается. Но ее всегда можно отключить. Поэтому нужна валидация на сервере.
Сервер
Приступаем к реализации сервера (server.js):
const express = require('express')// body читает тело запроса// validationResult - результат валидацииconst { body, validationResult } = require('express-validator')const cors = require('cors')const app = express()const PORT = process.env.PORT || 5000app.use(cors())app.use(express.json())app.use(express.urlencoded({ extended: false }))// валидаторыconst validators = [ body('name').trim().notEmpty().isAlpha('ru-RU').escape(), body('email').normalizeEmail().isEmail(), // кастомный валидатор body('password').custom((value) => { const regex = /^[A-Z0-9_-]{8,12}$/i if (!regex.test(value)) throw new Error('Пароль не соответствует шаблону') return true })]// валидаторы передаются в качестве middlewareapp.post('/server', validators, (req, res) => { // извлекаем массив с ошибками из результата валидации const { errors } = validationResult(req) console.log(errors) // если массив с ошибками не является пустым if (errors.length) { res.status(400).json('Регистрация провалилась') } else { res.status(201).json('Регистрация прошла успешно') }})app.listen(PORT, () => { console.log(`Сервер готов. Порт: ${PORT}`)})
Полный список доступных валидаторов можно посмотреть здесь.
Добавим в package.json парочку скриптов server для запуска сервера и dev для одновременного запуска серверов:
"scripts": { "start": "react-scripts start", "build": "react-scripts build", "server": "nodemon server", "dev": "concurrently \"yarn server\" \"yarn start\""}
Выполняем yarn dev и тестируем отправку формы:
Прекрасно. Кажется, у нас все получилось.
Мы с вами рассмотрели очень простой вариант клиент-серверной валидации формы. Вместе с тем, более сложные варианты предполагают лишь увеличение количества валидаторов, общие принципы остаются такими же. Также стоит отметить, что валидацию формы на стороне клиента вполне можно реализовать средствами HTML (GitHub, CodeSandbox).
Благодарю за внимание и хорошего дня.