Разработка онлайн-ридера (читалки FB2) - PHP

Разработка онлайн-ридера (читалки FB2) - PHP

Опубликовано:
16.08.2021 в 20:31
Категория:
Предисловие: Рассказываю о том как недавно довелось разрабатывать ПО для чтения книг FB2 онлайн

Нынче выдался хороший год. Есть несколько хороших проектов, которые я со временем непременно опубликую.

Сегодня я хочу рассказать как раз о таком проекте - онлайн-читалка для FB2 формата. Но сначала немного предыстории.

Начало разработки

Наш старый партнер - магазин OldieWorld решил развиваться дальше и разработать онлайн читалку для каталога своих книг.

Первым делом необходимо выбрать формат из доступных нам. Почти все книги имели формат FB2, EPUB, PDF, TXT.  То есть выбрать мы можем FB2 или EPUB, так как они имеют данные разметки. Выбор был очевиден. Я выбрал FB2, так как мне он казался проще, все таки это по сути простой XML.

Как и любой другой разработчик я пошел в гугл искать готовые решения для парсинга FB2 формата. Но к великому сожалению готовых решений довольно мало.  Единственный пакет, который мне показался взрослым - GitHub - tizis/FB2-Parser: Simple FB2 to HTML converter.

Но я не смог его завести.

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

Разработка FB2 транслятора (Backend)

Первым делом пришла в голову мысль воспользоваться классом SampleXMLObject и поначалу это себя оправдывало. Я спокойно распарсил контент и описание книги, но выяснилось одно, но. Книга в формате FB2 содержит микро-разметку. Вот пример:

Мы видим

section - это страница или глава

p - параграф. Section может содержать еще овер-много section, а каждый другой тег title или p может содержать другую дичь в виде тегов empharsis, strong, poem и др.

Все это поставило точку на использовании SampleXMLObject ибо эта скотина конвертирует xml в достаточно плоский объект. То есть структура книги не сохраняется. А сохранение структуры очень важно, ибо для трансляции разметки как в книге необходимо много рекурсии.

Пришлось искать другие способы и благо таковы были. Я нашел для себя класс DOMDocument. 

Мне он показался крайне экзотичным, но со своей задачей он справляется хорошо. Данный класс представляет XML файл в виде DOM дерева и содержит необходимые методы для перемещения по нему прямо как в JavaScript. Например, есть метод:

$body = $document->getElementsByTagName('body')

Данный метод позволяет получить данные тега. Но не все так просто. Если вы попытаетесь получить тег таким образом, то вам выдадут экземпляр другого класса который уже называется DOMElement и содержит свои методы для работы.

Например, вы можете получить дочерние узлы:

$body->childNodes

Получить родителя:

$body->parentNode

Получить текстовую ноду:

$body->nodeValue

Изучив DOMDocument я принялся за работу. Пришлось потратить 5 полных рабочих дней, что бы выкатить рабочий прототип. Отдельной головной болью была разработка транслятора xml -> html. На текущий момент этот метод выглядит так:

/**
 * Рекурсивно распарсить в html в узле DOM
 * @param $element
 * @return string|null
 */
protected function parseHtml($element, $clear_node = false): ?string
{
    $content = '';

    if (!isset($element->childNodes)) {
        return null;
    }

    foreach ($element->childNodes as $child_element) {
        $tag_start = '';
        $tag_end = '';
        $node = $child_element->nodeName;

	// Обработка тегов (под каждый тег одно условие)
        if ($node == 'Название тега') {
            $tag_start = 'Открытие html тега';
            $tag_end = 'Закрытие html тега';
        }

      
        // Не обрабатывать вложенную секцию
        if ($node == 'section') {
            continue;
        }

        // Вернуть чистый текст без обработки
        if (isset($child_element->nodeValue) and $clear_node) {
            $content .= $child_element->nodeValue;
            continue;
        }

        // Если есть дочерние ноды вызвать рекурсивно
        if ($child_element->childNodes->length) {
            $content .= $tag_start . $this->parseHtml($child_element) . $tag_end;
        } else {
            $content .= ($tag_start . $child_element->nodeValue . $tag_end);
        }
    }

    return $content;
}

Работает достаточно просто. Мы можем передать секцию FB2 книги и на выходе получим html код в виде строки. Метод сам, рекурсивно обработает все дочерние узлы. Условия в этом методе проверяют имена тегов (узлов) и в зависимости от того какой тег в цикле устанавливают название тегов html и css классы.

На удивление обработка книги размеров 12мб занимает неприлично мало времени. Мне казалось такая дура будет жрать ресурсы, но на деле все куда проще. Но я все равно запилил кеширование тк., книги я получаю из приватного облака Amazon, поэтому кеш необходим, что бы пользователь не перекачивал книгу при каждом запросе.

В общем распарсить разметку было несложно, но что делать с картинками?

Оказывается картинки зашиты в формат и хранятся в виде бинарных данных. Вот как это выглядит.

Да выглядит ужасно. Но на деле все куда проще чем кажется с первого взгляда. Оказывается можно пропихнуть тегу атрибуту тега img src=bin и он это схавает.  Конечно код одной картинки это ДЕСЯТКИ ТЫСЯЧ таких символов и для любого разработчика будет сюрприз увидеть это в своем редакторе кода.

Я все же не извращенец, поэтому для улучшения результата просто кодируем эту простыню в base64. Сделать это не сложно (используется twig):

src="data:image/jpeg;base64,{{image}}"

Это все премудрости формата. 

Внешний вид приложения (FrontEnd)

Хотелось воплотить что-то по-настоящему удобное. Так как индексировать книжки нам не надо руки можно развязать, и построить интерфейс в виде SPA приложения.

Я терпеть не могу vue, angular, react хотя бы потому что я Backend разработчик. Да и эти фреймворки довольно тяжелые. Их избыточный функционал мне тоже не нужен. 

А у кого нет таких минусов? Правильно, добро пожаловать в alpine.js.

С этим фреймворком я познакомился недавно переписывая интерфейс своей CRM системы. И функционал Alpine мне зашел. Плюс он до безобразия прост.

Дизайн решил сделать минималистичным в серо-белых тонах. Далее со скриншотами.

При переходе по ссылке на книгу нас встречает обложка. В данном примере у нас отрывок, поэтому есть кнопка купить. У зарегистрированного пользователя в этом месте кнопка  - библиотека. Есть кнопка для перехода в полноэкранный режим.

Переключение страниц (глав) работает через кнопки в подвале, кнопке оглавление или кнопками на клавиатуре (A, D).

Шапка, подвал и боковая панель в постоянной фиксации. Это очень удобно. Если мы читаем книгу было бы грустно постоянно листать пол книги для доступа к элементам управления.

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

Зарегистрированный пользователь может не покидать читалку, выбор доступен прямо в интерфейсе. 

Мобильный интерфейс полностью аналогичен по возможностям с десктопом. Более того концептуально он так же удобен.

Шапка, подвал, боковая панель зафиксированны.

Вот так выглядит выбор книги из библиотеки.

Оглавление

Настройки

В настоящий момент читалка готова, вы можете ее пощупать почитав бесплатную книжку в магазине олди -  oldieworld.com/reader/demo/313.

Всем спасибо кто читает. Отдельно спасибо Миру Олди за доверие и многолетнее сотрудничество.

Оцените данный материал

На еду автору

Буду крайне благодарен за любую копеечку :)