На прошлой неделе я был вовлечен в новый проект, связанный с процессом управления запасами.

Моей целью было спроектировать и создать приложение, доступное в автономном режиме, которое может позволить пользователям либо искать, либо создавать или изменять элементы. Мне пришлось столкнуться с множеством проблем:

  • одна неделя на выполнение работы;
  • обработка в среднем 35 000 записей, в том числе офлайн (для целей поиска);
  • обработка автономных манипуляций с данными (создание / редактирование);
  • синхронизация данных между клиентом и сервером, когда соединение доступно.

Несколько технических вещей, которые нужно знать: использование в автономном режиме необходимо, потому что область, где хранятся предметы, может быть не полностью покрыта хорошим подключением к Интернету или доступом Wi-Fi (у целевого покупателя есть несколько местоположений). Мы решили не создавать собственное приложение из-за огромного количества времени, необходимого для публикации всего в сети, и потому, что нам нужен был полный контроль над процессом развертывания, чтобы планировать выпуск новых исправлений каждый час (да, каждый час, чтобы соответствовать новым сценариям, обнаруженным во время инвентарь).

Выбранная архитектура

Я решил использовать самое простое из возможных решений, чтобы выполнить свою работу:

  • Сервисы REST для управления данными;
  • Приложение PWA, написанное с использованием ReactJS;
  • PouchDB для обработки автономных данных.

Нам нужно сказать два слова о последнем пункте. После нескольких исследований, посвященных автономному управлению данными в веб-приложении, я обнаружил действительно хорошую библиотеку под названием PouchDB, которая идеально подходит для моего сценария. PouchDB - это онлайн / офлайн система хранения данных, которую можно использовать в приложениях JavaScript разными способами.

Обычно используемая конфигурация состоит из адаптера данных IndexedDB, который позволяет приложению использовать эту встроенную в браузер технологию (для ясности, IndexedDB). В любом случае он реализует репликацию и может использоваться вместе со многими другими технологиями.

Реализация

Процесс реализации был относительно простым. Начав с файла Excel, содержащего структуру данных каждого элемента, я разработал простую базу данных, размещенную на движке MariaDb, состоящую из таблиц пользователей и элементов (и нескольких других таблиц для изображений, журналов и отчетов). Инфраструктура REST была построена с использованием PHP в качестве базовой технологии и CakePHP 3.8 в качестве среды для настройки рабочей среды с аутентификацией JWT и протоколом CRUD REST, реализованным благодаря множеству библиотек с открытым исходным кодом (большинство из них создано командой FriendsOfCake, на самом деле умные ребята, они сэкономили мне много времени). Вся эта работа, благодаря сообществу разработчиков открытого исходного кода, потребовала трех часов работы.

Для пользовательского интерфейса, как упоминалось ранее, я выбрал ReactJS. Почему? Потому что я влюблен в эту технологию с момента ее рождения. Ничего больше. И я решил выбрать другой фреймворк с открытым исходным кодом, React-Admin. React-Admin - это то, что вам нужно, когда вам нужно создавать надежные приложения, готовые к использованию с мобильными и настольными клиентами. Это самый полезный фреймворк, который я видел для очень быстрого создания интерфейсов с высокой кривой обучения. React-Admin следует протоколу REST (и многие другие все еще реализованы, если вам нужно что-то другое), и с помощью нескольких шагов вы готовы создать процесс входа в систему и управляемые операции CRUD. Первое, что вам нужно сделать, это настроить authProvider и dataProvider.

AuthProvider отвечает за управление аутентификацией с помощью JWT, dataProvider предоставляет необходимые методы для обработки основных операций CRUD.

DataProvider - это ключ к моим автономным операциям.

Используя то, что команда React-Admin называет «функцией», я создал оболочку в дополнение к базовому dataProvider, которая проверяет, нет ли подключения к Интернету, и переключается между автономной и онлайн базой данных.

