[Разработка веб-сайтов, JavaScript, Клиентская оптимизация, ReactJS] iresine, нормализация данных на клиенте
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Нормализация. От нее мы или страдаем или пишем собственное решение с множеством проверок на существование сущности в общем хранилище. Попробуем разобраться и решить эту проблему!Описание проблемы
Представим себе такую последовательность:
- Клиентское приложение запрашивает список пользователей запросом к /users и получается пользователей с id от 1 до 10
- Пользователь с id 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 описывает свои действия с данными:
...normalizes query 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'); // user
store.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'); // user
store.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, которое отдает данные, знает о том какие данные и какого типа отдает. Однако в некоторых случаях нет возможности изменить код сервера для отдачи типа, в таких случаях можно добавить типы на клиенте.Теперь разберемся как все это автоматизировать.iresineiresine это библиотека созданная для нормализации данных и оповещении об их изменении.В данный момент 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 iresine
const 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']; // true
iresine.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-queryreact-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
===========
Похожие новости:
- [JavaScript, Node.JS, Angular, TypeScript] Angular Universal: проблемы реального приложения
- [JavaScript] Квест для прогеров на Java script
- [Высокая производительность, Разработка веб-сайтов, PHP, Программирование] Теория программирования: пакетные принципы и метрики
- [JavaScript, Программирование] Как написать интерфейс пользователя (UI) PlayStation 5 на JavaScript (перевод)
- [Информационная безопасность, JavaScript] JavaScript prototype pollution: практика поиска и эксплуатации
- [Веб-дизайн, Разработка веб-сайтов, Интерфейсы, Usability, Дизайн] Метод Content Brick. Как создать структуру веб-сайта быстрее и проще чем это было раньше? (перевод)
- [Разработка веб-сайтов, Гаджеты, Старое железо, Лайфхаки для гиков] Вторая жизнь пыльного Андроида
- [JavaScript] Контролируем JavaScript импорты с помощью Import maps
- [JavaScript, Программирование, VueJS] Сделаем худший Vue.js в мире (перевод)
- [Разработка веб-сайтов, Проектирование и рефакторинг, Терминология IT, Хранение данных] Символы Unicode: о чём должен знать каждый разработчик (перевод)
Теги для поиска: #_razrabotka_vebsajtov (Разработка веб-сайтов), #_javascript, #_klientskaja_optimizatsija (Клиентская оптимизация), #_reactjs, #_normalizatsija (нормализация), #_javascript, #_razrabotka_vebsajtov (
Разработка веб-сайтов
), #_javascript, #_klientskaja_optimizatsija (
Клиентская оптимизация
), #_reactjs
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 06:04
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Нормализация. От нее мы или страдаем или пишем собственное решение с множеством проверок на существование сущности в общем хранилище. Попробуем разобраться и решить эту проблему!Описание проблемы Представим себе такую последовательность:
Ответ: Зависит от компонента, который запросил данные. В компоненте, который использует данные из запроса к /users, будет отображаться старое имя. В компоненте, который использует данные из запроса к /user/3, будет отображаться новое имя.Вывод: В таком случае в системе существует несколько одинаковых по смыслу сущностей с разным набором данных.Вопрос: Почему это плохо? Ответ: В лучшем случае пользователь увидит разные имена одного человека в разных разделах сайта, в худшем переведет деньги на старые банковские реквизиты.Варианты решенияВ настоящее время существуют следующие варианты решения этой проблемы:
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); } } Использовать клиент graphql (apollo или relay) Apollo и relay это библиотеки, которые из коробки умеют нормализовать данные. Однако такое решение заставляет нас использовать graphql и apollo, которые, по моему мнению, имеют множество недостатков.НормализацияЧто такое нормализация и как она позволяет graphql клиентам бороться с указанной проблемой? Разберемся на примере apollo! Так apollo описывает свои действия с данными: ...normalizes query response objects before it saves them to its internal data store.
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. const store = new Map();
const user = { id: '0', type: 'user', name: 'alex', age: 24, }; const id = `${user.type}:${user.id}`; store.set(id, user);
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'); // user store.get('1'); // comment 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'); // user store.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; } @iresine/coreОсновной модуль библиотеки, именно он отвечает за парсинг данных, их нормализацию и оповещении подписчиков об изменении конкретной сущности. const iresine = new Iresine();
const oldRequest = { users: [oldUser], comments: { 0: oldComment, }, }; // new request data have new structure, but it is OK to iresine const 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']; // true iresine.get('comment:0' /*identifier for old and new comment*/) === newRequest.comments['0']; // true entityType + ':' + entityId;
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}`; }, }); const iresine = new Iresine({
getId: (entity) => { if (!entity) { return null; } if (!entity.id) { return null; } return entity.id; }, }); const user = {
id: '0', type: 'user', jobs: [ { name: 'milkman', salary: '1$', }, { name: 'woodcutter', salary: '2$', }, ], }; 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 =========== Похожие новости:
Разработка веб-сайтов ), #_javascript, #_klientskaja_optimizatsija ( Клиентская оптимизация ), #_reactjs |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 06:04
Часовой пояс: UTC + 5