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

В чем проблема хранить html в БД?
Проблемы проявляются в долгосроке. Рассмотрим наш гипотетический веб-сервис, в базе данных которого мы храним html для отображения некоторого контента.
Во-первых, фронтенд вашего сервиса должны поддерживать и периодически обновлять дизайн, это привычная практика, раз в три-пять лет происходят редизайны.
Соответственно, рано или поздно возникнет потребность в сильной переработке визуала. Фронтендеру будет необходимо обновить пользовательский интерфейс, для этого он должен учитывать, что на сервере весь HTML в БД является статичным и, возможно, даже есть устаревшие UI-элементы или, например, легаси CSS.
Волшебным образом HTML с CSS-классами не поменяются, для этого уже потребуется вмешательство второго разработчика — бэкендера, ему нужно будет провести ряд манипуляций по миграции одного формата вёрстки в другой, и хорошо, если это делается редко. Если это не так и изменения происходят часто, то это будет большая боль и постоянная трата ресурсов команды.
Можно, например, посмотреть, что выдается для мобильной версии фронтенда Хабра под недавним мегапостом ссылка на апи. Автоматизированно, без вмешательства человека, такие данные модифицировать ��ложно.

Некоторое количество заинлайненого css и javascript (около половины поста генерируется им) осталось за кадром.
Во-вторых, абсолютно точно не стоит забывать про нативные приложения. И если ваш сервис сейчас ориентируется только на веб, то с течением времени это может измениться.
Мобильные приложения — де-факто стандарт, каждая уважающая компания, ориентированная на расширение пользовательской аудитории, создаст мобильное приложение. Как разработчик вы должны быть к этому готовы. Также не стоит списывать десктопные приложения со счетов, они пользуются спросом как в b2b, так и в b2c, и привлекательны для аудитории за счет удобства.
В этот момент, когда у бизнеса появилась потребность, у команды нативной разработки появится проблема, бекенд отдает html верстку. В этом случае есть два основных пути решения:
Использовать WebView, что сразу заметно глазу пользователей. И да, такой подход жизнеспособен, он решает задачу, но 1) часто приводит к просадке перформанса и 2) следующее из первого, команда на борьбу с WebView в последствии начнёт тратить сил больше, чем эта технология на первом этапе сэкономила сил.
Использовать транслятор 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 вещи:
- Есть ли разница в скорости парсинга json с заинлайненым html и json с представлением статьи в виде json'а.
- Насколько быстро распарсить два формата вложенных друг в друга.
Зачем тестировать второй вариант? Если вы используете React, например, вместе с библиотекой "html_to_react" (внутри она использует "html-dom-parser"), то, думаю, вам будет интересно узнать об альтернативе.
Кратко опишу методику тестирования:
- Я заранее перевел с помощью утилиты пару хабровских статей в json.
- Есть три операции, которые, тестируются:
- Скорость парсинга статьи без преобразований (статья полностью в том виде в котором его выдает по апи хабр, в html)
- Скорость парсинга если html заменить на преобразованный моей утилитой json
- Скорость парсинга когда парсим сначала 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)- Скорость парсинга статьи полностью из json'а и с html внутри json'а по производительности не имеет значительной разницы.
- Производительность при двух проходах парсинга (сначала парсим json, а потом html) примерно в 2 раза меньше и не в пользу html.
Плюсы и минусы
плюсы:
- Снижение нагрузки на клиентские устройства: cделав подобное преобразование на сервере, в некоторых ситуациях можно немного ослабить нагрузку на гаджет пользователя и экономить заряд батареи
- Независимость от HTML и CSS: Мы не зависим от способа представления кнопки, спойлера или цитаты на клиенте, от css классов и атрибутов. Это позволяет создавать на основе данных с сервера нативные виджеты для Android, iOS, Flutter и десктопа без использования WebView или подобных решений.
- Минимизация зависимостей: Избавление от лишних зависимостей упрощает разработку и поддержку.
минусы:
Размер: json весит незначительно, но больше чем аналогичный html
возможно и обратное, если:
- у вас особый формат, размер которого лучше поддается оптимизации
- вы захотите использовать бинарный формат для представления ваших данных (забавно — bson оказался в моем случае даже больше чем стандартный json), тогда получится сократить размер, но опять же это достаточно трудоемко, а фичи не ждут.
Необходимость конвертации: json придется в любом случае конвертировать в нативные для данной платформы виджеты будь то веб с его html или нативный мир уже с собственными особенностями
И при этом не понятно, когда это лучше делать в случае веб-приложений:
- Можно конвертировать прямо на клиенте, что удобно в случае, если вы используете кастомные виджеты.
- Можно конвертировать на сервере, а на фронтенд отправлять уже все готовое.
- Сложность HTML: html очень многогранный с широким функционалом. Если необходимо поддерживать множество его возможностей, это может потребовать значительных усилий и привести к созданию сложностей, сопоставимых с разработкой браузера.
- Отсутствие стандартизации: Разработка собственного решения может обернуться созданием уникального велосипеда, который придется поддерживать самостоятельно.
Это все, конечно, хорошо, но как можно это применить?
Мой небольшой список:
- Создание платформо-независимого генерируемого в рантайме ui
- Отказ от редакторов, использующих html. Написать собственный редактор получится проще и, скорее всего, он будет безопаснее, т.к. встроить зловредный javascript будет сложнее.
Заключение
В качестве итога я бы хотел сказать, что не предлагаю всем разом переходить на описанный мной в данной статье подход. В первую очередь мне интересно мнение других разработчиков, как бекенд, так и фронтенд. Скорее всего, я не учел нюансы и готов выслушать и обсудить критику и замечания.