const dataProvider = requestHandler => (type, resource, params) => {
  // type contains requested operation: 
  // CREATE, GET_LIST, UPDATE etc.
  // .. handle the request ...
}
export default dataProvider;

Подробнее о dataProvider вы можете прочитать по этой ссылке.

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

Моя функция - это еще один провайдер, которого можно добавить в список разрешенных, выставив необходимые операции, необходимые для управления автономными данными:

import PouchDB from "pouchdb";
const inventoryDb = new PouchDB("inventory");
const offlineDataProvider = requestHandler => (type, resource, params) => {
  const isOffline = !navigator.onLine;
  if (!isOffline) {
   return requestHandler(type, resource, params);  
  }
  // Now starts working offline:
  // Every operation is executed on local database 
  // following PouchDB operation standards
  if (type === CREATE || type === UPDATE) {
    return inventoryDb.upser({...});  
  }
  else if (type === LIST) {
    return inventoryDb.allDocs().then(docs => ...);
  }
  else ...
}

Вы можете прочитать более подробную информацию о PouchDB API по этой ссылке.

Приложение проверяет (с помощью служб синхронизации) наличие обновлений и загружает вновь созданные или измененные элементы (только при наличии подключения к Интернету). Когда пользователи впервые входят в систему, они должны загрузить не менее 35000 записей для локального хранения (в IndexedDB) после завершения передачи (мы говорим о 25 МБ сжатых данных с точки зрения информации, содержащейся в ответе HTTP).

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

Технически цель заключалась в том, чтобы разрешить поиск по многим полям (большинство из них - строки), и я испробовал множество стратегий, используя несколько библиотек PouchDB: поиск и поиск. Ни один из них мне не помог: на стандартном по аппаратному ресурсу смартфоне каждый поиск занимал не менее 30-40 секунд. Используя PouchDB-find, я смог сократить время до 5-6 секунд, используя индексы, но только после выполнения первого поиска, когда было задействовано создание индексов и запрос другого времени для завершения (слишком много времени, по крайней мере 10 секунд плюс первый раз, суммированный с 20 другими секундами для реального поискового совпадения).

В этом поиске медленно происходит беспорядок, документ PouchDB спасает меня, когда я читаю подробности о методе API allDocs:

«Пожалуйста, используйте` allDocs () `. Шутки в сторону."

allDocs() - незамеченная звезда мира PouchDB. Он не только возвращает документы по порядку - он также позволяет вам изменять порядок, фильтровать по _id, нарезать и кубиков, используя операции «больше чем» и «меньше чем» на _id и многое другое.

Слишком многие разработчики упускают этот ценный API из виду, потому что неправильно его понимают. Когда разработчик говорит: «Мое приложение PouchDB работает медленно!», Это обычно происходит из-за того, что они используют медленный query() API, тогда как им следует использовать быстрый allDocs() API.

Я обнаружил кое-что интересное. Загрузка 35 000 документов занимает не менее 1 секунды из-за производительности, связанной с реализацией IndexedDB в браузере. Каждый документ состоит из первичного ключа и его редакций, кроме того, это не полная запись, а только проиндексированная часть (первичные ключи). После этого вы можете загрузить полные данные записи с помощью API метода get. И мне этого достаточно. Используя информацию из документации, я попробовал кое-что совершенно другое: я создал специальный ключ документа, который хранится во время процесса синхронизации и содержит первичный ключ и доступные для поиска поля. Этот процесс похож на построение и индексирование, основанное непосредственно на первичном ключе; таким образом я мог использовать возможности строковых ключей, разрешенные IndexedDB (без каких-либо проблемных ограничений с точки зрения допустимой длины).

Затем я основывал каждый автономный поиск на новом первичном ключе, используя базовые операции со строками, такие как regex и indexOf. Скорость поиска снизилась с 30 секунд до максимум 5 секунд, приемлемый временной диапазон при работе с приложением!

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

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