Как стать автором
Обновить

Vue/React Store и JS Request Manager

Время на прочтение9 мин
Количество просмотров5.1K

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

Теги:
Хабы:
Всего голосов 6: ↑5 и ↓1+4
Комментарии12

Публикации

Истории

Работа

Ближайшие события

Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн
Антиконференция X5 Future Night
Дата30 мая
Время11:00 – 23:00
Место
Онлайн
Конференция «IT IS CONF 2024»
Дата20 июня
Время09:00 – 19:00
Место
Екатеринбург