Pull to refresh

Не храните в базах данных HTML

Level of difficultyMedium
Reading time12 min
Views6.6K

Часто информация, которая отображается пользователю и имеет свойства разметки, хранится в базе данных и сразу в формате HTML. Я уверен, что те, кто хоть раз создавал свой блог, Хабр или что-то подобное, скажут: «Да, было». Но что, если я предложу другой подход? Использовать JSON.


Давайте вместе разберёмся в преимуществах и недостатках такого подхода к хранению данных.


Ирония создания новых стандартов: попытка объединить стандарты приводит к их увеличению


В чем проблема хранить html в БД?


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


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


Соответственно, рано или поздно возникнет потребность в сильной переработке визуала. Фронтендеру будет необходимо обновить пользовательский интерфейс, для этого он должен учитывать, что на сервере весь HTML в БД является статичным и, возможно, даже есть устаревшие UI-элементы или, например, легаси CSS.


Волшебным образом HTML с CSS-классами не поменяются, для этого уже потребуется вмешательство второго разработчика — бэкендера, ему нужно будет провести ряд манипуляций по миграции одного формата вёрстки в другой, и хорошо, если это делается редко. Если это не так и изменения происходят часто, то это будет большая боль и постоянная трата ресурсов команды.


Можно, например, посмотреть, что выдается для мобильной версии фронтенда Хабра под недавним мегапостом ссылка на апи. Автоматизированно, без вмешательства человека, такие данные модифицировать сложно.


Big article


Некоторое количество заинлайненого css и javascript (около половины поста генерируется им) осталось за кадром.


Во-вторых, абсолютно точно не стоит забывать про нативные приложения. И если ваш сервис сейчас ориентируется только на веб, то с течением времени это может измениться.


Мобильные приложения — де-факто стандарт, каждая уважающая компания, ориентированная на расширение пользовательской аудитории, создаст мобильное приложение. Как разработчик вы должны быть к этому готовы. Также не стоит списывать десктопные приложения со счетов, они пользуются спросом как в b2b, так и в b2c, и привлекательны для аудитории за счет удобства.


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


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


  2. Использовать транслятор html в нативные UI-элементы. Хорошо если для вашего платформы существует такой транслятор, но если нет или он не удовлетворяет вашим требованиям, то разработчику сложно позавидовать, написание собственноручно такого транслятора достаточно не тривиально и трудоемко. Кстати об в том числе этом, когда-то я написал статью о реализации мобильного приложения Хабра на Flutter, советую, там в том числе об этой проблеме есть комментарии и код.



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


В-третьих, безопасность. HTML является мощным инструментом для создания и отображения контента, но он также может быть использован для выполнения вредоносного кода. Если злоумышленник получит доступ к базе данных, он может использовать HTML-верстку для внедрения вредоносных скриптов на сайт или в приложение, что может привести к серьезным последствиям: взлом коснется не только компании и её внутренней инфраструктуры, но и напрямую затронет безопасность пользователей.


Универсальный способ хранения


Вместо того чтобы хранить HTML, нужно разделить отвественность: отдельно платформозависимая верстка и отдельно логика содержимого. Например, JSON с заранее определенной или легко расширяемой структурой. Это позволит каждому элементу иметь четкую структуру и не зависеть от представления, созданного фронтендером.


Преимущества такого подхода:


Мы полностью абстрагируемся от представления данных и сосредоточимся исключительно на их содержимом. Благодаря этому в любой момент сможем легко обновить код для рендеринга JSON в HTML, который будет отображаться в браузере. Более того, мы сможем написать свой легковесный рендеринг из JSON не только в HTML, но и для нативных приложений, не прибегая к использованию WebView.


Рассмотрим идею на примерах


Для этого предлагаю ознакомится с тем, что выдает по api Хабр (только уже без мегапостов) \
Важно: я предполагаю, что статьи хранятся в том же виде, в каком и отправляются.


Показывать я будут сначала html, а потом альтернативу в виде json.


Изображение с подписью: источник: https://habr.com/ru/company/tuturu/blog/526710/


<img src="https://habrastorage.org/webt/nz/2t/p3/nz2tp3ywl9fdr7quadxm1dzmhdo.jpeg"><br>
<sup> Хотя пеликан, конечно, улетит. Пеликаны они вообще такие. </sup><br>

{
    "type": "image",
    "src": "https://habrastorage.org/webt/nz/2t/p3/nz2tp3ywl9fdr7quadxm1dzmhdo.jpeg",
    "caption": "Хотя пеликан, конечно, улетит. Пеликаны они вообще такие."
}


Изображение с подписью: источник: https://habr.com/ru/post/526676/


