Vue/React Store и JS Request Manager

от автора

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


ссылка на оригинал статьи https://habr.com/ru/articles/569154/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *