Проверка формы регистрации пользователя звучит так просто, не правда ли? Просто убедитесь, что указанный адрес электронной почты сформирован правильно, и что пароль соответствует всем критериям, и все в порядке, верно?

Что касается меня, то только когда я начинаю реализовывать код, я понимаю, что на самом деле есть много движущихся частей. Это особенно верно при подключении полного стека приложения JavaScript, в данном случае клиента React и Apollo с сервером Express и GraphQL Yoga (также Apollo). Есть довольно много факторов, которые в конечном итоге влияют на процесс регистрации пользователя.

  1. Какие требуются кредитные данные? email/password или email/name/password или что-то более сложное и раздражающее?
  2. Какие проверки происходят, и происходят ли они на клиенте или сервере - требуется ли regex?
  3. Как schema выглядит для мутации signup?
  4. Как сообщения об ошибках отображаются пользователю - текст, цвет, положение на странице?
  5. При каких условиях отображаются эти ошибки?

Отказ от ответственности

Вероятно, существует столько же способов выполнить эту задачу, сколько существует разработчиков, реализующих ее. Это всего лишь один из способов, на который я наткнулся в последнее время, и мне понравилось, как оно получилось.

Логика сервера

Детали сервера не так важны, как преобразователь GraphQL Mutation, на котором мы собираемся сосредоточиться, но наблюдение за всем в действии может быть полезным. Клонируйте мой шаблон сервера и следуйте README, чтобы запустить свой собственный сервер, или просто следуйте инструкциям.

git clone https://github.com/benjaminadk/graphql-server-boilerplate-ts

Единственный код, который действительно интересует эту статью, - это логика проверки, преобразователь для мутации signup и соответствующая разметка внешнего интерфейса.

Схема

  • мутация signup возвращает массив Error или null
  • если возвращается null, мы можем предположить, что все прошло и в базе данных есть новый Пользователь
  • path каждого Error относится к имени поля, в котором возникла ошибка, поэтому в данном случае либо email, name, или password
  • message каждого Error - это короткое текстовое сообщение с описанием ошибки.
type Error {
  path: String!
  message: String!
}

type Mutation {
  signup(email: String!, name: String!, password: String!): [Error!]
}

Да проверка ошибок

  • Ага используется для проверки
  • синтаксис аналогичен React Prop Types
  • обеспечивает различные проверки типа, минимальной / максимальной длины, общей формы объекта и т. д.
  • передать пользовательское сообщение об ошибке в качестве последнего аргумента
  • formatYupError сопоставляет Ага форму ошибки с нашей схемой
const yup = require('yup')

const emailNotLongEnough = 'email must be at least 3 characters'
const nameNotLongEnough = 'name must be at least 3 characters'
const passwordNotLongEnough = 'password must be at least 8 characters'
const invalidEmail = 'email must be a valid email'

const validator = yup.object().shape({
  email: yup
    .string()
    .min(3, emailNotLongEnough)
    .max(100)
    .email(invalidEmail),
  name: yup
    .string()
    .min(3, nameNotLongEnough)
    .max(100),
  password: yup
    .string()
    .min(8, passwordNotLongEnough)
    .max(100)
})

const formatYupError = err => {
  const errors = []
  err.inner.forEach(e => {
    errors.push({
      path: e.path,
      message: e.message
    })
  })
  return errors
}

module.exports = { validator, formatYupError }

Регистрация мутаций Resolver

  • User означает, что ваш сервер обращается к своей модели базы данных
  • try/catch с abortEarly: false вызовет возврат ошибок до запуска любой другой логики.
  • проверьте базу данных для существующего пользователя с помощью email и выдайте ошибку, если она есть
  • наконец, создайте User, если не возникло никаких ошибок, но верните null
const { User } = require('./User')
const { validator, formatYupError } = require('./errorHelpers')

module.exports = async (_, args) => {
  const duplicateEmail = 'email already taken'

  try {
    await validator.validate(args, { abortEarly: false })
  } catch (err) {
    return formatYupError(err)
  }

  const { email, name, password } = args

  const userExists = await User.findOne({
    where: { email },
    select: ['id']
  })

  if (userExists) {
    return [
      {
        path: 'email',
        message: duplicateEmail
      }
    ]
  }

  const user = User.create({ email, name, password })

  await user.save()

  return null
} 

Клиентская логика

Опять же, точная настройка внешнего интерфейса не так важна, как сам SignupForm компонент. Мне нравится компонент высшего порядка или HOC версия Formik под названием withFormik. Компонент InnerForm содержит разметку JSX для формы, получающей реквизиты от внешнего компонента. Внешний SignupForm if where определены параметры Formik, определяющие поведение формы.

Форма регистрации

import React from 'react'
import { withFormik } from 'formik'

import { normalizeErrors, formatError } from '../../../utils/errorHelpers'
import { validUserSchema } from './validation'
import { Form, Field, Button } from './styles'
import Svg from '../../shared/Svg'

const fields = ['email', 'name', 'password']

