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

Indexeddb

Влияние service workerов на web-приложения

12.08.2020 10:04:09 | Автор: admin

Web-приложения всё больше "затачиваются" под мобильные устройства, а service worker'ы являются фундаментом прогрессивных web-приложений (PWA). При первом ознакомлении с данной технологией может сложиться впечатление, что основной задачей service worker'ов является кэширование контента. И это так. Задача service worker'ов обеспечение функционирования web-приложения в условиях нестабильного или вообще отсутствующего подключения к Сети, что достигается при помощи кэширования данных.


Под катом пара мыслей о том, к каким последствиям для web-приложений привело появление возможности кэшировать данные посредством service worker'ов.


Архитектура PWA


Вот классическая трёхуровневая архитектура web-приложения:


Добавление на клиенте service worker'а и инструментов для сохранения данных (Cache API и IndexedDB) превращают трёхуровневую архитектуру в пятиуровневую:


По сути, при отсутствии соединения с Сетью прогрессивное web-приложение должно работать на клиенте в классическом трёхуровневом режиме:


а когда появляется соединение с Сетью переходить на пятиуровневую:


  1. Presentation (Main Thread): пользовательский интерфейс;
  2. Client Logic (Service Worker): бизнес-логика обработки данных конкретного пользователя с учётом работы в offline & online режимах;
  3. Client Data (Cache API & IndexedDB): хранилища данных конкретного пользователя;
  4. Server Logic (Server): бизнес-логика обработки данных всех пользователей приложения;
  5. Server Data (DB): хранилище данных всех пользователей приложения;

Offline first


В web-разработке популярной является стратегия mobile first. Для PWA есть похожая стратегия offline first. Её суть в том, что приложение изначально разрабатывается для условий автономной работы (классическая трёхуровневая архитектура на клиенте), а затем расширяется до пятиуровневой.


Таким образом, самой первой задачей service worker'а является кэширование данных в объёме, достаточном для автономного функционирования приложения. Следующей организация очереди запросов для обмена данными конкретного пользователя с сервером. Затем мониторинг состояния соединения с сервером (online/offline) и обработка очереди запросов.


Итого, обеспечения функционирования приложения в offline-режиме это не только кэширование, а ещё и (в какой-то мере) дублирование серверной бизнес-логики обработки данных, плюс управление очередью запросов.


Типы трафика


Трафик между браузером клиента и сервером можно разделить на две части:



  • статика: контент, общий для всех клиентов (HTML/CSS/JS/images/...);
  • данные (API): контент (как правило, JSON), предназначенный как для всех пользователей (каталог продуктов), так и для конкретного пользователя (корзина покупок);

Для первого типа трафика (статика) браузер предоставляет Cache API простое хранилище "запрос" "ответ"


Для хранения данных (API) IndexedDB (NoSQL хранилище структурированных данных в формате JSON).


Типы хранилищ


Cache


В панели инструментов Chrome'а хранилище находится в Application / Cache / Cache Storage / <имя кэша>, содержимое выглядит примерно так:


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


IndexedDB


В панели инструментов Chrome'а хранилища объектов находятся в Application / Storage / IndexedDB / <имя базы> / <имя хранилища объектов>, содержимое выглядит примерно так:


База предоставляет транзакционный доступ к хранилищам для выполнения CRUD-операций, индексацию данных по ключевым полям, версионирование структуры базы. Запросы к IndexedDB выполняются в асинхронном режиме. Объём хранимых данных зависит от многих факторов, но вполне может достигать гигабайтов.


В общем, функционала вполне достаточно для реализации в рамках браузера приложения в классической трёхуровневой архитектуре.


Загрузка файлов service worker'а


При имплементации клиентской бизнес-логики в service worker'е он получается достаточно сложным функциональным блоком. В одном файле его код без средств сборки (типа webpack'а) не разместить. Для загрузки скриптов в область видимости service worker'а существует метод WorkerGlobalScope.importScripts(). Но его особенность в том, что он синхронный. Для service worker'а нет возможности динамической загрузки его компонентов:


