Админка магазина на vue.js. Урок 11. Обрабатываем ошибки на клиенте и сервере

сентябрь 17 , 2019
Метки:
Предыдущая статья Следующая статья Исходники

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

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

Работать с нашим приложением будут не программисты. Представьте менеджера, которому нужно забить в базу 3 сотни товаров, и это до завтра. Он не будет изучать нашу инструкцию, он сядет и сразу будет набивать по списку. Так сделает любой нормальный человек.

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


Валидация на клиенте и сервере. В чем разница?

Валидация на клиенте - это те ошибки, которые мы можем проверить средствами браузера. Например,

- юзер пытается добавить товар с пустым названием
- юзер забыл указать цену товара
- юзер вбивает длинное описание, хотя допускается максимум 200 символов.

Все это можно проверить в клиентской части приложения с помощью javascript.

Валидация на сервере - это все то, что можно проверить на клиенте, плюс некоторые специфичные вещи, часто связанные с базой. Например,

- юзер пытается добавить уже существующий товар
- юзер пытается удалить категорию, хотя в ней есть какие-то товары
- юзер пытается удалить бренд, но у него нет прав доступа на это

Все эти вещи проверяются на бекенде, клиент о них не знает.

Отмечу 2 момента:

- стоит по максимуму валидировать данные на клиенте
- дублировать эти же проверки на сервере.

Зачем это нужно?

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

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

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


Валидация в нашей админке интернет-магазина

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

В нашей админке мы не будем стелить соломку по всему коду, смысл уроков не в этом. Мы подробно рассмотрим валидацию только в одном компоненте, но покроем и клиентские, и серверные проверки. И научимся показывать юзерам ошибки в человеческом виде.

Мы проработаем форму добавления брендов, которую сделали в прошлом уроке. Сейчас мы можем добавить любой бренд, потому что никаких проверок нет. Исправим это и будем проверять такие вещи:

- не пустой ли бренд
- не превышает ли бренд 20 символов (практического смысла мало, но для примера нормально)
- существует ли этот бренд в базе
- нет ли ошибки в запросе (неверный роутер, проходили в третьем уроке)
- общие ошибки, например, сервис недоступен, бекенд лежит

Первые 2 проверки будем делать на клиенте, следующие 2 - на сервере. Пятая - это все невошедшие ошибки, например, упавший ajax-запрос или 500-ка от сервера.

Дублировать клиентские проверки на сервере мы не будем. Они очень простые и легко реализуются на php, не будем перегружать код. Другие серверные ошибки, номер 3 и 4, уже реализованы в третьем уроке, когда мы готовили api для админки.


Общая схема обработки ошибок

Сделаем так. В окошке добавления бренда заведем поле, куда будем выводить сообщение об ошибке. Пользователям все равно, клиентская это ошибка или серверная, для него это будет выглядеть одинаково, вот так

Чтобы понимать, какая именно ошибка произошла, добавим такое понятие - код ошибки. Например, на скриншоте код "brand_exists", что означает, бренд уже существует. Это проверка серверная, но красное сообщение об этом не знает. Код ошибки - единственное, на что будет ориентироваться это сообщение и все равно, откуда он придет, при клиентской валидации или с ответом от сервера. Тексты сообщений мы будем брать из отдельного конфига. Теперь давайте сделаем это на практике.


Валидация на клиенте

Откроем компонент BrandsNew.vue и добавим новое поле errorCode, в раздел data. Это тот самый код, который определяет наличие ошибки. По умолчанию - пустая строка. То есть вот так

    data () {
        return {
            visible: false,
            newBrand: '',
            // Новое поле
            errorCode: ''
        }
    }

Добавим еще вычисляемое логическое поле isError, которое покажет, если ли в данный момент ошибка

    computed: {
        isError () {
            return this.errorCode !== '';
        }
    }

Дальше нам нужно вывести сообщение об ошибки в окне добавления бренда. Добавим такой код между инпутом-брендом и кнопкой Добавить

    Ошибка: {{errorCode}}

Это выделенный красным текст, пример взяли из библиотеки minicss. Напоминаю, наше приложение на этом css-фреймворке. Чуток поправим стили, добавим в style

    mark {
        display: block;
        margin: 5px 0 10px 5px;
    }

Но вернемся к разметке. v-if="isError" означает, что сообщение будем выводить только, когда есть ошибка, логично. А в тексте сообщения выведем пока код ошибки - {{errorCode}}. Сначала убедимся, что наша схема работает, а потом заменим бездушный errorCode нормальным текстом.

Итак, errorCode у нас есть, нужны методы для работы с ним. Один метод будет устанавливать код, другой очищать. Добавим в раздел methods

    setError (errorCode) {
        this.errorCode = errorCode;
    },
    clearError () {
        this.errorCode = '';
    }

Можно было обойтись и одним только setError, но позже увидим, что clearError удобная штука и нам еще пригодится.

Базовая подготовка сделана, пора заняться самой валидацией. Напомню, мы будем проверять, что новый бренд не пустой и что не длиннее 20 символов. Метод валидации будет выполнять эти проверки и в случае ошибкок устанавливать соответствующий код codeError. Вот так выглядит метод validate

    validate (brand) {
        if (brand === '') {
            this.setError('brand_empty');
            return false;
        }
        if (brand.length > 20) {
            this.setError('brand_long_title');
            return false;
        }
    
        // Если валидация прошла успешно
        this.clearError();
        return true;
    }

Два простых if-а, которые проставляют нужные коды brand_empty или brand_long_title. А в случае успеха дергаем метод clearError, чтобы сбросить ошибку, если она была до этого. Например, юзер ввел правильно инфу со второго раза.

Важный момент. Помимо установки codeError мы возвращаем true или false в зависимости от итога валидации. Нам это нужно, чтобы понять, отправлять ли запрос на сервер с добавлением бренда или же ждать, пока юзер введет бренд правильно.

Посмотрим, какой метод добавления бренда мы написали раньше

    addBrand () {
        this.$store.dispatch('brands/addBrand', this.newBrand);
        this.newBrand = '';
        this.closeModal();
    }

Все просто, вызываем действие через dispatch, очищаем инпут с названием и закрываем модалку. Этот код нужно чуть переделать, вызывать действие только если валидация прошла успешно. Добавим условие if

    addBrand () {
        if (this.validate(this.newBrand)) {
            this.$store.dispatch('brands/addBrand', this.newBrand);
            this.newBrand = '';
            this.closeModal();
        }
    }

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


Валидация на сервере

С серверными проверками есть свои особенности.

Во-первых, на клиенте мы генерили коды ошибок сами, а для серверных нужно дождаться ответа и получить их из респонса. Да, мы эти коды (brand_exists и invalid_router) сами сделали в уроке по API. Но вообще мы можем даже и не знать, что за коды нам возвращает бекенд.

Во-вторых, есть интерфейсный момент. Сейчас в методе addBrand сразу после dispatch мы очищаем поле бренда и закрываем модалку. Это годится, если бренд успешно добавлен и никуда не годится, если сервер вернул ошибку. Значит, мы должны не просто дернуть dispatch, а дождаться ответа от сервера и только потом решать, что делать. Если ответ 200 и бренд добавлен, то закрывать модалку, а если ошибка, то выводить ее и оставлять модалку.

Если бы мы писали приложение на jquery, то сделали бы примерно так

    $.ajax({
        url: '',
        data: '',
        success: function() {
            // очищаем поле бренда и закрываем модалку
        },
        error: function() {
            // парсим код ошибки и выводим красное сообщение в модалке
        }
    });

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

Начнем с действия addBrand. Открываем файл store/modules/brands.js и смотрим, как реализовано сейчас

    addBrand (context, newBrand) {
        const data = 'title=' + newBrand;

        axios
            .post('/admin/api/v1/brands', data)
            .then(response => {
                context.commit('ADD_BRAND', response.data)
            });
    }

Отправляем запрос на сервер через axios и в случае успеха вызываем мутацию ADD_BRAND. Нам нужно сделать то же самое, только при этом еще и возвращать промис. Вот так

    addBrand (context, newBrand) {
        const data = 'title=' + newBrand;

        return new Promise((resolve, reject) => {
            axios
                .post('/admin/api/v1/brands', data)
                .then(response => {
                    context.commit('ADD_BRAND', response.data);
                    resolve(response);
                }, error => {
                    reject(error);
                });
        });
    }

Это стандартная заготовка для промисов. resolve и reject - это функции, которые выполняются соответственно при успехе и ошибке. А что именно делают эти функции, будем решать уже в другом месте - опять в компоненте BrandsNew, в методе addBrand. Немного изменим код.

Было

    addBrand () {
        if (this.validate(this.newBrand)) {
            this.$store.dispatch('brands/addBrand', this.newBrand);
            this.newBrand = '';
            this.closeModal();
        }
    }

Стало

    addBrand () {
        if (this.validate(this.newBrand)) {
            this.$store.dispatch('brands/addBrand', this.newBrand).then(
                response => {
                    this.newBrand = '';
                    this.closeModal();
                },
                error => {
                    let resp = error.response;
                    let errorCode = (resp && resp.data && resp.data.code) ? resp.data.code : 'unknown_error';
                    this.setError(errorCode);
                });
        }
    }

Кода прибавилось, но немного. В метод resolve попадает

    response => {
        this.newBrand = '';
        this.closeModal();
    }

Параметр response нам не нужен, просто для наглядности. Получается, если запрос прошел успешно, бренд добавлен, то очищаем поле в модалке и саму модалку закрываем. При ошибке выполняется другое

    error => {
        let resp = error.response;
        let errorCode = (resp && resp.data && resp.data.code) ? resp.data.code : 'unknown_error';
        this.setError(errorCode);
    });

