Обработка JSON ответов

При интеграции библиотеки Awesomplete с удалёнными источниками данных наиболее распространённым форматом обмена становится JSON. Сервер возвращает структурированный ответ, содержащий массив строк, объектов или сложных вложенных структур, а клиентская часть преобразует эти данные в список подсказок автодополнения.

Awesomplete не выполняет автоматическую обработку JSON-ответов. Полученные данные необходимо самостоятельно разобрать, преобразовать и передать в свойство list.

Базовая схема работы выглядит следующим образом:

  1. Пользователь вводит текст.
  2. Выполняется AJAX-запрос.
  3. Сервер возвращает JSON.
  4. JSON преобразуется в массив.
  5. Массив передаётся в Awesomplete.
  6. Отображается список подсказок.

Простейший JSON-массив строк

Наиболее простой вариант ответа сервера:

[
    "JavaScript",
    "Java",
    "Python",
    "PHP",
    "Rust"
]

Такой формат идеально подходит для Awesomplete, поскольку библиотека умеет работать напрямую с массивами строк.

Обработка ответа

fetch("/languages")
    .then(response => response.json())
    .then(data => {
        awesomplete.list = data;
    });

Что происходит в коде

Метод:

response.json()

автоматически преобразует JSON-строку:

["JavaScript","Java"]

в полноценный JavaScript-массив:

["JavaScript", "Java"]

После этого массив передаётся в:

awesomplete.list

JSON с объектом-обёрткой

Часто сервер возвращает не массив напрямую, а объект с дополнительными полями:

{
    "success": true,
    "items": [
        "JavaScript",
        "Java",
        "Python"
    ]
}

В этом случае необходимо извлечь нужное поле.

Пример обработки

fetch("/languages")
    .then(response => response.json())
    .then(data => {
        awesomplete.list = data.items;
    });

Причины использования обёртки

Подобная структура позволяет серверу передавать:

  • статус выполнения;
  • сообщения об ошибках;
  • пагинацию;
  • количество элементов;
  • дополнительные метаданные.

Например:

{
    "success": true,
    "count": 3,
    "items": [
        "JavaScript",
        "Java",
        "Python"
    ]
}

JSON-массив объектов

В реальных приложениях сервер обычно возвращает не строки, а объекты.

Пример:

[
    {
        "id": 1,
        "name": "JavaScript"
    },
    {
        "id": 2,
        "name": "Python"
    }
]

Awesomplete не знает, какое поле отображать, поэтому данные нужно преобразовать.


Преобразование объектов в строки

Самый простой способ — извлечь одно поле.

Использование map()

fetch("/languages")
    .then(response => response.json())
    .then(data => {

        const list = data.map(item => item.name);

        awesomplete.list = list;
    });

Результат преобразования

Из:

[
    { id: 1, name: "JavaScript" },
    { id: 2, name: "Python" }
]

получается:

[
    "JavaScript",
    "Python"
]

Сохранение оригинальных объектов

Иногда требуется не только показать текст, но и сохранить дополнительную информацию:

  • идентификатор;
  • категорию;
  • URL;
  • код;
  • описание.

В таком случае строки недостаточно.

Использование массива объектов Awesomplete

Awesomplete поддерживает следующий формат:

[
    {
        label: "JavaScript",
        value: "1"
    },
    {
        label: "Python",
        value: "2"
    }
]

Преобразование JSON

fetch("/languages")
    .then(response => response.json())
    .then(data => {

        const list = data.map(item => ({
            label: item.name,
            value: item.id
        }));

        awesomplete.list = list;
    });

Разница между label и value

Поле label

Отвечает за отображаемый текст.

label: "JavaScript"

Поле value

Передаётся в input после выбора элемента.

value: "1"

Отображение текста вместо ID

Если в value хранится идентификатор, после выбора пользователь увидит число:

<input value="1">

Во многих случаях это нежелательно.

Решение через replace()

new Awesomplete(input, {

    replace: function(selected) {
        this.input.value = selected.label;
    }
});

Работа со сложным JSON

Серверные API часто возвращают вложенные структуры.

Пример:

{
    "data": {
        "results": [
            {
                "id": 1,
                "attributes": {
                    "title": "JavaScript"
                }
            },
            {
                "id": 2,
                "attributes": {
                    "title": "Python"
                }
            }
        ]
    }
}