import('/modules/my-module.js')  .then((module) => {    // Do something with the module.  });

Все скрипты, которые могут понадобиться service worker'у, должны загружаться при его регистрации. Что и понятно service worker должен обеспечивать работоспособность web-приложения в offline-режиме, а для этого он сначала должен обеспечить свою собственную работоспособность.


Резюме


Добавление service worker'а (и IndexedDB) даёт возможность создавать браузерные приложения (PWA в режиме offline) по классической трёхуровневой схеме (интерфейс логика данные). Online-режим в PWA добавляет к трёхуровневой схеме ещё и "межсерверные" взаимодействия: Client Logic/Data Server Logic/Data. Бизнес-логика web-приложения распадается на две части: для отдельного пользователя и для совокупности всех пользователей, во многом совпадающие, но имеющие различия (например, ACL имеет смысл встраивать только в серверную сторону).


Синхронная загрузка скриптов в service worker'е ограничивает возможности разработчиков в выборе инструментария для реализации бизнес-логики в самом service worker'е (например, слабо поддерживается ES6 import), поэтому есть смысл оставлять за service worker'ами только лишь функции кэширования статики, а всю клиентскую бизнес-логику выводить в Main Thread (включая обработку данных и очередей запросов).


Поэтому схема архитектуры прогрессивного web-приложения, на мой взгляд, лучше выглядит в таком виде:


Service worker обрабатывает и кэширует только статику. Клиентская логика отрабатывает в основном потоке браузерного приложения, взаимодействует с хранилищем данных и сама контролирует переход из online в offline и обратно (т.е. обмен данными с серверной частью приложения).


Возможно, кому-то изложенное выше покажется несколько запутанным, но до написания этой статьи моё понимание роли service worker'ов в прогрессивных web-приложениях было ещё запутаннее. Буду признателен за комментарии, ещё больше проясняющие задачи service worker'ов и способы их использования.


Ссылки


Подробнее..

Кешируем CRUD в IndexedDB

28.04.2021 00:09:16 | Автор: admin

Допустим, у нас есть бекенд, который умеет хранить какие-то сущности. И у него есть апи для создания, чтения, изменения и удаления этих сущностей, сокращенно CRUD. Но апи на сервере, а пользователь забрался куда-то глубоко и половина запросов валится по таймауту. Не хотелось бы показывать бесконечный прелоадер и вообще блокировать действия пользователя. Offline first предполагает загрузку приложения из кеша, так может быть и данные брать оттуда?

Предлагается хранить все данные в IndexedDB (допустим, что их не очень много), и по возможности синхронизировать с сервером. Возникает несколько проблем:

  1. Если Id сущности генерится на сервере, в базе, то как жить без Id, пока сервер недоступен?

  2. При синхронизации с сервером, как отличать сущности созданные на клиенте от удаленных на сервере другим пользователем?

  3. Как разрешать конфликты?

Идентификация

Идентификатор нужен, так что будем его создавать сами. Для этого прекрасно подходит GUID или `+new Date()` с некоторыми оговорками. Только когда придет ответ от сервера с настоящим Id, надо везде его заменить. Если на эту свежесозданную сущность уже ссылаются другие, то эти ссылки тоже надо поправить.

Синхронизация

Изобретать велосипед не будем, посмотрим на репликацию баз данных. Смотреть на нее можно бесконечно, как на пожар, но вкратце, один из вариантов выглядит так: помимо сохранения сущности в IndexedDB, будем писать лог изменений: [time, 'update', Id=3, Name='Иван'], [time, 'create', Name='Иван', Surname='Петров'], [time, 'delete', Id=3]...

При получении с сервера свежих данных, мы можем сравнить их с существующими и вычислить аналогичный лог изменений на сервере. После этого объединить логи и отправить необходимые изменения на сервер, если интернет еще не пропал, и только после этого записывать изменения в IndexedDB. И не забыть обновить Id.

Конфликты

Конфликт - это не спор между двумя пользователями, чья точка зрения правильная, и поочередное исправление одной и той же записи до посинения. А вот ситуация, когда пользователи довольны и видят каждый свою версию - конфликт, а конкретно неконсистентность. Непрерывной консистентности в веб приложениях достичь несложно - при каждом изменении блокировать всех клиентов, кого это изменение касается, пока они все не подтвердят получение. Это никому не нравится, поэтому приходится идти на компромис: ладно, пусть иногда пользователи видят разное, но если все замрут и перестанут вносить изменения, то через некоторое время у всех будет одно и то же. Для этого придумали термин Eventual Consistency.

Оказалось, что ее можно достичь незаметно для пользователя, но не так просто. Можно использовать Operational Transformations (OT) или Conflict-free Replicated Data Types (CRDT) но для них придется довольно радикально поменять формат обмена данных с сервером. Если это невозможно, то можно на коленке сделать CRDT на минималках: добавить в сущность поле UpdatedAt и записывать в него время последнего изменения. Это не избавит от всех конфликтов, но снизит их количество на порядок.

Итак, при объединении двух логов группируем их по Id сущности и дальше работаем в каждой группе отдельно. Если в одном из логов есть операция удаления, то оставляем только ее. Пользователь, удаливший запись наверняка имел на это веские основания и не хотел бы, чтобы запись вдруг возродилась. Никто не любит зомби кроме зомби. Если в одном из логов есть операция создания сущности, то в другом логе должно быть пусто, ведь Id уникальный, ага. С изменениями немного сложнее - нужно посмотреть на время последнего изменения сущности в каждом из логов. Сравнить. И выбрать тот лог, в который изменение пришло позднее. Last write win. Проверим Eventual Consistency: если все пользователи перестанут вносить изменения и подключатся к интернету, у всех будут сущности последней версии. Отлично.

function mergeLogs(left, right){    const ids = new Set([        ...left.map(x => x.id),        ...right.map(x => x.id)    ]);    return [...ids].map(id => mergeIdLogs(        left.filter(x => x.id == id),        right.filter(x => x.id ==id)    )).reduce((a,b) => ({        left: [...a.left, ...b.left],        right: [...a.right, ...b.right]    }), {left: [], right: []});}function mergeIdLogs(left,right){    const isWin = log => log.some(x => ['create','delete'].includes(x.type));    const getMaxUpdate = log => Math.max(...log.map(x => +x.updatedAt));    if (isWin(left))        return {left: [], right: left};    if (isWin(right))        return {left: right, right: []};    if (getMaxUpdate(left) > getMaxUpdate(right))        return {left: [], right: left};    else        return {left: right, right: []};}

Эпилог

Реализации не будет, потому что в каждом конкретном случае есть свой дьявол в деталях, да и реализовывать тут по большому счету нечего - генерацию идентификатора и запись в indexedDB.

Конечно, CRDT или OT будут лучше, но если нужно сделать быстро, а на бекенд не пускают, то сгодится и это поделие.

Подробнее..

Категории

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

  • Имя: Макс
    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