Время идет, технологии растут, а разработка тяжела на перемены. Все больше и больше ресурсов зависят от API (но нет единых стандартов и решений). До сих пор популярен REST... Что бы сделать запрос на такой сервер, необходимо задать header (auth token), указать тип запроса, адрес, задать get параметры, указать параметры в теле и тип этого тела (json/multipart). Возможно я еще что то упустил (на хабре есть отдельная статья про REST). Благо есть axios и fetch и они от части решают проблему с отправкой запросов. Время идет дальше и количество запросов - которые нужно отправлять растет, а код начинает превращаться в свалку копипасты. Иногда делают мини конструкторы или обертки. Большая же часть живет по старинке.
Эта часть кода начинает жить в хранилищах (Store). В дальнейшем я буду приводить примеры на основе Vuex (Vue Store), но в целом думаю это актуально и для React и Angular. Почему Vue? Он проще чем React (нет такого многообразия хранилищ) и у меня на нем больше опыта. С Angular - я практически не работал.
Немного истории
Первый мой SPA был самописный (конец 2015год - была какая то версия Angular, но она показалась довольно таки медленной). Уже тогда запросы api были проблематичными и над этим была построена обертка.
Кусок кода из моего старого файла api.js (можно не смотреть).
function api_ajax_end(fun, status){
if (typeof window[fun] == 'function'){
window[fun](status);
}
}
function api_ajax_start(type, url, info, fun){
if(type!='GET'){info = JSON.stringify(info, null, '\t');}
var status = 0;
jQuery.ajax({
type: type,
url: url,
data: info,
contentType: 'application/json;charset=UTF-8',
// async: false,
timeout : 3000,
headers: { "login": login,"token": token},
success: function (msg) {
if ( typeof msg == "object" && msg['error'] === 0 && typeof msg['result'] == "object" ) {
status = msg['result'];
}
api_ajax_end(fun, status);
},
error: function (xhr) {
api_ajax_end(fun, -1);
}
});
}
/** api auth info */
function api_auth_info(info, fun){
api_ajax_start('GET', '/api/v1/auth/', info, fun);
}
/** api user auth { "login": "string", "password": "string" } */
function api_user_auth(info, fun){
api_ajax_start('POST', '/api/v1/auth/', info, fun);
}
/** api user registration { "login": "string", "password": "string", "email": "email" } */
function api_user_registration(info, fun){
api_ajax_start('POST', '/api/v1/users/', info, fun);
}
/** api user edit */
function api_user_edit(info, fun){
api_ajax_start('PUT', '/api/v1/users/', info, fun);
}
Количество запросов в том проекте было не так много (10-15), а сам код не претендует ни на что. Это просто работало и было моим первым SPA. Если не брать во внимание Promise и async/await, то это идеологически лучше чем то, что некоторые middle разработчики делают сейчас (просто вызывают axios или fetch, где необходимо).
Реальность
Увы копипаста запросов считается относительно нормальным явлением и для решения дальнейших проблем в axios используют глобальный конфиг. Выглядит это следующим образом.
Куски кода из реального проекта на Vue JS
// где то в одном файле
$axios.interceptors.request.use(function(config) {
config.headers['Authorization'] = 'Bearer ' + store.state.token
return config
})
// и где то в другом файле стора
{
// ...
actions: {
async getUser({ commit, state }) {
await this.$axios
.get("api.server.net/form")
.then(({ data: { data, countries } }) => {
commit("setUserData", data);
commit("setCountries", countries);
})
.catch(error => {
console.log("Error: ", error);
});
},
// ...
}
}
Можно сказать что в этом виноват Vue JS, но по факту эта проблема есть и в React. C Angular я работал очень давно и мало, но судя по документации и примерам, эта проблема есть и там.
Вернемся обратно к примеру. В этом примере есть ошибка как в глобальном axios конфиге (headers Authorization - передается всегда), так и захордкожены данные url и отсутствие обработки ошибки. Будем честны - это очень плохой код (это один из худших примеров), но это живет в каком то реальном проекте и работает. Я не хочу приводить код junior разработчиков (которые работали на галерах) и решали проблему прелоадера - через setTimeout 3 секунды.
Store - это плохо...
Использование store во многих проектах - не оправдано и только усложняет проект. Многие приготовили @.. для запуска в меня, просто по тому что, кто то сказал - "store это хорошо и без него не куда". Да store - это хорошо, у store есть классный инструмент для дебага (но нужен ли он на самом деле?). Да им (дебагом) приходиться пользоваться, но причина не в том что он классный и удобный, а в том что происходит не понятная вещь и надо понять откуда растут ноги... Это не значит, что нужно полностью отказаться от store (я в нем храню какие то словари, данные пользователя и др. глобальные вещи), нужно сделать его проще и убрать оттуда не нужное.
Проблемы store
Определимся с тем что такое проблема в текущем контексте. Проблема - это то, что вам придется писать ручками/решать самим сверх стандартного функционала (хотя это может быть гораздо проще).
Как можно было заметить - это отсутствие статуса загрузки (это очевидно).
Вторая проблема, есть ли данные в хранилище и можно ли их взять из кеша (это так же пишется ручками).
Третья проблема - обработка ошибки.
Четвертая - получение данных по id (по факту - все кладут болт и кладут в одну ячейку).
Пятая - конвертация данных.
Проблем на самом деле гораздо больше. Пример классического news store для показа детальной информации об одной новости (не будем брать реализацию через список - по мне это так же плохо).
Простой файл для реализации хранилища во Vue
export default {
state: {
newsItem: null,
}
getters: {
// это можно не писать, а использовать на прямую newsItem
getNewsItem(state) { return state.newsItem; }
}
mutations: {
setNewsItem: (state, payload) => {
state.newsItem = payload;
},
}
actions: {
loadNewsItem: async (context, payload) => {
let {data} = await Axios.get('/api/news');
context.commit('setNewsItem', data);
},
},
};
Это пример на основе первого что загуглилось. Для примера - не будем использовать константы (с namespase true - их использование становиться еще и проблематичным, но сейчас не об этом).
Пример решения проблем store
Для решения первой и второй проблемы - нам достаточно добавить в state newsItemStatus: 'CREATE | LOADING | SUCCESS | ERROR'.
Для решения третьей проблемы - можем присваивать данные ошибки в newsItem или создать newsItemError. newsItemError - позволит избежать ошибок if(newsItem) в уже написанном коде (по этому будем использовать этот вариант).
Четвертая проблема возникает - при необходимости отобразить 2 новости на станицу (можно конечно сделать еще 1 store... но это глупо).
Конвертация данных - в целом не очень сложная задача, но ей часто пренебрегают. После решения этих проблем получаем store.
Продвинутая реализация Vue Store
import Vue from 'vue';
import newsItemPrepare from 'functionNewsItemPrepare';
const getKey = function (search) {
return 'newsItem_' + search.newsId;
};
const getNew = function () {
return {
loadStatus: "CREATE",
request : null, // для возможности отменить запрос
data : null,
error : null,
};
};
export default {
state: {
newsItemObject: {
// newsItem_1: { loadStatus: "CREATE", request: null, data: null, error: null }
},
},
getters: {
getNewsItem(state) {
// тут можно приянять только 1 параметр, по этой причине ожидаем {newsId: 12} (делается для дальнейшего единообразия)
return function(search){
return state.newsItemObject[ getKey(search) ] ? state.newsItemObject[ getKey(search) ] : null;
};
},
// + getNewsItemStatus(), getNewsItemError()
},
mutations: {
setNewsItem (state, {search, data=null, error=null}) {
var key = getKey(search);
state.newsItemObject[ key ].loadStatus = data ? 'SUCCESS' : 'ERROR';
state.newsItemObject[ key ].request = 'null';
state.newsItemObject[ key ].data = data;
state.newsItemObject[ key ].error = error;
},
clearNewsItem (state) {
state.newsItemObject = {};
},
},
actions: {
async getNewsItem({ state, getters, commit, dispatch }, search) {
// Очистим или инициализируем
Vue.set(state.newsItemObject, key, getNew());
Vue.set(state.newsItemObject[key], 'loadStatus', 'LOADING');
// Обработку запроса я упрощу с 30-40 строк до
try {
let request = Axios.get('/api/news');
Vue.set(state.newsItemObject[key], 'request', request);
let {data} = await request;
commit('setNewsItem', {search: search, data: newsItemPrepare(data)});
} catch (e) {
commit('setNewsItem', {search: search, error: e});
}
}
}
};
В итоге получаем файлы по 100 строк, которые не очень понятны с первого взгляда (и со второго тоже). Так же недоумение для чего нам все это... Ответ - прост, чтоб сделать это:
Пример использование оптимального Store во Vue
// <tempate if="newsStatus === 'CREATE' || newsStatus === 'LOADING'">loading ...</tempate>
// <tempate if="newsStatus === 'SUCCESS'">{{news.name}}</tempate>
// <tempate if="newsStatus === 'ERROR'">error ...</tempate>
computed: {
newsStatus(){
store.getNewsItemStatus({newsId: 10});
}
news(){
store.getNewsItem({newsId: 10});
}
}
create(){
if(newsStatus === 'CREATE') {
store.loadNewsItem({newsId: 10});
}
}
Это приносит еще пару проблем, которые нам надо решать. Одни из самых простых - это показать сообщение с ошибкой (сетевая или серверная) или вывести alert. В таком варианте существование кода и его поддержка проблематична. Это приводит к написанию оберток над store, написанию доп. компонентов. Ко всему этому надо написать документацию. В итоге к простому сайту/админке с тремя сущностями, мы накидываем ~600 лишних строк кода (это пальцем в небо и кажущийся мне минимум). Поддержка и внедрение такого функционала проблематично (к вам придут новые разработчики и на копипастят ...).
Приведу пример с использованием обертки над store и на сколько уменьшается количество кода (но это для понимания как можно делать, без рассмотрения дополнительных компонентов).
Пример Vue Store с использованием обертки
import stateCacheCreate from 'stateCacheCreate';
import newsItemPrepare from 'functionNewsItemPrepare';
export default stateCacheCreate({
name: 'newsItem',
getKey({ newsId }) {
return "newsItem_" + newsId;
},
getRequestData(search) {
return {
methodName: 'getNewsById',
methodData : {
newsId : search.newsId,
},
}
},
prepare: newsItemPrepare
});
Фитбек - такая реализация не понятна и сложна. Тут каждый решает сам - что ему надо.
Request Manager?
Request Manager - так я называю API SDK для общения с сервером, которую вы пишете сами и затачиваете под свой проект или проекты. Вы можете описать все свои методы или только часть.
При чем же Request Manager?
В целом не корректно сравнивать Request Manager и Store (это разные вещи и решают разные задачи). Проблема только в том, что Store решает проблемы получения данных, те. чем отправлять, как отправлять, куда и тд., хотя не должен знать об этом ничего... Это приводит к раздуванию хранилища и хранению там бесполезных данных, которые могут не очищаться и храниться мертвым грузом.
Давайте представим что мы используем Vue без Store и напишем страницу с новостью, как бы мы ее хотели видеть в идеале.
Пример Vue страницы без Store - ожидания
// <template><div v-if="newsItem">{{newsItem.name}}</div></template>
const loadNewsById = async (newsId) => {
// ...
return news;
}
export default {
props: ['newsId'],
data() { return {newsItem: null}; },
async create() {
this.newsItem = await loadNewsById(this.newsId)
}
}
Моя реальность
// <template><div v-if="newsItem">{{newsItem.name}}</div></template>
<script>
export default {
props: ['newsId'],
data() { return {newsItem: null}; },
async create() {
try {
this.newsItem = RequestManager.News.getById({ newsId: this.newsId })
} catch(err) {
// ...
}
}
}
По моему, все очень просто. Возникает вопрос, а нужен ли здесь Store если все так просто? Мое мнение - нет.
А как же loading, сообщения об ошибке и прочее?
Для страниц я использую некоторую надстройку, которая решает эти проблемы. Loading используется глобальный (с одной стороны это не очень хорошо, но решает огромное количество проблем, при отправке форм, долгой загрузке и прочие моменты - тут каждый решает сам)
Предобработка данных?
На удивление она тоже есть.
Я буду делать кучу запросов запросов к серверу?
По умолчанию - да. Но есть возможность кешировать конкретные запросы, дописав в одном месте cache: true (но лучше функцию, которая будет отдавать "(string) ключ" или "false"). У меня не возникало проблем, с тем что надо много чего кешировать. Сам cache - пока что очень примитивен.
Создаем Request Manager
Возможно и вы создавали свой менеджер запросов в том или ином виде. В целом эта задача довольно не обычная. Для моего менеджера запроса прошло более двух лет и получилось отделить ядро от запросов. А сейчас попытаемся частично интегрироваться с https://reqres.in . Для начала установить пакет
npm install js-request-manager
Дальше мы просто описываем нашу схему в фай RmReqres
import RequestClass from "js-request-manager/src/Class/RequestClass";
import RequestManager from "js-request-manager/src/RequestManager";
//
const ReqresAPIRequestSchema = {
User: {
getList: ({page = null}) => {
return new RequestClass({
name: 'User::getList',
type: 'GET',
url : 'api://users',
get: {
page: page
}
});
},
getById: ({userId}) => { // not found 23
return new RequestClass({
name: 'User::getList',
type: 'GET',
url : api://users/${userId},
responsePrepare: (respData) => {
return respData.data;
},
});
},
create: ({name, job}) => { // response 201
return new RequestClass({
name: 'User::create',
type: 'POST',
url : api://users,
});
},
update: ({userId, user }) => { // response 201
return new RequestClass({
name: 'User::update',
type: 'PUT', // or PATCH
url : api://users,
});
},
delete: ({userId }) => { // response 204
return new RequestClass({
name: 'User::delete',
type: 'DELETE',
url : api://users/${userId},
});
},
}
};
export default RequestManager(ReqresAPIRequestSchema,{
hostSchema: {
api: "https://reqres.in/api/"
},
RequestClient: {
name: 'FETCH',
}
});
Поздравляю, наш менеджер запросов готов. Осталось только вызвать
import RmReqres from "RmReqres";
RmReqres.User.getList({page: 1}).then(console.log, console.error);
// RmReqres.User.getById({userId: 10})
// RmReqres.User.create({name:'', job: ''})
Заключение
Request Manager делает код чище и решает множество проблем на которые обычно все закрывают глаза. Да он не идеален, но это лучше чем ничего. Наиболее оптимально и привычнее для всех - будет совместное использование Store и Request Manager. Но все таки не стоит перегружать store бесполезной информацией. Для меня же интереснее получить фидбек и возможно кто то захочет по участвовать в этом, а кто то просто будет использовать такое решение, а кто то останется при своем. В общем "Перемен...")