Здесь параметр error уже нужен - это ответ от сервера, который нужно распарсить и вытащить код ошибки. Разберем по порядку. В первой строке пишем в переменную resp ответ от сервера. Дальше идет диковатая конструкция resp && resp.data && resp.data.code. Зачем так сложно? Ведь мы возвращаем ответ с бекенда в виде { code: 'код ошибки', ... }, то есть поле code там будет всегда. Не совсем. Код будет, но только для тех случаев, когда наш бекенд нормально обработает ошибку. Например, найдет, что бренд уже существует, или невалидный роутер (ошибка в запросе, в урле). Если же бекенд просто лежит и вернет нам 500-ку, то в error.response может прийти null или пустая строка. Поэтому resp.data.code выкинет нам ошибку в консоль и поломает работу.

Поэтому именно здесь мы подстилаем соломку и проверяем всю цепочку error.response.data.code. Только если data.code действительно существует, то мы понимаем, что эту ошибку вернул наш бекенд и в errorCode мы записываем валидное значение. А в противном случае мы понимаем, что случилась какая-то хрень и поэтому ставим unknown_error. Типа "случилась непонятная хрень". Так и напишем пользователю.

Задача программиста как можно меньше пугать юзеров непонятными ошибками и стараться по максимуму обрабатывать их руками. Другое дело, что пользователям вообще по фигу, лежит ли у вас бекенд, упала ли база и кто-то накосячил в коде. Ему важно, что приложение не работает и все. Поэтому есть смысл не перегружать юзера техническими деталями, а выдать общую ошибку и попросить попробовать позже. Естественно, предполагается, что мы уже устраняем косяки и поднимаем бекенд.

Это все, что касается валидации на сервере. Можно проверять, попытаться добавить существующий бренд или изменить урл /admin/api/v1/brands на какой-нибудь левый. Если все сделали правильно, то при добавлении бренда будет уходить запрос на сервер, возвращаться ошибка и модалка не закроется, а выведет код.

Когда мы будем тестировать работу, то заметим одну визуальную неприятность. Допустим, попытались мы добавить пустой бренд, нам выскочила ошибка brand_empty. Изменяем бренд в инпуте, а ошибка так и торчит. Бесит. Давайте добавим одну строчку, чтобы при установке фокуса в инпуте ошибка пропадала. Теперь инпут будет выглядеть так

    

@focus="clearError" - вот и пригодился метод clearError!


Вывод нормального сообщения об ошибке вместо кода

Коды ошибок у нас есть, осталось задать им адекватные тексты. Можно сделать это и в компоненте BrandsNew, но мы поступим интереснее. Заведем в папке admin/vue/src папку configs, а в ней файлик brands.js. Давно было пора, потому что хорошее дело - выносить подобные вещи в конфиги, а не держать их в компонентах. В файлике-конфиге напишем так

    export default {
        errors: [{
            code: 'brand_empty',
            message: 'Бренд не может быть пустым'
        }, {
            code: 'brand_long_title',
            message: 'Название не должно превышать 20 символов'
        }, {
            code: 'brand_exists',
            message: 'Бренд с таким названием уже существует'
        }, {
            code: 'invalid_router',
            message: 'Ошибка запроса. Попробуйте еще раз'
        }, {
            code: 'unknown_error',
            message: 'Неизвестная ошибка. Попробуйте позже'
        }]
    }

Просто массив из двух полей: код ошибки и текст сообщения. Можно было обойтись объектом, чтобы не бегать по массиву. Но на практике такие простые конфиги часто разрастаются. То появится тултип с подробным описанием ошибки, то для каждой ошибки нужен будет свой цвет или еще какая-то фигня. Или вместо текстового кода мы решим использовать числовой. В общем, чтобы не приходилось переделывать структуру конфига, сразу будем использовать массив объектов - в него добавлять новые поля безопасней.

Идем обратно в компонент BrandsNew. В теге script импортируем lodash и конфиг брендов

    import _ from 'lodash';
    import config from '../configs/brands';

Создаем новое вычисляемое поле в computed

    errorMessage () {
        return this.errorCode !== ''
            ? _.find(config.errors, { code: this.errorCode }).message
            : '';
    }

Это и есть сообщение об ошибке, которое берется из конфига по нужному коду. Осталось воткнуть его в разметку вместо errorCode. Вот так

    {{errorMessage}}

Вот теперь все. У нас есть рабочая схема, по которой мы обрабатываем и клиентские, и серверные ошибки, используя один формат данных. Теперь чтобы добавить новую проверку, достаточно будет воткнуть ее в метод validate, если проверка клиентская, и в php-шный код, если серверная. Главное, вернуть с бекенда json формата { code: 'код ошибки', ... }. И добавить новый объект в конфиг, чтобы показывать адекватные тексты пользователям.

До встречи в следующих уроках.

Все уроки админки на vue.js

Предыдущая статья Следующая статья Исходники
Метки:
Заходите в группу в контакте - https://vk.com/webdevkin
Анонсы статей, обсуждения интернет-магазинов, vue, фронтенда, php, гита.
Истории из жизни айти и обсуждение кода.
Как Вам статья? Оцените!