js-request-manager
Version:
JS Request Manager is a library for creating an sdk library for your server (API). The API type doesn't matter, whether it's REST or RPC
688 lines (564 loc) • 24.9 kB
Markdown
# Менеджер запросов к API.
Request Manager - это библиотека для создания sdk библиотеки для вашего сервера (API).
Тип API не имеет значения, будь то REST или RPC.
В итоге мы получаем возможность централизованно обрабатывать все запросы.
Мы не заморачиваемся с преобразованием данных в коде, а лишь работаем с чистыми данными.
Лучше 1 раз увидеть, чем 100 раз услышать.
[Documentation in English](readme.md)
# Как это выглядит после настройки
### async/await
```js
try {
let responseData = await RequestManager.Auth.authorization({login: 'admin', password: 'pass'})
// ... user code for success response
} catch (err) {
// ... user code for error response or server error
return;
}
```
### Promise
```js
RequestManager.Auth.authorization({login: 'admin', password: 'pass'}).then(
(result) => { /* ... user code for success response */ },
(error) => { /* ... user code for error response or server error */ }
);
```
# Установка
<details open>
<summary><b style="font-size: 1.3em;">NPM</b></summary>
```shell
# # install a request client, such as axios or fetch (or any other)
# npm install axios # for use axios client
# npm install node-fetch # for use fetch client in nodejs
# # install file download (if necessary)
# npm install js-file-download # for file download (or use other)
# # install
npm install js-request-manager
```
</details>
<details>
<summary><b style="font-size: 1.3em;">Yarn</b></summary>
```shell
# # install a request client, such as axios or fetch (or any other)
# yarn add axios # for use axios client
# yarn add node-fetch # for use fetch client in nodejs
# # install file download (if necessary)
# yarn add js-file-download # for file download (or use other)
# # install
yarn add js-request-manager
```
</details>
<details>
<summary><b style="font-size: 1.3em;">package.json</b></summary>
```json5
{
"dependencies": {
// "axios": "^0.21.1", or "node-fetch": "^2.6.1", or js fetch()
// js-file-download": "^0.4.12", or other
"js-request-manager": "^1.0.0",
// ..
}
}
```
</details>
# Инициализация (варианты):
- Скопировать [файлы оптимального примера](https://github.com/oploshka/js-request-manager/tree/develop/example/create/RmOptimalStructureCreate) к себе в проект (рекомендуется).
- Скопировать [файл минимального примера](https://github.com/oploshka/js-request-manager/tree/develop/example/create/RmSimpleCreate) к себе в проект.
- или создать файл вручную.
<details>
<summary><b style="font-size: 1.3em;">Пример создания вручную</b></summary>
> ```js
> import RequestManager from 'js-request-manager/src/RequestManager';
> import RequestClass from "js-request-manager/src/Class/RequestClass";
> // request sender
> import axios from 'axios';
>
> const requestSchema = {
> Auth: {
> authorization: ({login, password}) => {
> return new RequestClass({
> name : 'authorization',
> type : 'POST',
> url : 'api://authorize', // https://domain.test/api/authorize
> params: {
> get: {},
> post: {login, password},
> },
> responsePrepare: (data) => {
> return {token: data.jwt};
> },
> errorMessage: 'Not correct login or password',
> });
> },
> }
> }
>
> const Config = {
> hostSchema: {
> api: 'https://domain.test/api',
> },
> Hook: {
> RequestPromise (requestPromise, settings) { console.log(requestPromise, settings); }
> },
> RequestClient: {
> async send(obj) { return await axios(obj); }
> }
> }
>
> const RmSimpleCreate = RequestManager(requestSchema, Config);
>
> // optional (recommended use global request manager)
> global.RequestManager = RmSimpleCreate
>
> export default RmSimpleCreate
>
> ```
</details>
# RequestManager принимает следующие настройки:
```js
const RequestSchema = {/* ... request schema */ };
// Config - all parameters are optional
const Config = {
hostSchema: {},
RequestPrepare: {
data(requestType, requestUrl, requestData) {
return requestData;
},
type(requestType, requestUrl, requestData) {
return requestType;
},
url(requestType, requestUrl, requestData) {
return requestUrl.getUrl();
},
requestClientDataPrepare(requestClientData, requestClass) {
return requestClientData;
},
},
ResponsePrepare: {
isError(riObject) {
return false;
},
getErrorInfo: async (riObject, requestClass, Config) => {
return {code: 'error', message: 'Undefined error', data: riObject}
},
getSuccessInfo: async (riObject, requestClass, Config) => {
return riObject.data
},
},
Hook: {
RequestPromise(requestPromise, settings) {
console.log(requestPromise, settings);
}
},
RequestClient: {
name: 'AXIOS', // or FETCH
async send(obj) {
return await axios(obj);
},
async fileDownload(data, ri, requestClass, Config) {
// add file download code
return {};
},
getRequestClientObject(requestObj, requestClass, Config) {
return requestObj;
},
isNetworkError(axiosResponse, requestClass, Config) {
return false;
}
getRiObject(axiosResponse, requestClass, Config) {
return {httpStatus: 200, contentType: '', data: {}, headers: {}};
}
}
}
```
## RequestSchema setting information
Тут описывается как мы будем группировать все наши запросы, какие параметры они будут принимать и в каком виде их отсылать.
Так же можем изменить или подменить данные ответа.
<details>
<summary><b style="font-size: 1.3em">RequestSchema</b></summary>
```js
// schema example
const RequestSchema = {
Auth: {
authorization: ({login, password}) => { return new RequestClass({/* ... */}); },
registration : ({email, login, password}) => { return new RequestClass({/* ... */}); },
},
News: {
getAll : () => { return new RequestClass({/* ... */}); },
getById: ({id}) => { return new RequestClass({/* ... */}); },
getOldNews: () => { return new RequestClass({/* ... */}); },
getNewNews: () => { return new RequestClass({/* ... */}); },
create:({name, desc}) => { return new RequestClass({/* ... */}); },
delete:({id}) => { return new RequestClass({/* ... */}); },
},
Tags: {
News: {
getAll : () => { return new RequestClass({/* ... */}); },
},
User: {
getAll : () => { return new RequestClass({/* ... */}); },
},
},
getTheme : () => { return new RequestClass({/* ... */}); },
};
```
Один запрос описывается функцией, которая принимает один объект и возвращает RequestClass
Пример как это вызывать
```js
RequestManager.Auth.authorization({login: 'admin', password: 'pass'});
RequestManager.News.getAll();
RequestManager.News.getById({id});
RequestManager.Tags.News.getAll();
RequestManager.Tags.User.getAll({}).then(console.log, console.error);
RequestManager.getTheme();
```
</details>
<details>
<summary><b style="font-size: 1.3em;">RequestSchema RequestClass</b></summary>
Это класс, которым мы описываем все наши запросы.
```js
import RequestClass from "js-request-manager/src/Class/RequestClass";
request = new RequestClass({
name : '', // String - request name (need for debug or custom prepare)
type : '', // String - request type [GET|POST|PUT|DELETE ... or other custom ]
url : '', // String - request url
params : {
get : {}, // Send GET params
post: {}, // Send POST params
},
responsePrepare: (response) => { // Function
return {token: response.jwt}; // change response
},
cache : false, // Create request cache
errorMessage: '', // String or Function
// For load file
fileName: 'test.txt', // String or Function
})
```
</details>
## Config setting information
Тут мы можем изменить стандартное поведение Request Manager, подписаться на события, задать alias для url.
Далее можно посмотреть детальную информацию по RequestSchema и Config
Config будут описан в форме Config.[Тип настроек].[Подтип настроек] = [Настройка]
------------------------------------------------------
<details>
<summary><b style="font-size: 1.3em;">Config.hostSchema</b></summary>
Задаем alias для url.
Делаем это для того, чтобы не писать полные имена доменов во всех запросах.
Пример:
```js
const hostSchema = {
auth : 'https://auth.domain.test/api',
apiV1 : 'https://domain.test/api/v1',
apiV2 : 'https://v2.domain.test/api',
};
```
В дальнейшем при описании запросов можно использовать сокращения
```js
RequestClass({ url: 'auth://authorize' /* ... */}); // url => https://auth.domain.test/api/authorize
RequestClass({ url: 'apiV1://users' /* ... */}); // url => https://domain.test/api/v1/users
RequestClass({ url: 'apiV2://news' /* ... */}); // url => https://v2.domain.test/api/news
```
</details>
------------------------------------------------------
<details open>
<summary><b style="font-size: 1.3em;">Config.RequestPrepare</b></summary>
В данном объекте мы можем дополнить/переопределить/изменить/удалить данные, которые будут в запросе.
RequestPrepare является объектом. Смотри параметры (ниже) в Config.RequestPrepare.[Подтип настроек].
</details>
<details>
<summary><b style="font-size: 1.3em;">Config.RequestPrepare.data</b></summary>
Эта функция позволяет изменить/дополнить/подменить данные запроса.
Это отрабатывает для всех запросов!!!
```js
// example Config.RequestPrepare.data
function RequestPrepare_data(requestType, requestUrl, requestData) {
if(requestType === 'POST-REPLACE') { // add new types of queries if you understand what this is for.
return { test: "test"} // replace requestData
}
requestData.time = Date(); // add date in send request
if(requestData.debugInfo) {
console.log(requestData.debugInfo) // view debug info
delete requestData.debugInfo; // delete data (debugInfo) from the request
}
if(requestData.arrayInfo) {
requestData.arrayInfo = JSON.stringify(requestData.arrayInfo) // replace/conver data
}
return requestData;
}
```
</details>
<details>
<summary><b style="font-size: 1.3em;">Config.RequestPrepare.type</b></summary>
Эта функция позволяет изменить/подменить тип запроса.
Это отрабатывает для всех запросов!!!
```js
// example Config.RequestPrepare.type
function RequestPrepare_type(requestType, requestUrl, requestData) {
if(requestType === 'POST-REPLACE') { // add new types of queries if you understand what this is for.
return 'POST' // replace requestType
}
const allowedRequestType = {
'GET': true,
'POST': true,
}
if(!allowedRequestType[requestType]) {
console.warn('Not correct request type', requestType)
return 'GET'; // replace
}
return requestType; // return original request type
}
```
</details>
<details>
<summary><b style="font-size: 1.3em;">Config.RequestPrepare.url</b></summary>
Эта функция позволяет изменить/подменить url запроса.
Это отрабатывает для всех запросов!!!
```js
// example Config.RequestPrepare.url
function RequestPrepare_url(requestType, requestUrl, requestData) {
if(requestType === 'POST-REPLACE') {
return 'https://test.domain.com/test-url' // replace url
}
return requestUrl.getUrl(); // warning requestUrl - is RequestLinkClass
}
```
</details>
<details>
<summary><b style="font-size: 1.3em;">Config.RequestPrepare.requestClientDataPrepare</b></summary>
Эта функция позволяет изменить объект передаваемый в axios или fetch.
Добавить заголовки, настройки и тп.
```js
function RequestPrepare_requestClientDataPrepare(requestClientData, requestClass) {
let token = localStorage.getItem('user-token');
if (token) {
// add axios header
requestClientData.headers['Authorization'] = `Token ${token}`;
}
return requestClientData;
},
```
ps: это общий callback
</details>
------------------------------------------------------
<details open>
<summary><b style="font-size: 1.3em;">Config.ResponsePrepare</b></summary>
В данном блоке мы работаем с ответом (можем изменить).
ResponsePrepare является объектом.
Смотри параметры (ниже) в Config.ResponsePrepare.[Подтип настроек].
</details>
<details>
<summary><b style="font-size: 1.3em;">Config.ResponsePrepare.isError</b></summary>
Тут мы проверяем является ли ответ ошибкой
```js
function ResponsePrepare_isError(responseData){
if( !(200 <= riObject.httpStatus && riObject.httpStatus < 300) ) {
return true;
}
if(!riObject.data.success) {
return true;
}
return false;
};
```
</details>
<details>
<summary><b style="font-size: 1.3em;">Config.ResponsePrepare.getErrorInfo</b></summary>
Если ResponsePrepare_isError - true, то мы пытаемся получить информацию об ошибке.
Данную информацию отдаем в виде объекта.
```js
async function ResponsePrepare_getErrorInfo(riObject, requestClass, Config) => {
return {
code: 'error',
message: riObject.data.error || 'Неизвестная ошибка',
data: riObject.data,
};
};
```
</details>
<details>
<summary><b style="font-size: 1.3em;">Config.ResponsePrepare.getSuccessInfo</b></summary>
Если ResponsePrepare_isError - false, то мы получаем чистые данные из ответа.
Рассмотрим на примере ответа:
```json5
{ "success": true, "result": { "a": 1, "b": 2} }
```
В данном случае, мы хотим получить - { "a": 1, "b": 2}. Для этого используем следующий код:
```js
async function ResponsePrepare_getSuccessInfo(riObject, requestClass, Config) => {
return riObject.data.result;
};
```
</details>
------------------------------------------------------
<details open>
<summary><b style="font-size: 1.3em;">Config.Hook</b></summary>
Hook - можем подписаться на события Request Managera.
Hook является объектом.
Смотри параметры (ниже) в Config.Hook.[Подтип настроек].
</details>
<details>
<summary><b style="font-size: 1.3em;">Config.Hook.RequestPromise</b></summary>
Данное событие вызывается после того как запрос отправлен.
Применим для loading, ведения статистики, логирования, вывода сообщений об ошибках
```js
function Hook_RequestPromise(requestPromise, settings){
requestPromise.then(
(result) => {},
(error) => {
alert(settings.errorMessage) // alert error
});
};
```
</details>
------------------------------------------------------
<details open>
<summary><b style="font-size: 1.3em;">Config.RequestClient</b></summary>
Набор функция позволяет определить, через что мы будем отсылать запрос.
Обычно используется axios или fetch (можно использовать и другие).
В примерах ниже мы будем использовать axios.
В 99% необходимо передать axios. Делается это через Config.RequestClient.send.
Остальное должно работать корректно!
Для добавления пользовательских токенов, заголовков (и тп) в axiosObj используйте "Config.RequestPrepare.requestClientDataPrepare"
Переопределение остального имеет смысл, если вы используете другой клиент для отправки
или axios выпустили новую версию (не совместимую со старой).
ps: Стоит отметить axios на фронте и на беке - ведет себя немного по разному
</details>
<details>
<summary><b style="font-size: 1.3em;">Config.RequestClient.name</b></summary>
Тут мы говорим, какой пресет Config.RequestClient использовать (AXIOS | FETCH).
По дефолту будет использоваться пресет AXIOS.
</details>
<details>
<summary><b style="font-size: 1.3em;">Config.RequestClient.send</b></summary>
Тут мы говорим, что отправка будет через "axios".
```js
import axios from 'axios';
async function RequestClient_send(obj) {
return await axios(obj);
},
```
</details>
<details>
<summary><b style="font-size: 1.3em;">Config.RequestClient.fileDownload</b></summary>
Тут мы говорим, что мы будет загружать файлы.
```js
import fileDownload from 'js-file-download';
async fileDownload(data, ri, requestClass, Config) {
const download = async (data, ri, requestClass, Config) => {
let fileName = requestClass.getFileName();
fileDownload(ri.data, fileName, ri.contentType);
}
download(data, ri, requestClass, Config)
return {};
}
```
</details>
<details>
<summary><b style="font-size: 1.3em;">Config.RequestClient.getRequestClientObject</b></summary>
Данная настройка работает корректно в большинстве случаев.
Не нужно ее переопределять без острой необходимости!
Для добавления пользовательских токенов, заголовков (и тп) в axiosObj используйте "Config.RequestPrepare.requestClientDataPrepare"
Тут мы конвертируем данные из объекта запроса RequestManagera в объект axios. Только конвертируем!
```js
function getRequestClientObject(requestObj, requestClass, Config) {
const axiosObj = {
method : requestObj.type,
url : requestObj.url,
headers : {}
};
// axiosObj.responseType = 'application/json';
if(requestClass.getFileName()){
axiosObj.responseType = 'blob';
}
if(!isEmpty(requestObj.data.get)){
axiosObj.params = requestObj.data.get;
}
if(!isEmpty(requestObj.data.post)){
axiosObj.data = requestObj.data.post;
}
if(requestObj.data.post instanceof FormData){
axiosObj.data = requestObj.data.post;
axiosObj.headers['Content-Type'] = 'multipart/form-data';
}
return axiosObj;
},
```
</details>
<details>
<summary><b style="font-size: 1.3em;">Config.RequestClient.isNetworkError</b></summary>
В js сетевые ошибки не могут получить много информации.
При возникновении сетевых ошибок, мы их обрабатываем отдельно.
Если это сетевая ошибка - то вернем сообщение с текстом ошибки.
Данная настройка работает корректно в большинстве случаев.
Не нужно ее переопределять без острой необходимости!
```js
function isNetworkError(axiosResponse, requestClass, Config) {
if(/* axiosResponse.isAxiosError && */ !axiosResponse.response) {
return axiosResponse.message ? axiosResponse.message : 'Неизвестная сетевая ошибка';
}
},
```
</details>
<details>
<summary><b style="font-size: 1.3em;">Config.RequestClient.getRiObject</b></summary>
Вытаскиваем информацию из ответа в riObject (внутренний тип Request Manager).
Данная настройка работает корректно в большинстве случаев.
Не нужно ее переопределять без острой необходимости!
```js
function getRiObject(axiosResponse, requestClass, Config) {
const ri = {
httpStatus : -1,
contentType : '',
data : {},
headers : {}
}
const clearContentType = (contentType) => {
return contentType ? contentType.split(';')[0] : '';
}
// get status
if(axiosResponse.status) {
ri.httpStatus = axiosResponse.status;
} else if(axiosResponse.request && axiosResponse.request.status) {
ri.httpStatus = axiosResponse.request.status;
}
// get headers
if(axiosResponse.headers) {
ri.headers = axiosResponse.headers;
}
// get contentType
if( ri.headers['content-type']) {
ri.contentType = clearContentType( axiosResponse.headers['content-type'] );
}
// get data
if(axiosResponse.data){
ri.data = axiosResponse.data;
if(ri.data instanceof Blob){
ri.contentType = clearContentType( ri.data.type );
}
}
return ri;
},
```
</details>
------------------------------------------------------
# Преимущества
- Возможность интегрировать в любой js проект (webpack, Vue, React, Next, Nuxt, Angular и ...).
- 1 точка отправки для всех запросов.
- Возможность создать sdk и переиспользовать в разных проектах.
- Удобство использования (группировка по типам, единообразие, response prepare).
- Кэширование запросов (опционально, по умолчанию выключено).
- Работа с фейковыми данными.
- Использование глобальных обработчиков ошибок (user notify)
- Logger, Statistics, Loading, ...
Многое из этого требует интеграции с вашей системой.
Финальный функционал определять вам.
# Недостатки
- Нет возможности сделать универсальное решение (разнообразие API не знает границ).
Возможно, кому то потребуются доп. функционал.
Такие случаи сведены к миниму, но все же они могут случится.
- Сложность. Интеграция с API требует опыта. Нужно понимать, что и где происходит.
Данное решение это упрощает, но всегда хочется проще...
- Решение расчитано на общение через json.
Для работы с другими форматами (xml, yaml, ...)
возможно потребуется использовать дополнительные библиотеки.