Middle: питання на співбесіді з теми «Форми й валідація»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
3 питання
Рівень 1 - вбудована валідація браузера: атрибути required, type="email", minlength, pattern, min/max.
<input v-model="form.email" type="email" required />
Безкоштовно й доступно, але повідомлення виглядають по-різному в браузерах, їх важко стилізувати, а складні правила («пароль збігається», «дата закінчення після початку») не виражаються. Атрибут novalidate на формі вимикає перевірку браузера, коли ви робите власну.
Рівень 2 - власна валідація через computed:
const errors = computed(() => ({
email: /^\S+@\S+$/.test(form.email) ? null : 'Некоректна адреса',
password: form.password.length >= 8 ? null : 'Щонайменше 8 символів',
}))
Підходить для двох-трьох полів. Далі починаються стани «поле торкнулися», «форму відправляли», асинхронні перевірки - і код розростається.
Рівень 3 - бібліотека й схема. VeeValidate керує станом полів, а Zod описує правила:
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import { z } from 'zod'
const schema = toTypedSchema(z.object({
email: z.string().email('Некоректна адреса'),
password: z.string().min(8, 'Щонайменше 8 символів'),
}))
const { defineField, errors, handleSubmit, meta } = useForm({ validationSchema: schema })
const [email, emailAttrs] = defineField('email')
const [password, passwordAttrs] = defineField('password')
const onSubmit = handleSubmit(async (values) => {
await api.post('/register', values) // values вже типізовані з схеми
})
Бібліотека дає errors, стан dirty/touched/valid (meta), валідацію на blur чи input, відправку лише валідних даних.
Схема - спільна мова: та сама схема Zod типізує дані форми й відповідь, її можна використати й для перевірки відповіді API.
Що варто знати:
- адаптер
@vee-validate/zodоголошує peer-залежність від певної мажорної версії Zod - перевіряйте сумісність, перш ніж оновлювати Zod до нової мажорної версії; - клієнтська валідація - лише для зручності. Обійти її - справа однієї секунди в DevTools. Справжня перевірка - на сервері, а помилки 422 від Laravel треба показувати поруч із клієнтськими (
setErrors()у VeeValidate); - дублювання правил між Laravel і Zod неминуче - тримайте на клієнті прості правила формату, а бізнес-правила (унікальність, ліміти) - на сервері;
- альтернативи: FormKit (з компонентами полів), Vuelidate, TanStack Form.
Поле файлу не працює з v-model - значення <input type="file"> можна лише прочитати:
<script setup lang="ts">
import { ref } from 'vue'
import axios from 'axios'
const file = ref<File | null>(null)
const progress = ref(0)
function onChange(e: Event) {
file.value = (e.target as HTMLInputElement).files?.[0] ?? null
}
async function upload() {
if (!file.value) return
const data = new FormData()
data.append('avatar', file.value)
data.append('title', 'Аватар')
await axios.post('/api/avatar', data, {
onUploadProgress: (e) => {
progress.value = e.total ? Math.round((e.loaded / e.total) * 100) : 0
},
})
}
</script>
<template>
<input type="file" accept="image/*" @change="onChange" />
<progress v-if="progress" :value="progress" max="100" />
<button @click="upload">Завантажити</button>
</template>
Головні моменти:
FormDataформує запитmultipart/form-data. ЗаголовокContent-Typeне встановлюють вручну - браузер сам додає його з правильнимboundary. РучнийContent-Type: multipart/form-dataбезboundary- сервер не розбере тіло;- прогрес завантаження дає
XMLHttpRequest(подіяupload.onprogress), і саме його використовує axios у браузері. Уfetchпрогресу відправки немає - лише прогрес отримання відповіді через потоки; PUT/PATCHз файлом у Laravel: PHP не розбираєmultipartдля цих методів. ВідправляйтеPOSTз полем_method=PUT(method spoofing).
Попередній перегляд зображення:
const preview = ref<string | null>(null)
watch(file, (f, _, onCleanup) => {
if (!f) return
const url = URL.createObjectURL(f)
preview.value = url
onCleanup(() => URL.revokeObjectURL(url)) // звільнити пам'ять
})
Великі файли:
- скасування -
AbortControllerіsignalв опціях axios, щоб користувач міг зупинити завантаження; - ліміти:
upload_max_filesizeіpost_max_sizeу PHP,client_max_body_sizeу Nginx - помилка413чи порожній$request->file()часто означає саме це; - пряме завантаження в сховище: сервер видає підписаний URL S3/R2, браузер вантажить файл туди напряму, а застосунку повідомляє лише ключ. PHP не тримає гігабайти в пам'яті й не впирається в тайм-аути;
- частинами (chunked upload, протокол tus) - для дуже великих файлів і нестабільних з'єднань.
Перевірка на клієнті (accept, розмір файлу) - для зручності; сервер усе одно валідує тип і розмір (mimes, max).
У формах повторюється той самий набір: мітка, поле, підказка, помилка, атрибути доступності. Його виносять у компонент поля.
defineModel (Vue 3.4+) - найкоротший спосіб підтримати v-model на власному компоненті:
<!-- TextField.vue -->
<script setup lang="ts">
import { useId } from 'vue'
defineOptions({ inheritAttrs: false })
const model = defineModel<string>({ required: true })
const props = defineProps<{ label: string; error?: string; hint?: string }>()
const id = useId()
</script>
<template>
<div class="field">
<label :for="id">{{ props.label }}</label>
<input
:id="id"
v-model="model"
v-bind="$attrs"
:aria-invalid="props.error ? 'true' : undefined"
:aria-describedby="props.error ? `${id}-error` : undefined"
/>
<p v-if="props.hint && !props.error">{{ props.hint }}</p>
<p v-if="props.error" :id="`${id}-error`" class="error">{{ props.error }}</p>
</div>
</template>
<TextField v-model="form.email" label="Email" type="email" autocomplete="email" :error="errors.email?.[0]" />
Що тут важливо:
defineModelстворює ref, синхронізований з батьком: читання - значенняmodelValue, запис - подіяupdate:modelValue. Без нього довелося б оголошувати проп і подію вручну;inheritAttrs: false+v-bind="$attrs"- атрибути, передані компоненту (type,autocomplete,placeholder,required), потрапляють на<input>, а не на обгорткуdiv. Інакшеtype="email"опиниться наdivі не працюватиме;- модифікатори теж підтримуються:
const [model, modifiers] = defineModel({ set(v) { return modifiers.trim ? v.trim() : v } }); - кілька моделей на одному компоненті:
defineModel('from')іdefineModel('to')для діапазону дат -v-model:fromіv-model:to.
Типові помилки:
- мутувати проп напряму (
props.modelValue = ...) - Vue попередить, а батько не дізнається про зміну; - локальна копія значення без синхронізації - поле перестає реагувати на скидання форми батьком;
- об'єкт у
defineModelі зміна його властивостей (model.value.city = ...) мутує об'єкт батька в обхід події - для об'єктів присвоюйте нове значення (model.value = { ...model.value, city }).
Межа абстракції: компонент поля відповідає за розмітку й доступність, а не за валідацію. Правила й стан форми лишаються у формі чи бібліотеці валідації - тоді той самий TextField працює з будь-яким підходом.