<img src="https://habrastorage.org/webt/lr/ql/v9/lrqlv9u6cxmuoyn-0hgrik3z0aw.jpeg"><br>
<i>Макет пункта захоронения ЖРО на одном из трех подобных российских объектов.</i><br>

{
    "type": "image",
    "src": "https://habrastorage.org/webt/lr/ql/v9/lrqlv9u6cxmuoyn-0hgrik3z0aw.jpeg",
    "caption": "Макет пункта захоронения ЖРО на одном из трех подобных российских объектов."
}

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


Рассмотрим более комплексный пример.


Оригинал откуда взята часть текста


А вот так может например выглядит спойлер (просто напомню, что в html5 давно есть details):


Большой кусок кода
<div class="spoiler" role="button" tabindex="0">
    <b class="spoiler_title">Я приведу список моих документов. Он очень скучный</b>
    <div class="spoiler_text">Итак, нужно было перевести на немецкий:<br />
        <br />
        <ul>
            <li>Мои дипломы (бакалавра и магистра) с приложениями</li>
            <li>Диплом супруги с приложением</li>
            <li>Свидетельство о браке</li>
            <li>Свидетельства о рождении детей</li>
            <li>Трудовую книжку</li>
            <li>Резюме</li>
        </ul><br />
        Перед выполнением перевода нужно не забыть поставить апостиль на «Свидетельство о браке» и «Свидетельства о
        рождении детей». Вот тут-то я понял, почему эти документы нельзя ламинировать.<br />
        <br />
        Также необходимо проверить факт соответствия диплома по базе данных <a
            href="https://anabin.kmk.org/anabin.html" rel="nofollow">Anabin</a>.<br />
        <br />
        Далее нужно было:<br />
        <br />
        <ul>
            <li>Получить приглашение от немецкой компании</li>
            <li>Получить копию трудового договора со стороны компании. В моем случае оригинал с «мокрой» печатью и
                подписями не понадобился</li>
            <li>Оформить медицинскую страховку</li>
            <li>Подписать бумагу о том, что являешься цивилизованным человеком и в курсе, как вести себя в Германии
            </li>
            <li> заполнить <a
                    href="https://germania.diplo.de/ru-ru/service/05-VisaEinreise/langfristigerAufenthalt/-/1611410?openAccordionId=item-1611410-3-panel"
                    rel="nofollow">анкету </a>на получение национальной визы </li>
            <li>Взять гражданский и заграничный паспорт с копиями</li>
            <li>Сфотографироваться на визу</li>
        </ul><br />
        Разумеется, все документы должны оформляться на немецком языке.<br />
        <br />
        Интересный нюанс: есть несоответствие информации на сайте <a href="https://germania.diplo.de/ru-ru"
            rel="nofollow">представительства Германии в России</a> и реальных требований. А именно: сертификат,
        подтверждающий знания немецкого языка предоставлять не нужно. Кроме того, не нужно иметь договор аренды
        жилья в Германии, хотя этот пункт присутствует в анкете. Напомню, это не нужно если вам открывают рабочую
        визу с последующим оформлением «blue card», но если вы переезжаете по-другому алгоритму — и документы также
        могут потребоваться другие.<br />
        <br />
        В немецком посольстве все были предельно вежливыми, белыми и пушистыми.<br />
        <br />
        Однако, но из-за какой-то внутренней ошибки они чуть было не выдали мне рабочую визу на 3 месяца вместо 6-и.
        Так что доверяй, но проверяй!<br />
        <br />
        Да, немцы тоже косячат. <br />
    </div>
</div>

Иииии… преобразуем это в json:


{
    "type": "details",
    "title": "Я приведу список моих документов. Он очень скучный",
    "child": {
        "type": "div",
        "children": [
            {
                "type": "tp",
                "text": "Итак, нужно было перевести на немецкий:"
            },
            {
                "type": "unordered_list",
                "children": [
                    {
                        "type": "tp",
                        "text": "Мои дипломы (бакалавра и магистра) с приложениями"
                    },
                    {
                        "type": "tp",
                        "text": "Диплом супруги с приложением"
                    },
                    {
                        "type": "tp",
                        "text": "Свидетельство о браке"
                    },
                    {
                        "type": "tp",
                        "text": "Свидетельства о рождении детей"
                    },
                    {
                        "type": "tp",
                        "text": "Трудовую книжку"
                    },
                    {
                        "type": "tp",
                        "text": "Резюме"
                    }
                ]
            },
            {
                "type": "tp",
                "text": "Перед выполнением перевода нужно не забыть поставить апостиль на «Свидетельство о браке» и «Свидетельства о рождении детей». Вот тут-то я понял, почему эти документы нельзя ламинировать."
            },
            {
                "type": "paragraph",
                "children": [
                    {
                        "type": "span",
                        "text": "Также необходимо проверить факт соответствия диплома по базе данных",
                        "mode": []
                    },
                    {
                        "type": "link_span",
                        "text": "Anabin",
                        "src": "https://anabin.kmk.org/anabin.html"
                    },
                    {
                        "type": "span",
                        "text": ".",
                        "mode": []
                    }
                ]
            },
            {
                "type": "tp",
                "text": "Далее нужно было:"
            },
            {
                "type": "unordered_list",
                "children": [
                    {
                        "type": "tp",
                        "text": "Получить приглашение от немецкой компании"
                    },
                    {
                        "type": "tp",
                        "text": "Получить копию трудового договора со стороны компании. В моем случае оригинал с «мокрой» печатью и подписями не понадобился"
                    },
                    {
                        "type": "tp",
                        "text": "Оформить медицинскую страховку"
                    },
                    {
                        "type": "tp",
                        "text": "Подписать бумагу о том, что являешься цивилизованным человеком и в курсе, как вести себя в Германии"
                    },
                    {
                        "type": "paragraph",
                        "children": [
                            {
                                "type": "span",
                                "text": "заполнить",
                                "mode": []
                            },
                            {
                                "type": "link_span",
                                "text": "анкету ",
                                "src": "https://germania.diplo.de/ru-ru/service/05-VisaEinreise/langfristigerAufenthalt/-/1611410?openAccordionId=item-1611410-3-panel"
                            },
                            {
                                "type": "span",
                                "text": "на получение национальной визы",
                                "mode": []
                            }
                        ]
                    },
                    {
                        "type": "tp",
                        "text": "Взять гражданский и заграничный паспорт с копиями"
                    },
                    {
                        "type": "tp",
                        "text": "Сфотографироваться на визу"
                    }
                ]
            },
            {
                "type": "tp",
                "text": "Разумеется, все документы должны оформляться на немецком языке."
            },
            {
                "type": "paragraph",
                "children": [
                    {
                        "type": "span",
                        "text": "Интересный нюанс: есть несоответствие информации на сайте",
                        "mode": []
                    },
                    {
                        "type": "link_span",
                        "text": "представительства Германии в России",
                        "src": "https://germania.diplo.de/ru-ru"
                    },
                    {
                        "type": "span",
                        "text": "и реальных требований. А именно: сертификат, подтверждающий знания немецкого языка предоставлять не нужно. Кроме того, не нужно иметь договор аренды жилья в Германии, хотя этот пункт присутствует в анкете. Напомню, это не нужно если вам открывают рабочую визу с последующим оформлением «blue card», но если вы переезжаете по-другому алгоритму — и документы также могут потребоваться другие.",
                        "mode": []
                    }
                ]
            },
            {
                "type": "tp",
                "text": "В немецком посольстве все были предельно вежливыми, белыми и пушистыми."
            },
            {
                "type": "tp",
                "text": "Однако, но из-за какой-то внутренней ошибки они чуть было не выдали мне рабочую визу на 3 месяца вместо 6-и. Так что доверяй, но проверяй!"
            },
            {
                "type": "tp",
                "text": "Да, немцы тоже косячат."
            }
        ]
    }
}

Вообще, сложно спорить с тем, что XML удобнее, чтобы представлять rich-text.
Только взгляните на этого монстра, если представить rich-text в формате JSON:


{
    "type": "paragraph",
    "children": [
        {
            "type": "span",
            "text": "Также необходимо проверить факт соответствия диплома по базе данных",
            "mode": []
        },
        {
            "type": "link_span",
            "text": "Anabin",
            "src": "https://anabin.kmk.org/anabin.html"
        },
        {
            "type": "span",
            "text": ".",
            "mode": []
        }
    ]
}

Примечание: mode используется для того, чтобы представлять такие значения как bold, italic, strike и группировать их, просто пример не очень удачный.


Столько символов, чтобы представить один параграф с одной ссылкой.


Удобно ли это? Не уверен, легко можно выяснить какие стили у конкретного span без необходимости ходить для поиска предыдущих нод, но, например, в андроид есть стандартный TextView, который без проблем сработается с простеньким html


Все это я, конечно, переводил не в ручную (кроме некоторых кейсов). Я брал некоторые статьи и прогонял html через свою небольшую консольную утилиту на Dart, её результаты работы вы и увидели. Да, она написана не идеально, но для проверки концепта — сойдет.


Потыркать утилиту и посмотреть код можно по ссылке на мой репозиторий.


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


