Время идет, технологии растут, а разработка тяжела на перемены. Все больше и больше ресурсов зависят от 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 бесполезной информацией. Для меня же интереснее получить фидбек и возможно кто то захочет по участвовать в этом, а кто то просто будет использовать такое решение, а кто то останется при своем. В общем "Перемен...")