Извлечение вложенных данных

fetch("/api/search")
    .then(response => response.json())
    .then(data => {

        const results = data.data.results;

        const list = results.map(item => ({
            label: item.attributes.title,
            value: item.id
        }));

        awesomplete.list = list;
    });

Проверка существования полей

При работе с удалённым API структура ответа может изменяться. Отсутствие нужного поля вызывает ошибки.

Проблемный код:

data.results.map(...)

Если results отсутствует:

Cannot read properties of undefined

Безопасная проверка данных

Проверка через if

fetch("/api/search")
    .then(response => response.json())
    .then(data => {

        if (!data.results) {
            return;
        }

        awesomplete.list = data.results;
    });

Использование optional chaining

Современный JavaScript позволяет безопасно обращаться к вложенным свойствам.

const results = data?.data?.results;

Полный пример

fetch("/api/search")
    .then(response => response.json())
    .then(data => {

        const results = data?.data?.results;

        if (!results) {
            return;
        }

        awesomplete.list = results;
    });

Проверка типа данных

Иногда сервер возвращает не массив, а объект или строку.

Проверка через Array.isArray()

fetch("/api/search")
    .then(response => response.json())
    .then(data => {

        if (!Array.isArray(data)) {
            return;
        }

        awesomplete.list = data;
    });

Обработка пустого JSON

Сервер может вернуть пустой массив:

[]

Awesomplete корректно обрабатывает такую ситуацию.

awesomplete.list = [];

Список подсказок просто не отображается.


Фильтрация JSON перед выводом

Перед передачей данных в Awesomplete их можно фильтровать.

Пример удаления неактивных элементов

[
    {
        "name": "JavaScript",
        "active": true
    },
    {
        "name": "Old Language",
        "active": false
    }
]

Обработка

fetch("/languages")
    .then(response => response.json())
    .then(data => {

        const filtered = data
            .filter(item => item.active)
            .map(item => item.name);

        awesomplete.list = filtered;
    });

Сортировка JSON-данных

Awesomplete может отображать данные в том порядке, в котором они были получены.

Сортировка по алфавиту

const sorted = data.sort((a, b) => {
    return a.name.localeCompare(b.name);
});

Полный пример

fetch("/languages")
    .then(response => response.json())
    .then(data => {

        const sorted = data
            .sort((a, b) => a.name.localeCompare(b.name))
            .map(item => item.name);

        awesomplete.list = sorted;
    });

Ограничение количества результатов

Некоторые API возвращают тысячи записей.

Перед передачей данных в Awesomplete желательно ограничить размер списка.

Использование slice()

const limited = data.slice(0, 10);

Полный пример

fetch("/search")
    .then(response => response.json())
    .then(data => {

        const list = data
            .slice(0, 10)
            .map(item => item.name);

        awesomplete.list = list;
    });

Преобразование JSON через reduce()

Иногда требуется сложная логика обработки.

Пример

const list = data.reduce((result, item) => {

    if (item.active) {

        result.push({
            label: item.name,
            value: item.id
        });
    }

    return result;

}, []);

Обработка ошибок JSON

Ошибка может возникнуть:

  • при недоступности сервера;
  • при неверном JSON;
  • при сетевом сбое;
  • при ошибке авторизации.

Использование catch()

fetch("/search")
    .then(response => response.json())
    .then(data => {

        awesomplete.list = data;
    })
    .catch(error => {

        console.error(error);

        awesomplete.list = [];
    });

Ошибка некорректного JSON

Если сервер возвращает:

Internal Server Error

вместо JSON, метод:

response.json()

выбросит исключение.

Поэтому обработка ошибок обязательна.


Проверка HTTP-статуса

Даже при ошибке fetch() не всегда вызывает catch().

Например:

404 Not Found

не считается сетевой ошибкой.


Правильная проверка response.ok

fetch("/search")

    .then(response => {

        if (!response.ok) {
            throw new Error("Ошибка сервера");
        }

        return response.json();
    })

    .then(data => {

        awesomplete.list = data;
    })

    .catch(error => {

        console.error(error);
    });

Асинхронная обработка через async/await