const InnerForm = props => {
  const { values, touched, errors, isSubmitting, handleChange, handleBlur, handleSubmit } = props

  return (
    <Form onSubmit={handleSubmit}>
      {fields.map(field => {
        let error = Boolean(errors[field] && touched[field])

        return (
          <Field key={field} error={error}>
            <label>{field}</label>
            <input
              type={field}
              onChange={handleChange}
              onBlur={handleBlur}
              value={values[field]}
              name={field}
              placeholder={field === 'email' ? 'Ex. [email protected]' : ''}
              spellCheck={false}
            />
            <div className='error'>{formatError(errors[field])}</div>
          </Field>
        )
      })}
      <Button type='submit' disabled={isSubmitting}>
        {isSubmitting ? <Svg name='logo' /> : 'Sign up'}
      </Button>
    </Form>
  )
}

const SignupForm = withFormik({
  mapPropsToValues: () => ({ email: '', name: '', password: '' }),

  validationSchema: validUserSchema,

  handleSubmit: async (values, { props, setErrors, setSubmitting }) => {
    await new Promise(resolve => setTimeout(resolve, 3000))
    const errors = await props.submit(values)
    if (errors) {
      setErrors(normalizeErrors(errors))
    } else {
      props.onFinish()
    }
    setSubmitting(false)
  },

  displayName: 'SignupForm'
})(InnerForm)

export default SignupForm

Formik полезен, потому что он заботится о небольших раздражающих деталях, таких как touched, что равно true, если пользователь поместил курсор в заданное поле. Если пользователь отправляет форму без ввода каких-либо полей и оставляет всю форму пустой, запускаются все ошибки. Как только пользователь вводит допустимые данные в поле, которое отображало состояние ошибки, это поле автоматически возвращается в нормальное состояние, давая пользователю мгновенную обратную связь. Formik также обрабатывает state форму в целом и обработчики событий для каждого поля и формы в целом. Проверка Formik предназначена для того, чтобы, при желании, Ага проверять все. Чтобы понять общую картину, лучше всего увидеть форму в действии.

Это мой полный компонент из клона OfferUp, над которым я работаю. Я поставил setTimeout функции handleSubmit, чтобы проиллюстрировать другую встроенную функцию Formik - состояние отправки. Один из обратных вызовов, доступных для опции Formik handleSubmit, - setSubmitting. Он автоматически устанавливается на true при вызове handleSubmit и соответствует опоре isSubmitting, передаваемой в InnerForm. Я использовал Стилизованные компоненты для создания компонентов формы, и передача isSubmitting компоненту Button позволяет мне одновременно отключать кнопку, предотвращая множественные отправки, и отображать счетчик загрузки, позволяющий пользователю узнать, что происходит. Formik предлагает Form, Field и другие компоненты оболочки, но я обнаружил, что создавать свои собственные проще в настройке.

Проверка

Formik предлагает несколько вариантов проверки и очень гибок в этом отношении. Фактически, настройка валидации даже не требуется. Помните, что наш сервер уже выполняет собственную проверку. Мы могли бы полностью игнорировать проверку на стороне клиента, но для повышения производительности лучше ограничить запросы HTTP, когда они нам не нужны, и пользователь будет быстрее получать обратную связь с проверкой на стороне клиента. Другая стратегия проверки - это написание встроенных функций JavaScript Formik validate option, но это больше работы, чем мы хотим. Наконец, последний вариант - передать Схему проверки в Formik.

Наш validationSchema будет выглядеть довольно знакомо. Это почти идентично проверке на стороне сервера, но добавлено required. Сама схема GraphQL выдает ошибку, а пустая строка передается преобразователю благодаря оператору ! (не null). Проверка сервера по-прежнему актуальна, потому что она выдает ошибку duplicateEmail, когда пользователь пытается зарегистрироваться с уже существующим адресом электронной почты. Проверка также полезна для тестирования, и в процессе разработки мы можем вводить данные через графический интерфейс GraphQL или программно, и мы хотим, чтобы база данных была чистой, а данные были правильными.

import * as yup from 'yup'

const emailNotLongEnough = 'email must be at least 3 characters'
const emailRequired = 'Please enter an email address'
const invalidEmail = 'email must be a valid email'
const nameNotLongEnough = 'name must be at least 3 characters'
const passwordNotLongEnough = 'password must be at least 3 characters'
const fieldRequired = 'This field is required'

export const validUserSchema = yup.object().shape({
  email: yup
    .string()
    .min(3, emailNotLongEnough)
    .max(100)
    .email(invalidEmail)
    .required(emailRequired),
  name: yup
    .string()
    .min(3, nameNotLongEnough)
    .max(100)
    .required(fieldRequired),
  password: yup
    .string()
    .min(8, passwordNotLongEnough)
    .max(100)
    .required(fieldRequired)
})

Контейнер мутации

Компонент контейнера с логикой, которая взаимодействует с сервером, требуется для полного примера. Функция submit вызывает мутацию signup, а функция onFinish вызывается только тогда, когда все проходит успешно. Самое интересное в этой настройке заключается в том, что ошибка duplicateEmail сервера плавно интегрируется в систему Formik. Теперь у нас есть проверка клиента и сервера, заключенная в один и тот же пакет.