Бенчмарк


Что я вообще сравниваю? Мне интересно 2 вещи:


  1. Есть ли разница в скорости парсинга json с заинлайненым html и json с представлением статьи в виде json'а.
  2. Насколько быстро распарсить два формата вложенных друг в друга.

Зачем тестировать второй вариант? Если вы используете React, например, вместе с библиотекой "html_to_react" (внутри она использует "html-dom-parser"), то, думаю, вам будет интересно узнать об альтернативе.


Кратко опишу методику тестирования:


  • Я заранее перевел с помощью утилиты пару хабровских статей в json.
  • Есть три операции, которые, тестируются:
    1. Скорость парсинга статьи без преобразований (статья полностью в том виде в котором его выдает по апи хабр, в html)
    2. Скорость парсинга если html заменить на преобразованный моей утилитой json
    3. Скорость парсинга когда парсим сначала json а после парсим html внутри.
  • тестируем библиотеку только для построения dom дерева, замеры для "html_to_react" не проводились

Код бенчмарка:


import Benchmark from "benchmark";
import fs from 'fs';
import htmlToDOM from 'html-dom-parser';

function parse(str) {
    return JSON.parse(str);
}

function prepareAndParse(str) {
    return htmlToDOM(JSON.parse(str)["textHtml"]);
}

const suiteName = '526574'
const defaultHtml = fs.readFileSync(`./test_data/${suiteName}/html.json`);
const jHtml = fs.readFileSync(`./test_data/${suiteName}/jhtml.json`);

const suite = new Benchmark.Suite;

suite.add('Json c html2json', () => parse(jHtml))
suite.add('Html в json', () => parse(defaultHtml))
suite.add('Html в json и парсинг html-dom-parser', () => prepareAndParse(defaultHtml))

suite.on('complete', function() {
    console.log('Fastest is ' + this.filter('fastest').map('name'));
}).on('cycle', function(event) {
    console.log(String(event.target));
})

suite.run()

Результаты тестирования


После запуска бенчмарка мы получим такие результаты:


Json c html2json x 1,892 ops/sec ±0.79% (88 runs sampled)
Html в json x 1,917 ops/sec ±0.81% (89 runs sampled)
Html в json и парсинг html-dom-parser x 798 ops/sec ±3.04% (86 runs sampled)

  1. Скорость парсинга статьи полностью из json'а и с html внутри json'а по производительности не имеет значительной разницы.
  2. Производительность при двух проходах парсинга (сначала парсим json, а потом html) примерно в 2 раза меньше и не в пользу html.

Плюсы и минусы


плюсы:


  1. Снижение нагрузки на клиентские устройства: cделав подобное преобразование на сервере, в некоторых ситуациях можно немного ослабить нагрузку на гаджет пользователя и экономить заряд батареи
  2. Независимость от HTML и CSS: Мы не зависим от способа представления кнопки, спойлера или цитаты на клиенте, от css классов и атрибутов. Это позволяет создавать на основе данных с сервера нативные виджеты для Android, iOS, Flutter и десктопа без использования WebView или подобных решений.
  3. Минимизация зависимостей: Избавление от лишних зависимостей упрощает разработку и поддержку.

минусы:


  1. Размер: json весит незначительно, но больше чем аналогичный html


    возможно и обратное, если:


    • у вас особый формат, размер которого лучше поддается оптимизации
    • вы захотите использовать бинарный формат для представления ваших данных (забавно — bson оказался в моем случае даже больше чем стандартный json), тогда получится сократить размер, но опять же это достаточно трудоемко, а фичи не ждут.

  2. Необходимость конвертации: json придется в любом случае конвертировать в нативные для данной платформы виджеты будь то веб с его html или нативный мир уже с собственными особенностями


    И при этом не понятно, когда это лучше делать в случае веб-приложений:


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

  3. Сложность HTML: html очень многогранный с широким функционалом. Если необходимо поддерживать множество его возможностей, это может потребовать значительных усилий и привести к созданию сложностей, сопоставимых с разработкой браузера.
  4. Отсутствие стандартизации: Разработка собственного решения может обернуться созданием уникального велосипеда, который придется поддерживать самостоятельно.

Это все, конечно, хорошо, но как можно это применить?


Мой небольшой список:


  1. Создание платформо-независимого генерируемого в рантайме ui
  2. Отказ от редакторов, использующих html. Написать собственный редактор получится проще и, скорее всего, он будет безопаснее, т.к. встроить зловредный javascript будет сложнее.

Заключение


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

Tags:
Hubs:
Total votes 7: ↑4 and ↓3+3
Comments32

Articles