Русский
Русский
English
Статистика
Реклама

Iresine, нормализация данных на клиенте

Нормализация. От нее мы или страдаем или пишем собственное решение с множеством проверок на существование сущности в общем хранилище. Попробуем разобраться и решить эту проблему!

Описание проблемы

Представим себе такую последовательность:

  1. Клиентское приложение запрашивает список пользователей запросом к /users и получается пользователей с id от 1 до 10

  2. Пользователь с id 3 меняет свое имя

  3. Клиентское приложение запрашивает пользователя с id 3 с помощью запроса к /user/3

Вопрос:Какое имя пользователя с id 3 будет в приложении?
Ответ:Зависит от компонента, который запросил данные. В компоненте, который использует данные из запроса к /users, будет отображаться старое имя. В компоненте, который использует данные из запроса к /user/3, будет отображаться новое имя.

Вывод: В таком случае в системе существует несколько одинаковых по смыслу сущностей с разным набором данных.

Вопрос:Почему это плохо?
Ответ:В лучшем случае пользователь увидит разные имена одного человека в разных разделах сайта, в худшем переведет деньги на старые банковские реквизиты.

Варианты решения

В настоящее время существуют следующие варианты решения этой проблемы:

  • Не обращать внимание

  • Нормализовать данные собственноручно

  • Использовать клиент graphql (apollo или relay)

Не обращать внимание

Это самый очевидный и заманчивый вариант. В некоторых случаях клиентское приложение действительно может позволить себе иметь одинаковые сущности с разными данными. Но что делать со случаями, когда это недопустимое поведение? Как быть с разработчиками, которые не хотят создавать приложение с такими дефектами?

Нормализовать данные собственноручно

Примером собственноручной реализации может послужить код для mobx:

class Store {  users = new Map();  async getUsers() {    const users = await fetch(`/users`);    users.forEach((user) => this.users.set(user.id, user));  }  async getUser(id) {    const user = await fetch(`/user/${id}`);    this.users.set(user.id, user);  }}

И если пример с mobx выглядит приемлемо, то нормализация в redux простоужасает. Работать с таким кодом становится сложнее по мере его увеличения и совсем неинтересно

Использовать клиент graphql (apollo или relay)

Apollo и relay это библиотеки, которые из коробки умеют нормализовать данные. Однако такое решение заставляет нас использовать graphql и apollo, которые, по моему мнению, имеют множество недостатков.

Нормализация

Что такое нормализация и как она позволяет graphql клиентам бороться с указанной проблемой? Разберемся на примере apollo! Так apollo описывает свои действия с данными:

...normalizesquery response objects before it saves them to its internal data store.

Что включает в себя указанноеnormalize?

Normalization involves the following steps:

1. The cache generates a unique ID for every identifiable object included in the response.
2. The cache stores the objects by ID in a flat lookup table.

То есть apollo формирует уникальный идентификатор для каждой сущности, для которой возможно его сформировать. Apollo использует его как ключ в хранилище всех сущностей. Вот как примерно выглядит формирование идентификатора и его хранение:

const store = new Map();const user = {  id: '0',  type: 'user',  name: 'alex',  age: 24,};const id = `${user.type}:${user.id}`;store.set(id, user);

Комбинация типа и id дает нам по-настоящему уникальный ключ. Мы можем быть уверены, что если встретим другого пользователя с таким же типом и id, то это будет тот же пользователь.

Получение уникального идентификатора

Apollo достигает указанного эффекта, запрашивая при каждом запросе внутреннее поле __typename, а как достигнуть похожего эффекта без graphql?

Поскольку мы не имеем внутренних полей с типами, то должны полагаться только на поля данных. Вот несколько решений:

  • сделать поле id или аналогичное поле глобально уникальным

  • добавить информацию о типах сущности в данные

    • добавить типы на сервере

    • добавить типы на клиенте

Сделать поле глобально уникальным

В таком случае хранение сущностей будет выглядеть вот так:

const store = new Map();const user = {  id: '0',};const comment = {  id: '1',};store.set(user.id, user);store.set(comment.id, comment);// ...store.get('0'); // userstore.get('1'); // comment

Решение выглядит достаточно удобным в использовании, однако реализация глобально уникальных полей id будет затруднительна. Как правило, сущности хранятся в базе данных и имеют id уникальный только внутри коллекции/таблицы (или другими словами какого-то типа). А значит, чтобы сделать id глобально уникальным, нужно приложить много усилий.

Добавить информацию о типах

В таком случае хранение сущностей выглядеть вот так:

const store = new Map();const user = {  id: '0',  type: 'user', // <-- new field};const comment = {  id: '1',  type: 'comment', // <-- new field};function getStoreId(entity) {  return `${entity.type}:${entity.id}`;}store.set(getStoreId(user), user);store.set(getStoreId(comment), comment);// ...store.get('user:0'); // userstore.get('comment:1'); // comment

По-прежнему удобно, но при этом требует от нас добавления особого поля в данных. Как мне кажется эта небольшая жертва окупается возможностью автоматического отслеживания изменения в данных. Именно этот вариант я выбрал предпочтительным для себя.

Где добавлять типы в данные?

Проблема нормализации данных особенно характерна для клиентских приложений. Поэтому рассмотрим вопрос - в какой момент добавлять информацию о типах в данные. Мы можем выбрать один из указанных вариантов для добавления типов.

  • На сервере, при отдаче данных:

app.get('/users', (req, res) => {  const users = db.get('users');  const typedUsers = users.map((user) => ({    ...user,    type: 'user',  }));  res.json(typedUsers);});
  • На клиенте, при получении данных:

function getUsers() {  const users = fetch('/users');  const typedUsers = users.map((user) => ({    ...user,    type: 'user',  }));  return typedUsers;}

Как мне кажется вариант добавления данных на сервере является предпочтительным. Api, которое отдает данные, знает о том какие данные и какого типа отдает. Однако в некоторых случаях нет возможности изменить код сервера для отдачи типа, в таких случаях можно добавить типы на клиенте.

Теперь разберемся как все это автоматизировать.

iresine

iresineэто библиотека созданная для нормализации данных и оповещении об их изменении.

В данный момент iresine состоит из следующих модулей:

Так iresine работает с react-query:

@iresine/core

Основной модуль библиотеки, именно он отвечает за парсинг данных, их нормализацию и оповещении подписчиков об изменении конкретной сущности.

const iresine = new Iresine();const oldRequest = {  users: [oldUser],  comments: {    0: oldComment,  },};// new request data have new structure, but it is OK to iresineconst newRequest = {  users: {    0: newUser,  },  comments: [newComment],};iresine.parse(oldRequest);iresine.parse(newRequest);iresine.get('user:0' /*identifier for old and new user*/) === newRequest.users['0']; // trueiresine.get('comment:0' /*identifier for old and new comment*/) === newRequest.comments['0']; // true

Как видим из идентификаторов, по которым мы получаем сущности из хранилища, @iresine/core использует следующую схему для создания идентификаторов:

entityType + ':' + entityId;

По умолчанию @iresine/core берет тип из поляtype, а id из поляid. Это поведение можно изменить, передав собственные функции. Например попробуем использовать такой же идентификатор как в apollo:

const iresine = new Iresine({  getId: (entity) => {    if (!entity) {      return null;    }    if (!entity.id) {      return null;    }    if (!entity.__typename) {      return null;    }    return `${entity.__typename}:${entity.id}`;  },});

Так же мы можем обрабатывать и глобально уникальное поле id:

const iresine = new Iresine({  getId: (entity) => {    if (!entity) {      return null;    }    if (!entity.id) {      return null;    }    return entity.id;  },});

А что @iresine/core делает с сущностями, где идентификатор не обнаружен? Например такими:

const user = {  id: '0',  type: 'user',  jobs: [    {      name: 'milkman',      salary: '1$',    },    {      name: 'woodcutter',      salary: '2$',    },  ],};

user имеет своей идентификатор в хранилище, а как быть с jobs? У них нет ни поля type ни поля id! @iresine/core следует простому правилу: если у сущности нет идентификатора, то она становится частью ближайшей родительской сущности с идентификатором.

@iresine/core являет универсальной библиотекой, которая знает о том как распарсить данные и точечно уведомлять подписчиков. Но использовать ее напрямую довольно нудно и утомительно! Посмотрим как сделать этот процесс удобнее.

@iresine/react-query

react-query это прекрасная библиотека, с которой я бы посоветовал ознакомиться каждому. Но в ней отсутствует нормализация данных, и именно этот факт вдохновил меня на написание iresine.

@iresine/react-query это плагин для react-query. Он позволяет использовать функцию нормализации и обновления данных @iresine/core на данных хранилища react-query. Вся работа по нормализации происходит автоматически и клиент работает с react-query так, как бы работал без iresine.

import Iresine from '@iresine/core';import IresineReactQuery from '@iresone/react-query';import {QueryClient} from 'react-query';const iresineStore = new IresineStore();const queryClient = new QueryClient();new IresineReactQueryWrapper(iresineStore, queryClient);// now any updates in react-query store will be consumbed by @iresine/core

Схема взаимодействия выглядит так(была приведена выше):

Итог

Нормализация данных на клиенте это проблема. Сейчас она решается разными способами с разной степенью успешности. В написанном выше материале автор предлагает свой способ решения этой проблемы. Если сократить все предложение до нескольких слов, то они будут звучать какдобавьте информацию о типах в данные, а после этого используйтеiresine

Источник: habr.com
К списку статей
Опубликовано: 17.03.2021 14:12:14
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Разработка веб-сайтов

Javascript

Клиентская оптимизация

Reactjs

Нормализация

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru