Заранее прошу прощения - получился некоторый лонгрид, и, возможно, решение простое, но мне пришлось подробно описать контекст и привести примеры своей реализации.
Есть приложение, построенное на микрофронтендах. Каждое используемое в нем микроприложение, будь то вью или реакт, раньше общалось с бэком через кастомную либу, использующую
NGSI (что в принципе не суть важно, но важно понимать, что у NGSI есть свои конкретные методы для работы с бэком).
Теперь понадобилось дать микроприложениям возможность работать через разные api - т.е. в каких-то случаях это может оказаться NGSI, в каких-то - Axios, ну или мало ли что еще.
Я решил сделать посредническую либу, что-то типа паттерна "репозиторий", которая имеет точку входа, и в зависимости от переданных пропсов, инстанцирует тот или иной класс для работы с api. Выглядит примерно так
1. Есть файл базового репозитория. Это абстрактный класс, от которого будут экстендиться конкретные репозитории
// File BaseRepository.ts
export abstract class BaseRepository {
readonly defaultErrorMessage = "Method not implemented."
entity(payload: IEntityPayload) {
throw new Error(this.defaultErrorMessage);
}
list(payload: IListPayload) {
throw new Error(this.defaultErrorMessage);
}
// В реальности методов гораздо больше. Какие-то из них могут быть
// лишь в одном репозитории и отсутствовать в другом.
// Но большинство методов есть и там, и там.
// Но наверное, тут, в абстрактном классе нужно перечислить их все,
// даже если в каком-то репозитории они останутся нереализованными?
}
2. Есть файлы самих репозиториев
// File ApiRepo1.ts
export class ApiRepo1 extends BaseRepository {
readonly api
constructor(config: AxiosRequestConfig) {
super();
this.api = axios.create(config)
}
async list({ someQueryKey }: IListPayload) {
// api.get - обычный метод get у Axios
// apiURLs - это Map с урлами запросов
return await this.api.get(String(apiURLs.get(someQueryKey)))
}
async entity({ someQueryKey }: IEntityPayload) {
return await this.api.get(String(apiURLs.get(someQueryKey)))
}
}
// File ApiRepo2.ts
export class ApiRepo2 extends BaseRepository {
async list(payload: IListPayload) {
// listEntities, как и getEntity ниже - это методы NGSI, предварительно импортированные
return await listEntities(payload);
}
async entity(payload: IEntityPayload) {
return await getEntity(payload);
}
}
3. Далее в индексном файле либы я импортирую все эти репозитории и создаю точку для подключения:
import { ApiRepo1 } from "./ApiRepo1";
import { ApiRepo2 } from "./ApiRepo2";
type TRepoLib = {
name: string
config: {
[index: string]: string | boolean | number
}
}
const repoMap = new Map<string, any>([
["api-repo-1", ApiRepo1],
["api-repo-2", ApiRepo2]
]);
export {
type TRepoLib,
repoMap
}
При использовании в микроприложении, на основе полученных им пропсов, я инициализирую для работы тот или иной репозиторий:
import { repoMap } from '@lib-api-repo'
// lib - объект из пропсов. Его может и не быть, тогда передается дефолтное значение
const APILib = repoMap.get(lib?.name || 'api-repo-2')
const api = new APILib(lib?.config)
Это еще довольно сырая версия, но основная идея, думаю, понятна.
Тут возникает ряд вопросов, касающихся типизации:
1. Как правильней избавиться от any в этой конструкции? Или, может, заменить эту конструкцию чем-то совсем другим?
const repoMap = new Map<string, any>([
["api-repo-1", ApiRepo1],
["api-repo-2", ApiRepo2]
])
Тут надо учесть, что по факту в двух репозиториях какие-то методы пересекаются, а какие-то объединяются (я имею в виду union и intersection).
Поэтому, скажем, в таком виде было бы некорректно, поскольку часть методов просто выпадет из списка типов.
type TRepoUnion = typeof ApiRepo1 | typeof ApiRepo2
const repositoriesMap = new Map<string, TRepoUnion>()
Равно как и intersection тут не прокатит - сразу посыпятся ошибки.
Использовать тут дженерик не получится, как мне кажется, т.к., собственно, откуда он возьмется - ведь в пропсы невозможно передать тип, а микроприложение понятия не имеет, с каким репозиторием ему предстоит работать. Оно знает только имя.
2. Как типизировать response внутри микроприложения. Но тут попроще - по сути, независимо от репозитория, который используется, респонсы должны быть одинаковыми, т.е. их нужно в самой либе приводить к некоему унифицированному виду. Иначе, понятно, невозможно будет пользоваться. Но опять же, пока в Map не определен тип репозитория - респонс будет тупо any.
3. Ну и в целом, буду рад каким-то прочим советам и альтернативным взглядам на реализацию подобной задачи.