Чистые, масштабируемые формы с Vue Composition API

Spread the love

Перевод статьи: Anthony Gore — Clean, Scalable Forms with Vue Composition API

Формы — одна из самых сложных частей разработки веб-интерфейса, где обычно накапливается много грязного кода.

Компонентно-ориентированные фреймворки, такие как Vue.js 2, многое сделали для улучшения масштабируемости кода, но проблема чистоты кода форм сохраняется.

В этом уроке я покажу вам, как новое Vue Composition API (начиная с Vue 3) сделает ваш код форм намного чище и более масштабируемым.

Содержание:


Почему код формы часто отстой

Ключевым шаблоном проектирования основанных на компонентах сред, таких как Vue, является состав компонентов. Этот шаблон говорит нам, что нужно абстрагировать функции нашего приложения в изолированные, специализированные компоненты, которые связывают состояние с props и событиями.

Однако формы нельзя аккуратно абстрагировать в соответствии с этим шаблоном, поскольку функциональность и состояние формы явно не принадлежат какому-либо одному компоненту, и поэтому их разделение часто вызывает больше проблем, чем пользы.

Другая важная причина, по которой код форм часто отстой в приложениях Vue, заключается в том, что вплоть до Vue 2 у Vue не было надежных средств повторного использования кода между компонентами. Это важно в формах, так как входные данные часто различаются, но имеют много общего в функциональности.

Основной метод повторного использования кода, предлагаемый Vue 2, — это миксины, которые как многие утверждают, явный анти-паттерн.

Vue Composition API

Composition API — это новый способ определения компонентов с помощью Vue.js, который станет основной функцией Vue 3. Он также доступен для использования сегодня в Vue 2 в качестве плагина.

Это новое API предназначено для борьбы с некоторыми из упомянутых мною проблем (не только в формах, но и в любом аспекте архитектуры приложения).

Если вы все еще не знакомы с API-интерфейсом композиции или не знаете, для чего он нужен, я рекомендую сначала прочитать документацию, а также другую статью, которую я написал, «Когда использовать новый API-интерфейс Vue (а когда нет»).

Composition API — это не замена классическому Vue API, а то, что вы можете использовать при необходимости. Как вы увидите в этой статье, создание чистого и масштабируемого кода формы является идеальным вариантом использования.

Добавление Composition API в проект Vue 2

Так как я пишу это руководство до выпуска Vue 3, давайте добавим API композиции в проект Vue 2 в качестве плагина.

Мы начнем с создания нового проекта Vue CLI ( используем только необходимые функции — все, что нам нужно — без роутера, Vuex и т. д.) И установим плагин Composition API с помощью NPM.

$ vue create composition-api-form
$ cd composition-api-form
$ npm i -S @vue/composition-api

Далее, давайте добавим плагин к нашему экземпляру Vue в main.js.

src/main.js

import Vue from "vue";
import App from "./App.vue";

import VueCompositionApi from "@vue/composition-api";
Vue.use(VueCompositionApi);

new Vue({
  render: h => h(App)
}).$mount('#app');

Создание компонентов ввода формы

Чтобы сделать это простым примером, мы собираемся создать форму с двумя input — имя и адрес электронной почты. Давайте создадим их как отдельные компоненты.

$ touch src/components/InputName.vue
$ touch src/components/InputEmail.vue

Теперь давайте настроим шаблон компонента InputName обычным способом, включая элемент ввода HTML с директивой v-model, создающий двустороннюю привязку с компонентом.

src/components/InputName.vue

<template>
  <div>
    <label>
      Name
      <input type="text" v-model="input" name="name" />
    </label>
  </div>
</template>
<script>
export default {
  name: 'InputName'
}
</script>

Настройка формы

Оставим пока input и настроим форму. Вы можете создать ее как отдельный компонент, чтобы сделать ее многократно используемой, но для простоты руководства я просто объявлю ее в шаблоне компонента App.

Мы добавим атрибут novalidate, чтобы браузер знал, что мы сами реализуем пользовательскую проверку. Мы также будем слушать событие отправки формы submit, предотвращая автоматическую отправку и обработаем это событие с помощью метода onSubmit, который мы вскоре объявим.

Затем мы добавим компоненты InputName и InputEmail и свяжем с ними локальный переменные name и email соответственно.

src/App.vue

<template>
  <div id="app">
    <form novalidate @submit.prevent="onSubmit">
      <InputName v-model="name" />
      <InputEmail v-model="email" />
      <button type="submit">Submit</button>
    </form>
  </div>
</template>
<script>
import InputName from "@/components/InputName";
import InputEmail from "@/components/InputEmail";
export default {
  name: 'App',
  components: {
    InputName,
    InputEmail
  }
}
</script>

Давайте теперь определим функциональность формы, используя Composition API. Мы добавим метод setup в определение компонента, где мы объявим две переменные name и email, используя метод ref Composition API. Этот метод необходимо будет импортировать из пакета Composition API.

Затем мы объявим функцию onSubmit для обработки отправки формы. Я не буду определять какую-либо функциональность, поскольку она не имеет отношения к этому уроку.

Наконец, нам нужно вернуть две переменные состояния и метод, который мы создали из функции setup, чтобы они были доступны для шаблона компонента.

src/App.vue

...
import { ref } from "@vue/composition-api";

export default {
  name: "App",
  setup () {
    const name = ref("");
    const email = ref("");
    function onSubmit() {
      // submit to backend or whatever you like
      console.log(name.value, email.value);
    }
    return {
      name,
      email,
      onSubmit
    }
  },
  ...
}

Настройка input

Далее мы собираемся определить функциональность компонента InputName.

Поскольку родительская форма использует v-model с этим компонентом, важно объявить значение prop, которое будет составлять половину двусторонней привязки.

Давайте создадим функцию setup. В этот метод передаются props, как и объект контекста, что дает нам доступ к методам экземпляра компонента. Мы можем деструктурировать этот второй аргумент и получить метод emit. Нам понадобится это для выполнения другой половины двусторонней привязки v-model, то есть для реактивного выделения новых значений входных данных.

Прежде чем мы перейдем к этому, давайте объявим входную переменную состояния input, которая будет привязана к входному HTML-элементу, который мы объявили в шаблоне.

Значением этой переменной будет то, что мы вернем из определенной функции композиции useInputValidator. Эта функция будет обрабатывать всю общую логику проверки.

Мы передадим значение prop этому методу, и вторым аргументом будет функция обратного вызова, которая возвращает проверенное входное значение. Давайте использовать этот обратный вызов, чтобы выдать этот ввод как событие и выполнить контракт v-model.

src/components/InputName.vue

import useInputValidator from "@/features/useInputValidator";

export default {
  name: "InputName",
  props: {
    value: String
  },
  setup (props, { emit }) {
    const { input } = useInputValidator(
      props.value, 
      value => emit("input", value)
    );
    return {
      input
    }
  }
}

Функция проверки ввода

Давайте теперь создадим композиционную функцию useInputValidator. Для этого мы сначала создадим папку features, а затем создадим для нее файл модуля.

$ mkdir src/features
$ touch src/features/useInputValidator.js

В файле модуля мы собираемся экспортировать функцию. Мы только что увидели, что ему понадобятся два аргумента — props, значение полученное из родительской формы, которую мы будем называть startVal, и метод обратного вызова, который мы будем вызывать onValidate.

Помните, что эта функция должна возвращать входную переменную состояния input, поэтому давайте продолжим и объявим об этом, присваивая ref, которая инициализируется значением, предоставленным props.

Прежде чем мы вернем значение input из функции, давайте посмотрим его значение и вызовем обратный вызов onValidate, используя входные данные в качестве аргумента.

src/features/useInputValidator.js

import { ref, watch } from "@vue/composition-api";

export default function (startVal, onValidate) {
  let input = ref(startVal);
  watch(input, value => { 
    onValidate(value);
  });
  return {
    input
  }
}

Добавление валидаторов

Следующим шагом является добавление функций валидатора. Для компонента InputName у нас есть только одно правило проверки — minLength, обеспечивающее ввод трех символов или более. Для еще не созданного компонента InputEmail потребуется подтверждение корректности электронный почты.

Теперь мы создадим эти валидаторы в служебном модуле JavaScript validators.js в папке src. В реальном проекте вы, вероятно, вместо этого использовали бы стороннюю библиотеку.

Я не буду подробно останавливаться на функциях валидатора, но обратите внимание на две важные вещи:

  • Это функции, которые возвращают функции. Эта архитектура позволяет нам настраивать проверку, передавая аргументы, которые становятся частью замыкания.
  • Функция, возвращаемая каждым валидатором, всегда возвращает либо строку (сообщение об ошибке), либо значение NULL в случае отсутствия ошибки.

src/validators.js

const minLength = min => {
  return input => input.length < min 
  ? `Value must be at least ${min} characters` 
  : null;
};

const isEmail = () => {
  const re = /\S+@\S+\.\S+/;
  return input => re.test(input)
  ? null
  : "Must be a valid email address";
}

export { minLength, isEmail };

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

Внутри watcher input мы теперь обработаем функции проверки. Давайте используем метод map массива validators, передавая текущее значение входных данных каждому методу validator.

Возврат будет записан в новой переменной состояния, errors, которые мы также вернем в потребляющий компонент.

src/features/useInputValidator.js

export default function (startVal, validators, onValidate) {
  const input = ref(startVal);
  const errors = ref([]);
  watch(input, value => {
    errors.value = validators.map(validator => validator(value));
    onValidate(value);
  });
  return {
    input,
    errors
  }
}

Наконец, возвращаясь к компоненту InputName, мы собираемся предоставить три обязательных аргумента методу useInputValidator. Помните, что второй аргумент теперь является массивом валидаторов, поэтому давайте объявим массив на месте и передадим в minLength, который мы получим путем импорта из файла валидаторов.

minLength — это фабричная функция, поэтому мы вызываем функцию со значением минимальной длины, которую мы хотим указать.

Теперь мы также получаем два объекта, возвращаемых нашей композиционной функцией — input и errors. Оба они будут возвращены из метода setup для доступности в контексте рендеринга компонента.

src/components/InputName.vue

...
import { minLength } from "@/validators";

export default {
  ...
  setup (props, { emit }) {
    const { input, errors } = useInputValidator(
      props.value, 
      [ minLength(3) ],
      value => emit("input", value)
    );
    return {
      input,
      errors
    }
  }
}

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

Во-первых, мы ясно видим, где наши переменные состояния объявляются и изменяются без необходимости переходить в отдельный файл модуля mixin. Во-вторых, нам не нужно беспокоиться о столкновении имен между нашими локальными переменными и функцией композиции.

Отображение ошибок

Переходя к шаблону нашего компонента InputName, теперь у нас есть массив потенциальных ошибок для отображения. Давайте делегируем это компоненту ошибки под названием ErrorDisplay.

src/components/InputName.vue

<template>
  <div>
    <label>
      Name
      <input type="text" v-model="input" name="name" />
    </label>
    <ErrorDisplay :errors="errors" />
  </div>
</template>
<script>
...
import ErrorDisplay from "@/components/ErrorDisplay";

export default: {
  ...
  components: {
    ErrorDisplay
  }
}
</script>

Функциональность ErrorDisplay слишком тривиальна, чтобы здесь ее описывать.

Повторное использование кода

Это основная функциональность нашей формы с Composition API. Целью этого руководства было создание чистого и масштабируемого кода формы, и я хочу доказать вам, что мы сделали это, заканчивая определением нашего второго пользовательского ввода InputEmail.

Если цель этого руководства была достигнута, у вас не должно возникнуть проблем с ее пониманием без моего дальнейшего пояснения!

src/components/InputEmail

<template>
  <div>
    <label>
      Email
      <input type="email" v-model="input" name="email" />
    </label>
    <ErrorDisplay v-if="input" :errors="errors" />
  </div>
</template>
<script>
import useInputValidator from "@/features/useInputValidator";
import { isEmail } from "@/validators";
import ErrorDisplay from "./ErrorDisplay";

export default {
  name: "InputEmail",
  props: {
    value: String
  },
  setup (props, { emit }) {
    const { input, errors } = useInputValidator(
      props.value, 
      [ isEmail() ], 
      value => emit("input", value)
    );
    return {
      input,
      errors
    }
  },
  components: {
    ErrorDisplay
  }
}
</script>
Была ли вам полезна эта статья?
[3 / 5]

Spread the love
Подписаться
Уведомление о
guest
0 Комментарий
Inline Feedbacks
View all comments
0
Будем рады вашим мыслям, пожалуйста, прокомментируйте.x
()
x