В рубрике DOU Проекторвсе желающие могут презентовать свой продукт (как стартап, так и ламповый pet-проект). Если вам есть о чем рассказать — приглашаем поучаствовать. Если нет — возможно, серия вдохновит на создание собственного made in Ukraine продукта. Вопросы и заявки на участие присылайте на editors@dou.ua.
Всем привет! Меня зовут Андрей Кияновский, и я активный пользователь интернета с тех пор, когда он был еще по модему, а знакомство с владельцем BBS-борды считалось за привилегию. Свою первую серьезную программу я написал в школе на СМ-2М (это компьютер размером с комнату). Потом работал девелопером, системным администратором, бизнесменом, менеджером проектов и сейчас зарабатываю на жизнь как certified Atlassian administrator. Оглядываясь назад и сравнивая удовольствие от работы девелопером и головняк от работы менеджером, решил заняться проектом tabXpert — расширением для управления вкладками в браузере. Проект появился частично как ностальгия по программированию, а частично как попытка сделать работу таких же людей, как я, проводящих всю жизнь за компьютером, немного более эффективной.
Идея
Вы наверняка сталкивались с затрудненной навигацией в браузере из-за большого количества вкладок, проблемами при падении браузера, потерей времени на поиск в истории или управление закладками.
На рынке уже существует несколько расширений для Chrome, пытающихся упростить жизнь активного интернет-пользователя, так или иначе сохраняя состояние вкладок и окон браузера. Просмотрев существующие предложения, такие как OneTab, Session Buddy, Tabs Outliner, я понял, что в этом «так или иначе» и есть основная загвоздка. Мой идеальный менеджер вкладок должен уметь:
- сохранять окна автоматически в реальном времени;
- «подхватывать» ранее закрытые окна, чтобы продолжить, где остановился;
- не создавать дубликаты одного и того же окна, одно окно — одна тема;
- синхронизировать данные с облаком в реальном времени;
- иметь быстрый и удобный интерфейс.
Так что немного почитав и подумав над архитектурой, я решил размять мозги. Тем более у меня как раз был перерыв в трудоустройстве.
Реализация
Скоро сказка сказывается, да не скоро дело делается. Я был очень наивным, полагая, что смогу осуществить этот проект за три-четыре месяца и $15-20К денег. Уже прошло полтора года и бюджет превышен многократно. Начинал я один, потом нашел частного инвестора, и теперь у нас есть еще один девелопер.
Давайте остановимся на трех основных задачах, которые заняли больше всего времени:
- синхронизация в реальном времени;
- устойчивость к перезапускам и падениям браузера;
- быстрый и, надеюсь, удобный UI.
Синхронизация в реальном времени
Так как синхронизация — ключевой момент, с самого начала надо было понимать, как мы будем синхронизировать состояния окна с разных компьютеров и обновлять его в реальном времени. Например, чтобы поработав дома на ноутбуке, продолжить тему на работе и, вернувшись домой, открыть ноутбук и продолжить с обновленным состоянием незакрытого окна. Вариант сохранять состояние в разных «версиях» и предоставлять пользователю возможность их «менеджить» — это перекладывание проблемы с больной головы на здоровую, и такое решение почти не будет отличаться от существующих реализаций.
Состояние окна с другого компьютера не получится просто «применить» в силу асинхронности событий, необходимо объединять изменения на уровне вкладок. Но как понять, с какого компьютера состояние вкладки более новое? Идея использовать время обновления вкладки сразу отпала, потому что при загрузке браузера вкладки сразу обновятся и изменения с удаленного компьютера не будут применены. Дабы не изобретать колесо, мы решили реализовать three-way merge, как сделано в Git. Перед перехватом управления над окном другого компьютера, состояние окна сохраняется как родительское (как ветка в Git). При сравнении двух разных «версий» одного окна легко определить какая вкладка изменилась, сравнивая ее состояние с родительским. В редких случаях конфликта открывается дополнительная вкладка, так что пользователь гарантированно ничего не теряет при синхронизации.
В процессе тестирования мы столкнулись с проблемой зацикливания синхронизации в случае, если одно и то же окно активно на двух компьютерах и вы авторизуетесь на каком либо сайте. Так как на втором компьютере авторизации нет, это приводит к редиректу на страницу авторизации, что на основном компьютере приводит к редиректу на основной URL и так по кругу. Хотя мы и решили эту проблему для большинства случаев путем введения ограничения на обновление состояния вкладки в DB, для надежности была добавлена опция, которая просто закроет окно, если вы его обновите на другом компьютере. Это не только самое надежное решение, но и правильное — люди около второго компьютера вряд ли обрадуются, если он внезапно начнет издавать звуки.
Для хранения данных на клиенте (Chrome под Windows, macOS и Linux) была выбрана база данных NoSQL PouchDB (хранилище на IndexedDB) — тонкий клиент для сервера на CouchDB, сильной стороной которой является синхронизация данных. База данных работает в Worker, чтобы не замедлять UI.
В настоящий момент синхронизация еще в разработке, надеемся выпустить ее осенью 2018 г. Синхронизация будет платной (локальная версия полностью бесплатная, без рекламы и каких-либо других способов монетизации). Пример работы синхронизации вкладок в разных профилях в реальном времени (синхронизация происходит через облако):
Устойчивость к перезапускам и падениям браузера
Для избежания появления дубликатов сохраненных окон после перезапуска браузера, tabXpert должен уметь сопоставлять окна между разными сессиями, когда включена опция Chrome, «запускать ранее открытые вкладки». Не существует простого способа сопоставить окна в разных сессиях — не сохраняется ни id вкладок, ни id окон. Решили считать хеш по URL вкладок и по его значению сопоставлять окна в разных сессиях. Так как запись в DB производится только в idle режиме (для оптимизации), мгновенное сохранение состояния окна, включая хеш вкладок, происходит в local storage, который достаточно быстрый и не сохраняет историю.
Основная сложность оказалась в том, что существует множество ситуаций, когда Chrome восстанавливает вкладки окна частично или добавляет новые, например:
- вкладки могут не восстановиться, если они загружались в момент завершения работы браузера или при его падении;
- вкладки могут добавляться при старте и при восстановлении после падения браузера;
- chrome:// вкладки теряются, если открыть ранее нормальное окно в инкогнито-режиме.
Для решения этой проблемы tabXpert производит сопоставление окон старой и новой сессий, учитывая все возможные изменения. Для каждого окна вычисляется два вида хеша — постоянный для данных, сохраненных в DB, и мгновенный для текущего состояния, сохраненного в local storage. Также в local storage хранится два состояния окна — текущее и предыдущее, так как иногда Chrome успевает сохранить новое состояние в local storage, но восстанавливает при этом предыдущее состояние. Для окна, которое надо сопоставить после перезапуска браузера, хеш просчитывается для всех возможных вариантов изменений. В случае частичного сопоставления, tabXpert восстанавливает недостающие вкладки. Для синхронизации текущего и сохраненного окна используется тот же механизм, что и для сетевой синхронизации.
Интерфейс пользователя
При написании UI мы прошли через Angular, Vue и остановились на React. Angular оказался слишком тяжелым для относительно простого интерфейса. Vue не сильно распространен, и специалистов по нему я не нашел. В общем проблему выбора окончательно решило наличие опытного девелопера, который смог решить все вопросы с использованием React.
Клиентская часть расширения работает на связке React + Redux. За сборку проекта отвечает Webpack, а транспиляцией самых свежих конструкций JavaScript в рабочий код занимается Babel. Часть ошибок в коде превентивно ловим инструментом semistandard.
Каскадные таблицы стилей написаны на любимом многими SCSS, реализация тем оформления — на Styled Components. Из коробки доступны две цветовых схемы: светлая и темная.
Иконки используются в формате SVG, благодаря этому они выглядят максимально резко на экранах с высокой плотностью пикселей. Да и стилизовать SVG гораздо проще, чем шрифтовые иконки.
Для реализации большинства элементов интерфейса используем библиотеку компонентов React Material UI. С решением проблемы медленного рендеринга больших списков нам помогла библиотека React List. Многоязычность реализована с помощью компоненты react-i18next.
Расширение имеет два режима работы: в обычной вкладке и в попапе, всплывающем при нажатии на иконку расширения на тулбаре Chrome или по горячей клавише. Для устранения постоянной подгрузки и моргания favicon (в DB хранятся только URL) мы реализовали кэширование и храним их памяти в виде data URL. Для поиска по вкладкам используем Elasticlunr.
Для ускорения скорости загрузки попапа мы используем server-side rendering, генерируя HTML-разметку для попапа в фоновой странице каждый раз при изменении данных активного окна. Благодаря этому экран попапа отображается практически мгновенно, после чего подгружается основной скрипт, который добавляет интерактивность.
Результат
Главный эффект от использования tabXpert — это значительная экономия времени, потому что вместо управления вкладками вы начинаете управлять темами (окнами браузера), остальное берет на себя tabXpert. Таким образом, вместо открытия большого количества вкладок в одном окне, вы просто открываете новое окно для новой темы. Когда вы закончили работать с темой, вы закрываете окно. Когда хотите продолжить — открываете и продолжаете, все изменения будут сохранены в этом же окне и вам для этого ничего делать не надо, все происходит автоматически.
Когда вы работаете с несколькими окнами, возникает необходимость быстрого переключения между окнами, например прочитать почту или посмотреть перевод слова. Вы можете назначить горячую клавишу на вкладку (до 9 штук), быстро на нее переключиться и вернуться назад по Alt-0. Это чрезвычайно удобно.
tabXpert также поможет решить проблему нехватки памяти при большом количестве открытых вкладок. Уже существует достаточно популярное расширение The Great Suspender, которое автоматически замораживает неиспользуемые вкладки, выгружая их из памяти (и загружая, когда вы возвращаетесь на эту вкладку). tabXpert понимает, когда вкладка заморожена, и запоминает ее состояние. В следующий раз, когда вы открываете сохраненное окно, tabXpert загружает вкладки в том состоянии, в котором они были на момент закрытия окна. Таким образом, вы не только экономите память, но и время, так как окно с замороженными вкладками восстанавливается гораздо быстрее, чем в обычном режиме.
Мы серьезно относимся к приватности. Во-первых, расширение не имеет доступа к данным на вкладках, только к состоянию вкладки. Во-вторых, кроме обычных окон, которые будут синхронизироваться с облаком, существует локальный тип окна, который не синхронизируется с облаком (и хранится в отдельной DB), и секретный тип окна, которое вообще не сохраняется в DB. В-третьих, данные, передаваемые в облако, будут зашифрованы ключом, сформированным от пароля пользователя и случайной соли. Таким образом, данные пользователя не будут скомпрометированы даже в случае взлома облака.
Следующий минутный проморолик показывает основную идею tabXpert в действии:
Также есть много других возможностей, таких как перетаскивание вкладок между окнами, разъединение и объединение окон, открытие нормального окна в инкогнито режиме и наоборот, тип секретности по умолчанию для инкогнито-окон, светлая и темная темы, быстрое отключение звука, экспорт и импорт, включая импорт из OneTab и Session Buddy. Есть также возможность быстро скопировать HTML-ссылку вкладки (название и URL одновременно) для того, чтобы вставить ее в письмо или документ, как например эта ссылка, по которой вы можете скачать tabXpert и попробовать его в действии: tabXpert — Chrome Web Store.
В ближайших планах у нас:
- синхронизация с облаком;
- промосайт;
- дополнительные языки.
Также я подумываю об интеграции управления закладками прямо в интерфейс tabXpert.
А что было бы интересно вам? Пишите в комментариях ваши впечатления. Буду рад ответить на любые вопросы.