Выполнение HTTP-запросов с помощью Axios в TypeScript

Выполнение HTTP-запросов с помощью Axios в TypeScript

20 октября 2022 г.

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

Итак, начнем. Во-первых, нам нужно создать сервис Axios. Все наши сервисы в текущем проекте основаны на классах, поэтому нам нужно создать класс AxiosService. Существует два основных подхода к использованию Axios: напрямую использовать объект axios из импорта или создать новый экземпляр с помощью axios.create. Мы будем использовать последний. Сервис будет иметь 2 поля: экземпляр, который имеет тип AxiosInstance и cancelToken, который имеет тип CancelTokenStatic:

class AxiosService {
    instance: AxiosInstance;
    cancelTokenStatic: CancelTokenStatic;
}

Затем в конструкторе мы создаем axiosInstanse:

constructor(url: string) {
    this.instance = axios.create({
        headers: {
            Accept: 'application/json',
            'Content-Type': 'application/json',
        },
        baseURL: url
    });
    this.cancelTokenStatic = axios.CancelToken;
}

Функция axios.create может принимать объект с параметрами. В нашем случае мы установили поле headers и сказали, что наш экземпляр будет работать с json и предоставить базовый URL-адрес, который будет использоваться для каждого запроса. axios.CancelToken обеспечивает логику отмены запросов, поэтому мы также сохраняем ее

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

setBaseURL(url: string): void {
    this.instance.defaults.baseURL = url;
}

Полная версия класса AxiosService:

import axios, { AxiosInstance, CancelTokenStatic } from 'axios';

class AxiosService {
    instance: AxiosInstance;
    cancelTokenStatic: CancelTokenStatic;

    constructor(url: string) {
        this.instance = axios.create({
            transformRequest: [
                (data, headers): string => {
                    headers['Authorization'] = `Bearer ${authService.getToken()}`;
                    return JSON.stringify(data);
                },
            ],
            headers: {
                Accept: 'application/json',
                'Content-Type': 'application/json',
            },
            baseURL: url,
        });
        this.cancelTokenStatic = axios.CancelToken;
    }

    setBaseURL(url: string): void {
        this.instance.defaults.baseURL = url;
    }
}

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

HttpService будет иметь 3 поля:

class HttpService {
    private axiosInstance: AxiosInstance;
    private sourceMap: { [key: string]: CancelTokenSource[] } = {};
    private cancelToken: CancelTokenStatic;
}

axiosInstance и cancelToken мы установим в конструкторе, sourceMap будет нашим хранилищем, где мы будем хранить токены отмены для запросов

constructor(instance: AxiosInstance, cancelToken: CancelTokenStatic) {
    this.axiosInstance = instance;
    this.cancelToken = cancelToken;
}

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

private createCancelToken = (cancelKey: string): CancelToken => {
    const source = this.cancelToken.source();

    if (this.sourceMap[cancelKey]) {
        this.sourceMap[cancelKey].push(source);
    } else {
        this.sourceMap[cancelKey] = [source];
    }

    return source.token;
};

Приватная функция createCancelToken сначала создает новый источник токена отмены. Затем мы проверяем, есть ли в нашем sourceMap поле с ключом cancelKey, затем мы просто добавляем новый cancelSource в массив или создаем новое поле в sourceMap, а источник является первым элементом в массиве.

Следующим шагом является создание метода cancelPreviousRequests.

cancelPreviousRequests = (cancelKey: string): void => {
    const requests = this.sourceMap[cancelKey] || [];

    requests.forEach((item) => {
        item.cancel('cancel');
    });

    this.sourceMap[cancelKey] = [];
};

Мы берем cancelKey из аргумента и находим такое поле в sourceMap. Затем мы перебираем в нем каждый CancelTokenSource и вызываем функцию cancel. А затем удалите целевое поле в sourceMap, сохранив в нем пустой массив.

Теперь мы можем начать с логики запросов. Мы будем использовать общий метод sendRequest для всех запросов. Этот метод будет принимать некоторые параметры. Определим для них интерфейс:

interface RequestOptions {
    url: string; // url for request
    method?: Method; // HTTP method
    params?: Dictionary<any> | string; // request params
    cancelKey?: string; // key for sourceMap if request can be canceled
  responseType?: ResponseType; // type for response
}

И метод sendRequest, который вернет Promise:

sendRequest<T = unknown>({
    url,
    method = 'GET',
    params,
    cancelKey,
    responseType,
  }: RequestOptions): Promise<AxiosResponse<T> | void> {
    let cancelToken;

    if (cancelKey) {
        cancelToken = this.createCancelToken(cancelKey);
    }

    switch (method) {
        case 'POST':
            return this.postRequest({ url, data: params, cancelToken });

        default:
            return this.getRequest({
                url,
                cancelToken,
                responseType,
            });
    }
}

Мы используем универсальный для возвращаемого типа для ответа и метод, который мы установили на GET по умолчанию. Во-первых, мы проверяем, есть ли у нас cancelKey, затем нам нужно создать cancelToken и отправить его в sourceMap. Затем мы используем конструкцию switch для перебора методов запроса и вызова соответствующих функций.

Функция запроса также будет принимать некоторые параметры, поэтому нам нужно определить для них интерфейс:

type RequestParams = {
    url: string;
    cancelToken?: CancelToken;
    data?: Dictionary<any> | string;
};

У нас есть 2 базовых метода HTTP для запросов POST и GET:

private async postRequest({
    url,
    data,
    cancelToken,
}: RequestParams): Promise<AxiosResponse | void> {
    try {
        return await this.axiosInstance.post(url, data, { cancelToken });
    } catch (error) {
      return Promise.reject(error);
    }
}

private async getRequest({
    url,
    cancelToken,
}: RequestParams): Promise<AxiosResponse | void> {
    try {
        return await this.axiosInstance.get(url, { cancelToken });
    } catch (error) {
        return Promise.reject(error);
    }
}

Здесь нет ничего особенного. Мы просто вызываем соответствующий запрос из axiosInstance с нашими параметрами

Полный код для HttpService:

import {
    AxiosResponse,
    Method,
    CancelTokenSource,
    CancelToken,
    AxiosInstance,
    CancelTokenStatic,
} from 'axios';
import { axiosService } from './AxiosService';

interface RequestOptions {
    url: string;
    method?: Method;
    params?: Dictionary<any> | string;
    cancelKey?: string;
}

interface RequestParams {
    url: string;
    cancelToken?: CancelToken;
    data?: Dictionary<any> | string;
}

class HttpService {
    private axiosInstance: AxiosInstance;
    private sourceMap: { [key: string]: CancelTokenSource[] } = {};
    private cancelToken: CancelTokenStatic;

    constructor(instance: AxiosInstance, cancelToken: CancelTokenStatic) {
        this.axiosInstance = instance;
        this.cancelToken = cancelToken;
    }

    private createCancelToken = (cancelKey: string): CancelToken => {
        const source = this.cancelToken.source();
        if (this.sourceMap[cancelKey]) {
            this.sourceMap[cancelKey].push(source);
        } else {
            this.sourceMap[cancelKey] = [source];
        }
        return source.token;
    };

    cancelPreviousRequests = (cancelKey: string): void => {
        const requests = this.sourceMap[cancelKey] || [];
        requests.forEach((item) => {
            item.cancel('cancel');
        });
        this.sourceMap[cancelKey] = [];
    };

    private async postRequest({
        url,
        data,
        cancelToken,
    }: RequestParams): Promise<AxiosResponse | void> {
        try {
            return await this.axiosInstance.post(url, data, { cancelToken });
        } catch (error) {
            return Promise.reject(error);
        }
    }

    private async getRequest({ url, cancelToken }: RequestParams): Promise<AxiosResponse | void> {
        try {
            return await this.axiosInstance.get(url, { cancelToken });
        } catch (error) {
            return Promise.reject(error);
        }
    }

    sendRequest<T = unknown>({
        url,
        method = 'GET',
        params,
        cancelKey,
    }: RequestOptions): Promise<AxiosResponse<T> | void> {
        let cancelToken;

        if (cancelKey) {
            cancelToken = this.createCancelToken(cancelKey);
        }

        switch (method) {
            case 'POST':
                return this.postRequest({ url, data: params, cancelToken });

            default:
                return this.getRequest({
                    url,
                    cancelToken,
                });
      }
}

Пример использования:

httpService.cancelPreviousRequests('testRequest');
httpService
    .sendRequest<string[]>({
        url: '/api/testurl',
        cancelKey: 'testRequest',
    })
    .then((response) => {
        if (response?.data) {
            console.log(response.data)
        }
    })
    .catch((error) => {
        console.warn(error);
    });


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE