Работа с Ajax запросами

Библиотека Awesomplete поддерживает работу с динамическими источниками данных. Вместо заранее подготовленного массива строк список подсказок может загружаться с сервера во время ввода текста. Такой подход особенно важен при работе с большими объёмами данных, поиском по базе пользователей, товарам, городам, тегам, статьям и другим сущностям, количество которых невозможно или нецелесообразно хранить в памяти браузера.

Ajax-запросы позволяют:

  • получать актуальные данные без перезагрузки страницы;
  • сокращать объём передаваемых данных;
  • реализовывать поиск в реальном времени;
  • подключать внешние API;
  • выполнять фильтрацию на стороне сервера;
  • уменьшать нагрузку на клиентское приложение.

Awesomplete не содержит встроенного Ajax-механизма. Библиотека отвечает только за отображение и управление списком подсказок. Загрузка данных реализуется вручную через:

  • fetch;
  • XMLHttpRequest;
  • axios;
  • jQuery.ajax;
  • любые другие HTTP-инструменты.

Базовая схема работы

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

  1. Пользователь вводит текст.
  2. Срабатывает событие input.
  3. Выполняется Ajax-запрос.
  4. Сервер возвращает массив данных.
  5. Полученный массив передаётся в awesomplete.list.
  6. Awesomplete обновляет выпадающий список.

Простейшая реализация через fetch

HTML

<input id="country-input">

JavaScript

const input = document.getElementById("country-input");

const awesomplete = new Awesomplete(input, {
    minChars: 2,
    maxItems: 10,
    autoFirst: true
});

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

    const query = this.value;

    if (query.length < 2) {
        awesomplete.list = [];
        return;
    }

    try {

        const response = await fetch(
            `/countries?q=${encodeURIComponent(query)}`
        );

        const data = await response.json();

        awesomplete.list = data;

    } catch (error) {

        console.error(error);

    }

});

Формат ответа сервера

Awesomplete ожидает массив элементов.

Пример JSON

[
    "Germany",
    "Georgia",
    "Greece"
]

После получения массива:

awesomplete.list = data;

список автоматически обновляется.


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

На практике сервер редко возвращает простые строки. Обычно используются объекты.

Ответ сервера

[
    {
        "id": 1,
        "name": "Germany"
    },
    {
        "id": 2,
        "name": "Georgia"
    }
]

Преобразование данных

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

Работа с label и value

Awesomplete поддерживает объекты специального формата.

Пример

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

Теперь:

  • label отображается в списке;
  • value используется как итоговое значение.

Кастомный select

Для более сложной логики используется переопределение replace.

Пример

const awesomplete = new Awesomplete(input, {

    replace(suggestion) {

        input.value = suggestion.label;

        input.dataset.id = suggestion.value;
    }

});

После выбора:

<input value="Germany" data-id="1">

Защита от слишком частых запросов

Без ограничений Ajax-запрос выполняется на каждый символ.

При быстром вводе это создаёт:

  • лишнюю нагрузку;
  • задержки;
  • переполнение очереди запросов;
  • проблемы производительности.

Debounce

Наиболее распространённое решение — debounce.

Пример debounce-функции

function debounce(callback, delay) {

    let timer;

    return function (...args) {

        clearTimeout(timer);

        timer = setTimeout(() => {
            callback.apply(this, args);
        }, delay);

    };

}

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

const search = debounce(async function () {

    const query = input.value;

    if (query.length < 2) {
        awesomplete.list = [];
        return;
    }

    const response = await fetch(
        `/countries?q=${encodeURIComponent(query)}`
    );

    const data = await response.json();

    awesomplete.list = data;

}, 300);

input.addEventListener("input", search);

Теперь запрос выполняется только через 300 мс после остановки ввода.


Минимальная длина запроса

Проверка количества символов обязательна.

Причины

  • защита сервера;
  • уменьшение нагрузки;
  • предотвращение бесполезных запросов;
  • ускорение интерфейса.

Пример

if (query.length < 3) {
    return;
}

Очистка списка при пустом вводе

Если поле очищено, подсказки также должны исчезнуть.

if (!query.trim()) {
    awesomplete.list = [];
}

Работа с асинхронностью

При быстром вводе возможна ситуация:

  1. Отправлен запрос "ge";
  2. Затем отправлен "ger";
  3. Первый ответ приходит позже второго;
  4. Старые данные затирают новые.

Это называется race condition.


Отмена предыдущего запроса

Современный способ решения — AbortController.

Пример

let controller;

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

    const query = this.value;

    if (controller) {
        controller.abort();
    }

    controller = new AbortController();

    try {

        const response = await fetch(
            `/countries?q=${query}`,
            {
                signal: controller.signal
            }
        );

        const data = await response.json();

        awesomplete.list = data;

    } catch (error) {

        if (error.name !== "AbortError") {
            console.error(error);
        }

    }

});

Индикатор загрузки

Во время выполнения Ajax-запроса желательно отображать состояние загрузки.

Пример

input.classList.add("loading");

После завершения:

input.classList.remove("loading");

Полный пример с загрузкой

let controller;

const awesomplete = new Awesomplete(input);

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

    const query = this.value.trim();

    if (query.length < 2) {
        awesomplete.list = [];
        return;
    }

    if (controller) {
        controller.abort();
    }

    controller = new AbortController();

    input.classList.add("loading");

    try {

        const response = await fetch(
            `/search?q=${encodeURIComponent(query)}`,
            {
                signal: controller.signal
            }
        );

        const data = await response.json();

        awesomplete.list = data;

    } catch (error) {

        if (error.name !== "AbortError") {
            console.error(error);
        }

    } finally {

        input.classList.remove("loading");

    }

});

Работа с axios

Многие приложения используют библиотеку Axios.

Пример

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

    const query = this.value;

    const response = await axios.get("/countries", {
        params: {
            q: query
        }
    });

    awesomplete.list = response.data;

});

Работа через jQuery.ajax

Пример

$("#country-input").on("input", function () {

    $.ajax({

        url: "/countries",

        data: {
            q: this.value
        },

        success(data) {

            awesomplete.list = data;

        }

    });

});

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

Запросы должны корректно кодировать пользовательский ввод.

Правильно

encodeURIComponent(query)

Неправильно

"/search?q=" + query

Без кодирования возникают ошибки при:

  • пробелах;
  • кириллице;
  • спецсимволах;
  • символах &, ?, =.

Обработка ошибок сервера

Ajax-запросы всегда должны иметь обработчик ошибок.

Пример

try {

    const response = await fetch(url);

    if (!response.ok) {
        throw new Error("Server error");
    }

} catch (error) {

    console.error(error);

}

Пустые результаты

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

[]

Awesomplete автоматически скроет список.

Иногда требуется собственное сообщение.

Пример

if (data.length === 0) {

    awesomplete.list = [
        "Ничего не найдено"
    ];

}

Кастомный item

Ajax-данные часто требуют сложного отображения.

Пример

const awesomplete = new Awesomplete(input, {

    item(item, inputValue) {

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

        element.innerHTML = `
            <strong>${item.name}</strong>
            <small>${item.code}</small>
        `;

        return element;
    }

});

Работа с HTML в подсказках

По умолчанию Awesomplete экранирует HTML.

Для ручного управления используется собственный item.

Пример ответа

[
    {
        "name": "Germany",
        "code": "DE"
    }
]

Отрисовка

item(item) {

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

    li.innerHTML = `
        <div class="country-row">
            <span>${item.name}</span>
            <span>${item.code}</span>
        </div>
    `;

    return li;
}

Подсветка совпадений

Awesomplete содержит встроенную функцию:

Awesomplete.ITEM

Можно комбинировать её с Ajax-данными.

Пример

item(item, input) {

    return Awesomplete.ITEM(
        item.name,
        input
    );

}

Кэширование результатов

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

Пример

const cache = {};

Использование кэша

if (cache[query]) {

    awesomplete.list = cache[query];
    return;

}

const response = await fetch(url);

const data = await response.json();

cache[query] = data;

awesomplete.list = data;

Серверная фильтрация

Наиболее эффективный подход — фильтрация на сервере.

Причины

  • экономия памяти;
  • уменьшение трафика;
  • высокая скорость;
  • отсутствие огромных массивов в браузере.

Клиентская фильтрация

Иногда сервер возвращает общий список.

Тогда фильтрацию выполняет Awesomplete.

Пример

const awesomplete = new Awesomplete(input, {

    filter(text, input) {

        return text.startsWith(input);
    }

});

Комбинирование Ajax и filter

awesomplete.list = serverData;

Затем:

filter(item, input) {

    return item.name.includes(input);
}

Работа с REST API

Awesomplete легко подключается к REST-сервисам.

Пример

fetch("/api/users?search=alex")

Типичный ответ

[
    {
        "id": 5,
        "username": "alex"
    }
]

Авторизация запросов

Иногда API требует токен.

Пример

fetch("/api/search", {

    headers: {
        Authorization: "Bearer TOKEN"
    }

});

Работа с POST-запросами

Не все поисковые API используют GET.

Пример

fetch("/search", {

    method: "POST",

    headers: {
        "Content-Type": "application/json"
    },

    body: JSON.stringify({
        query: input.value
    })

});

Lazy Loading

Иногда данные загружаются только при фокусе.

Пример

input.addEventListener("focus", async () => {

    if (awesomplete.list.length) {
        return;
    }

    const response = await fetch("/countries");

    awesomplete.list = await response.json();

});

Предзагрузка популярных значений

Часто комбинируются:

  • популярные элементы;
  • Ajax-поиск;
  • локальный кэш.

Пример

awesomplete.list = [
    "JavaScript",
    "Python",
    "PHP"
];

После ввода:

fetch(...)

Защита от дублирования

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

Удаление дублей

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

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

Перед передачей в Awesomplete данные часто преобразуются.

Пример

const prepared = data.map(item => ({
    label: item.title.trim(),
    value: item.id
}));

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

Не следует отображать сотни подсказок.

Ограничение

maxItems: 8

Оптимизация производительности

При работе с Ajax важно учитывать:

  • debounce;
  • отмену запросов;
  • кэширование;
  • минимальную длину поиска;
  • серверную фильтрацию;
  • ограничение результатов;
  • сжатие JSON;
  • пагинацию.

Пагинация

Некоторые API возвращают данные порциями.

Пример

fetch(`/search?q=${query}&page=1`)

Awesomplete обычно отображает только первую страницу.


Безопасность

Никогда нельзя вставлять HTML из Ajax-ответов без проверки.

Опасный код:

li.innerHTML = item.html;

Если сервер вернёт вредоносный JavaScript, возможен XSS.


Безопасное отображение

li.textContent = item.name;

Использование async/await

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

Без async/await

fetch(url)
    .then(response => response.json())
    .then(data => {
        awesomplete.list = data;
    });

С async/await

const response = await fetch(url);

const data = await response.json();

awesomplete.list = data;

Архитектурное разделение

Крупные приложения обычно разделяют:

  • UI;
  • сетевой слой;
  • обработку данных;
  • конфигурацию Awesomplete.

Пример

async function searchCountries(query) {

    const response = await fetch(
        `/countries?q=${query}`
    );

    return response.json();

}
input.addEventListener("input", async () => {

    const data = await searchCountries(
        input.value
    );

    awesomplete.list = data;

});

Работа с несколькими полями

Каждое поле может иметь собственный Ajax-источник.

Пример

createAutocomplete(
    "#countries",
    "/api/countries"
);

createAutocomplete(
    "#cities",
    "/api/cities"
);

Универсальная функция

function createAutocomplete(selector, url) {

    const input = document.querySelector(selector);

    const awesomplete = new Awesomplete(input);

    input.addEventListener("input", async () => {

        const response = await fetch(
            `${url}?q=${input.value}`
        );

        awesomplete.list = await response.json();

    });

}