Современный синтаксис делает код более читаемым.

Пример

async function loadSuggestions(query) {

    try {

        const response = await fetch("/search?q=" + query);

        if (!response.ok) {
            throw new Error("Ошибка запроса");
        }

        const data = await response.json();

        awesomplete.list = data;

    } catch (error) {

        console.error(error);
    }
}

Обработка Unicode и UTF-8

JSON-ответы могут содержать:

  • кириллицу;
  • иероглифы;
  • специальные символы;
  • emoji.

Пример:

[
    "Москва",
    "Караганда",
    "Алматы"
]

Awesomplete корректно работает с Unicode при условии правильной кодировки сервера:

Content-Type: application/json; charset=utf-8

Нормализация данных

Иногда API возвращает данные с лишними пробелами.

Очистка строк

const list = data.map(item => item.trim());

Преобразование регистра

Пример

const list = data.map(item => item.toLowerCase());

Удаление дубликатов

API может возвращать повторяющиеся значения.

Использование Set

const unique = [...new Set(data)];

Полный пример

fetch("/search")
    .then(response => response.json())
    .then(data => {

        const unique = [...new Set(data)];

        awesomplete.list = unique;
    });

Комбинирование нескольких полей

Иногда подсказка должна содержать несколько значений.

Исходный JSON

[
    {
        "city": "Караганда",
        "country": "Казахстан"
    },
    {
        "city": "Москва",
        "country": "Россия"
    }
]

Формирование строки

const list = data.map(item => {
    return `${item.city} (${item.country})`;
});

Использование кастомного item()

JSON можно отображать в виде сложной HTML-разметки.

Пример

new Awesomplete(input, {

    item: function(text, input) {

        const element = document.createElement("li");

        element.innerHTML = `
            <strong>${text.label}</strong>
            <small>${text.country}</small>
        `;

        return element;
    }
});

Формирование JSON

const list = data.map(item => ({
    label: item.city,
    country: item.country
}));

Обработка JSON с задержкой

Некоторые API отвечают медленно. Если пользователь быстро вводит текст, запросы начинают накладываться друг на друга.

Проблема устаревших ответов

Пользователь ввёл:

jav

а затем:

javascript

Первый запрос может завершиться позже второго и перезаписать актуальные данные.


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

let currentQuery = "";

input.addEventListener("input", async function() {

    currentQuery = input.value;

    const query = currentQuery;

    const response = await fetch("/search?q=" + query);

    const data = await response.json();

    if (query !== currentQuery) {
        return;
    }

    awesomplete.list = data;
});

Кэширование JSON-ответов

Повторные запросы можно сохранять локально.

Пример

const cache = {};

async function load(query) {

    if (cache[query]) {

        awesomplete.list = cache[query];
        return;
    }

    const response = await fetch("/search?q=" + query);

    const data = await response.json();

    cache[query] = data;

    awesomplete.list = data;
}

Преобразование серверных ошибок в JSON

Некоторые API всегда возвращают JSON даже при ошибках.

Пример:

{
    "success": false,
    "message": "Access denied"
}

Проверка success

fetch("/search")
    .then(response => response.json())
    .then(data => {

        if (!data.success) {

            console.error(data.message);
            return;
        }

        awesomplete.list = data.items;
    });

Подготовка данных перед отображением

JSON желательно приводить к единому формату до передачи в Awesomplete.

Оптимальная последовательность обработки:

  1. Проверка ошибок.
  2. Проверка структуры.
  3. Извлечение массива.
  4. Фильтрация.
  5. Сортировка.
  6. Удаление дубликатов.
  7. Ограничение размера.
  8. Преобразование структуры.
  9. Передача в awesomplete.list.

Универсальная функция обработки JSON

async function loadSuggestions(url) {

    try {

        const response = await fetch(url);

        if (!response.ok) {
            throw new Error("Ошибка сервера");
        }

        const data = await response.json();

        if (!Array.isArray(data.items)) {
            return [];
        }

        return data.items

            .filter(item => item.active)

            .sort((a, b) => {
                return a.name.localeCompare(b.name);
            })

            .slice(0, 10)

            .map(item => ({
                label: item.name,
                value: item.id
            }));

    } catch (error) {

        console.error(error);

        return [];
    }
}