Работа с внешними API

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

Ключевая особенность архитектуры заключается в разделении ответственности:

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

Получение данных из внешнего источника

Типовой сценарий основан на использовании fetch для обращения к REST API. Запрос формируется на основе текущего значения input-поля, которое отслеживается через событие input.

const input = document.querySelector("#search");
const awesomplete = new Awesomplete(input, {
    minChars: 2,
    maxItems: 10
});

Далее подключается обработка ввода:

input.addEventListener("input", async () => {
    const query = input.value.trim();

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

    const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
    const data = await response.json();

    awesomplete.list = data.results;
});

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


Дебаунсинг запросов к API

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

function debounce(fn, delay) {
    let timer;
    return (...args) => {
        clearTimeout(timer);
        timer = setTimeout(() => fn(...args), delay);
    };
}

Интеграция с Awesomplete:

const fetchSuggestions = debounce(async (query) => {
    const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
    const data = await response.json();
    awesomplete.list = data.results;
}, 300);

input.addEventListener("input", () => {
    const query = input.value.trim();

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

    fetchSuggestions(query);
});

Дебаунсинг снижает количество запросов и стабилизирует поведение интерфейса при быстром вводе.


Обработка гонок запросов (race conditions)

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

Решение основано на использовании идентификатора запроса:

let requestId = 0;

const fetchSuggestions = debounce(async (query) => {
    const currentId = ++requestId;

    const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
    const data = await response.json();

    if (currentId !== requestId) return;

    awesomplete.list = data.results;
}, 300);

Такой подход гарантирует, что в интерфейс попадают только актуальные результаты.


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

Для повторяющихся запросов используется кэш на стороне клиента. Это уменьшает задержки и снижает нагрузку на API.

const cache = new Map();

async function getSuggestions(query) {
    if (cache.has(query)) {
        return cache.get(query);
    }

    const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
    const data = await response.json();

    cache.set(query, data.results);
    return data.results;
}

Интеграция с Awesomplete:

const fetchSuggestions = debounce(async (query) => {
    const results = await getSuggestions(query);
    awesomplete.list = results;
}, 300);

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


Преобразование данных API в формат Awesomplete

Внешние API часто возвращают сложные объекты, тогда как Awesomplete ожидает массив строк или объектов с полями label и value.

Пример нормализации:

function normalize(data) {
    return data.map(item => ({
        label: item.title,
        value: item.id
    }));
}

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

const data = await response.json();
awesomplete.list = normalize(data.results);

При выборе сложных объектов важно учитывать, что Awesomplete отображает label, а в input подставляет value.


Обработка ошибок и fallback-логика

При нестабильной сети или ошибках API необходимо предотвращать поломку интерфейса.

async function getSuggestions(query) {
    try {
        const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);

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

        const data = await response.json();
        return data.results;
    } catch (error) {
        return [];
    }
}

Дополнительно может использоваться fallback на локальные данные:

const fallback = ["JavaScript", "Java", "Python", "PHP"];

Работа с пагинацией и расширяемыми результатами

Некоторые API возвращают данные постранично. В этом случае подсказки дополняются динамически.

let page = 1;
let currentQuery = "";

async function loadPage(query, page) {
    const response = await fetch(`/api/search?q=${query}&page=${page}`);
    return response.json();
}

При необходимости расширения списка:

const data = await loadPage(currentQuery, page);
awesomplete.list = [
    ...awesomplete.list,
    ...data.results
];
page++;

Такой подход используется для сценариев с бесконечной выдачей подсказок.


Ограничение частоты запросов и защита API

Помимо debounce часто применяется rate limiting на клиенте, особенно при работе с дорогими API.

let lastCall = 0;

function rateLimit(fn, limit) {
    return (...args) => {
        const now = Date.now();
        if (now - lastCall < limit) return;

        lastCall = now;
        fn(...args);
    };
}

Комбинация debounce и rate limit снижает риск перегрузки сервера и блокировки ключей API.


Работа с CORS и серверными прокси

В браузерной среде запросы к внешним API ограничиваются политикой CORS. При отсутствии разрешающих заголовков используется серверный прокси.

Клиентская часть:

fetch(`/proxy/search?q=${encodeURIComponent(query)}`)

Серверная часть (пример на Node.js):

app.get("/proxy/search", async (req, res) => {
    const query = req.query.q;

    const response = await fetch(`https://external-api.com/search?q=${query}`);
    const data = await response.json();

    res.json(data);
});

Проксирование позволяет централизовать кэширование, логирование и контроль доступа.


Полноценная интеграционная схема Awesomplete с API

const input = document.querySelector("#search");

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

const cache = new Map();
let requestId = 0;

async function getSuggestions(query) {
    if (cache.has(query)) return cache.get(query);

    const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
    const data = await response.json();

    cache.set(query, data.results);
    return data.results;
}

const fetchSuggestions = debounce(async (query) => {
    const currentId = ++requestId;

    const results = await getSuggestions(query);

    if (currentId !== requestId) return;

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

input.addEventListener("input", () => {
    const query = input.value.trim();

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

    fetchSuggestions(query);
});

Такая структура объединяет асинхронную загрузку, защиту от гонок, кэширование и нормализацию данных, сохраняя Awesomplete в роли лёгкого UI-слоя для отображения подсказок.