import { useMutation } from 'react-apollo'
import { withRouter } from 'react-router-dom'
import gql from 'graphql-tag'

const signupMutation = gql`
  mutation Signup($email: String!, $name: String!, $password: String!) {
    signup(email: $email, name: $name, password: $password) {
      path
      message
    }
  }
`

const SignupContainer = props => {
  const [mutate] = useMutation(signupMutation)

  async function submit(values) {
    const { data } = await mutate({
      variables: values
    })
    if (data) {
      return data.signup
    }
    return null
  }

  function onFinish() {
    props.history.push('/')
  }

  return props.children({ submit, onFinish })
}

export default withRouter(SignupContainer)

Контейнер используется с шаблоном Render Props для передачи submit, onFinish или любой другой желаемой логики своим дочерним элементам.

<SignupContainer>
  {({ submit, onFinish }) => <Signup submit={submit} onFinish={onFinish} />}
</SignupContainer>

Вспомогательные функции

Опытный глаз мог заметить пару вспомогательных функций в SignupForm примере. Функция normalizeErrors преобразует выдаваемые сервером ошибки в формат Formik, а formatError просто делает первую букву заглавной для стилизации.

export const normalizeErrors = errors => {
  return errors.reduce((acc, val) => {
    acc[val.path] = val.message
    return acc
  }, {})
}

export const formatError = error => error && error[0].toUpperCase() + error.slice(1)

Чтобы быть более точным, ошибка Formik затронута и состояние - это объект JavaScript. Каждый объект содержит ключ с атрибутом имени каждого поля в форме.

Стилизованные компоненты

Ниже приведены довольно простые стилизованные компоненты, которые я использовал. Все довольно просто, поскольку я использую опору error для переключения красного цвета, указывающего на ошибку проверки.

  • соответствующая часть темы, которую я использовал
const theme = {
  primary: '#00ab80',
  black: '#4a4a4a',
  white: '#ffffff',
  error: '#e05666',
  grey: [
    '#FAFAFA',
    '#F2F2F2',
    '#E6E5E5',
    '#D9D8D8',
    '#CDCCCB',
    '#C0BFBF',
    '#B3B2B2',
    '#A7A5A5',
    '#9A9898',
    '#817E7E',
    '#747272',
    '#676565',
    '#5A5858',
    '#4D4C4C',
    '#403F3F'
  ]
}
  • Компонент формы
export const Form = styled.form`
  width: 300px;
`
  • Полевой компонент
export const Field = styled.div`
  display: flex;
  flex-direction: column;
  color: ${p => (p.error ? p.theme.error : p.theme.black)};
  label {
    color: currentColor;
    font-size: 14px;
    font-weight: 700;
    text-transform: uppercase;
    margin-bottom: 8px;
  }
  input {
    color: currentColor;
    border: 1px solid ${p => (p.error ? 'currentColor' : p.theme.grey[4])};
    border-radius: 3px;
    font-size: 16px;
    padding: 12px 16px;
    margin-bottom: 8px;
    &::placeholder {
      color: ${p => p.theme.grey[5]};
    }
  }
  .error {
    display: ${p => (p.error ? 'block' : 'none')};
    color: currentColor;
    font-size: 14px;
  }
`
  • Компонент кнопки
const spin = keyframes`
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
`

const Button = styled.button`
  width: 100%;
  background-color: ${p => p.theme.primary};
  color: ${p => p.theme.white};
  border: 0;
  border-radius: 3px;
  font-size: 20px;
  font-weight: 700;
  line-height: 26px;
  padding: 8px 20px;
  margin-top: 20px;
  cursor: pointer;
  &:hover {
    background-color: ${p => `${darken(0.1, p.theme.primary)}`};
  }
  &:disabled {
    background-color: ${p => `${lighten(0.1, p.theme.primary)}`};
  }
  svg {
    justify-self: center;
    width: 25px;
    height: 25px;
    animation: ${spin} 1s linear infinite;
  }
  • SVG счетчик
<svg viewBox='0 0 50 50' xmlns='http://www.w3.org/2000/svg'>
  <path
    d='M49.941 23.322c1.292 19.172-18.683 32.553-35.957 24.086C5.969 43.477.66 35.575.06 26.677-1.233 7.505 18.742-5.874 36.016 2.593a24.9754 24.9754 0 0 1 13.161 16.062L44.4 24.602l-4.909-5.421c-1.346-3.382-3.9-6.35-7.613-8.169-5.005-2.454-10.941-2.056-15.571 1.046-9.976 6.683-8.968 21.644 1.816 26.931 5.005 2.453 10.94 2.054 15.57-1.047 4.716-3.16 6.979-8.172 6.912-13.134l3.95 4.359 5.32-6.62c.027.257.049.517.066.775z'
    fill='#ffffff'
  />
</svg>

Ознакомьтесь с SVG без художественных способностей, чтобы узнать больше о SVG в React.

Первоначально опубликовано на https://benjaminbrooke.me 30 августа 2019 г.