Олег Марков
Директива v-html во Vue.js - полный разбор и практические примеры
Введение
Директива v-html во Vue отвечает за прямую вставку HTML-кода в шаблон компонента. С ее помощью вы можете отрендерить разметку, которая приходит из базы данных, с сервера, из CMS или формируется динамически в рантайме.
Смотрите, я покажу вам, как это выглядит в простейшем виде:
<div id="app">
<!-- Здесь мы говорим Vue вставить HTML из свойства rawHtml -->
<div v-html="rawHtml"></div>
</div>
<script>
const app = Vue.createApp({
data() {
return {
// В это поле может прийти строка с HTML с сервера
rawHtml: '<strong>Привет, мир</strong>'
}
}
})
app.mount('#app')
</script>
Как видите, Vue не экранирует содержимое rawHtml, а именно вставляет его как HTML: тег <strong> отрабатывает, а не показывается как текст.
Но за такой гибкостью скрывается важный риск — XSS-уязвимости и неконтролируемое выполнение чужого кода. Поэтому давайте разберемся подробно:
- как именно работает v-html;
- чем она отличается от обычной интерполяции;
- в каких случаях ее стоит применять;
- как сделать это максимально безопасно;
- какие есть альтернативы и типичные ошибки.
Что делает директива v-html
Базовая идея
v-html говорит Vue: "Не обрабатывай строку как обычный текст, вставь ее как HTML-разметку". То есть движок рендера не экранирует угловые скобки и специальные символы, а отдает всё на обработку браузеру как HTML.
Если без v-html вы бы писали так:
<p>{{ text }}</p>
то с v-html — так:
<p v-html="text"></p>
Разница в том, как браузер увидит результат:
data() {
return {
text: '<strong>Важное сообщение</strong>'
}
}
{{ text }}→ на странице появится буквально<strong>Важное сообщение</strong>как текст.v-html="text"→ на странице будет жирный текст "Важное сообщение".
Где можно использовать v-html
Вы можете использовать v-html на любом стандартном HTML-элементе:
<div v-html="htmlContent"></div>
span v-html="inlineHtml"></span>
td v-html="tableCellHtml"></td>
Но обычно стараются использовать его на "контейнерных" элементах: div, section, article, потому что так проще визуально контролировать участок динамического HTML.
Синтаксис и типичные примеры
Простой пример с динамическим HTML
Давайте разберемся на базовом примере, когда HTML приходит с сервера:
<div id="app">
<!-- Вставляем HTML, полученный из API -->
<article v-html="postBody"></article>
</div>
<script>
const app = Vue.createApp({
data() {
return {
postBody: '' // сюда будет записан HTML ответа
}
},
created() {
// Здесь я размещаю пример асинхронной загрузки
fetch('/api/post/1')
.then(response => response.json())
.then(data => {
// Предположим, что сервер вернул поле htmlBody
// с уже подготовленной HTML-разметкой
this.postBody = data.htmlBody
})
}
})
app.mount('#app')
</script>
Комментарии к примеру:
postBody— строка с HTML;v-html="postBody"— вставляет разметку вarticle;- компонент будет автоматически перерендерен, когда
postBodyобновится.
Использование вычисляемых свойств с v-html
Иногда вам нужно подготовить HTML на основе данных в компоненте. Вы можете сделать это через вычисляемое свойство:
<div id="app">
<!-- Здесь мы используем вычисляемое свойство htmlMessage -->
<div v-html="htmlMessage"></div>
</div>
<script>
const app = Vue.createApp({
data() {
return {
username: 'Алексей',
isPremium: true
}
},
computed: {
htmlMessage() {
// Формируем строку с HTML на основе состояния
let base = `Здравствуйте, <strong>${this.username}</strong>`
// Добавляем дополнительный HTML, если пользователь премиум
if (this.isPremium) {
base += ' <span style="color: gold;">(премиум)</span>'
}
return base
}
}
})
app.mount('#app')
</script>
Здесь вы видите, как логика компонента влияет на итоговую HTML-разметку, которая потом вставляется через v-html.
Отличие v-html от интерполяций и v-text
Интерполяция {{ }} и экранирование
При использовании интерполяции Vue всегда экранирует HTML, чтобы защититься от XSS по умолчанию:
<p>{{ rawHtml }}</p>
Если rawHtml = '<em>Текст</em>', браузер отрисует именно текст <em>Текст</em>, а не курсив.
Это безопасно, но не дает вам возможности вставлять полноценный HTML.
v-text — аналог интерполяции
Директива v-text делает почти то же самое, что и {{ }}, но в виде атрибута:
<p v-text="rawHtml"></p>
Результат будет таким же, как при интерполяции: HTML внутри строки будет показан как текст.
v-html — отключение экранирования
С v-html вы сознательно отключаете экранирование:
<p v-html="rawHtml"></p>
И теперь строка будет интерпретироваться как HTML.
По сути:
{{ }}/ v-text — "покажи как текст, экранируя HTML";- v-html — "покажи как HTML, не экранируя, я сам отвечаю за безопасность".
Важные ограничения и особенности работы v-html
Нельзя использовать внутри v-html директивы Vue
Одна из ключевых особенностей: HTML, вставленный через v-html, не компилируется Vue как шаблон.
Смотрите пример:
<div id="app">
<!-- Пытаемся вставить шаблон с {{ }} -->
<div v-html="htmlWithMustache"></div>
</div>
<script>
const app = Vue.createApp({
data() {
return {
// Обратите внимание, мы пробуем использовать интерполяцию внутри строки
htmlWithMustache: '<p>Привет, {{ username }}</p>',
username: 'Ирина'
}
}
})
app.mount('#app')
</script>
На экране вы увидите Привет, {{ username }} — Vue не будет подставлять значение username внутрь HTML-строки, потому что этот HTML вставляется уже после компиляции шаблона.
То же самое касается других директив:
v-if;v-for;v-on(@click);v-bind(:).
Они не будут "оживать" внутри вставленного HTML.
Если вам нужно управлять поведением, событиями и реактивностью, лучше использовать обычный шаблон с компонентами, а не v-html.
События и реактивность
HTML, вставленный через v-html, не превращается в реактивный шаблон Vue:
- вы не можете вешать обработчики Vue через
@clickвнутри строки с HTML; - данные внутри
{{ }}не подставляются; - жизненный цикл такого "подшитого HTML" не связан с жизненным циклом компонента.
Если вам нужно добавлять интерактивный контент, подумайте о:
- создании подкомпонентов;
- использовании слотов;
- генерации разметки через
v-forи условные блокиv-if.
Безопасность v-html и XSS
Почему v-html потенциально опасен
Когда вы вставляете HTML без экранирования, вы даете возможность:
- вставлять
<script>теги; - вызывать inline-обработчики событий, например
onclick="..."; - использовать опасные URL вида
javascript:alert('xss')в ссылках.
Если такой контент формируется пользователем или приходит из ненадежного источника, это прямой путь к XSS.
XSS (Cross-Site Scripting) — уязвимость, когда злоумышленник может внедрить свой JavaScript-код в страницу и заставить браузер пользователя его выполнить.
Пример опасного HTML:
data() {
return {
// Пользователь ввел это в форму, а мы сохранили и показываем как есть
commentHtml: '<img src="x" onerror="alert(\'XSS\')" />'
}
}
Если вывести commentHtml через v-html, этот код выполнится при загрузке картинки.
Золотое правило использования v-html
Используйте v-html только тогда, когда:
Источник HTML контролируется вами:
- шаблоны в коде приложения;
- заранее подготовленный контент в базе, который вы не даете редактировать пользователям свободно;
- HTML, который вы сами сгенерировали с проверкой.
Или когда вы уверены, что контент прошел надежную очистку на бэкенде или на фронтенде с помощью проверенного HTML sanitizer.
Очищаем HTML перед вставкой
Частый практический подход — использовать библиотеку для "санитайза" HTML, например DOMPurify.
Теперь давайте посмотрим, что происходит в таком примере:
<div id="app">
<!-- Вставляем уже очищенный HTML -->
<div v-html="safeHtml"></div>
</div>
<script src="https://unpkg.com/dompurify@3.1.3/dist/purify.min.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
rawHtml: '' // сюда попадает "грязный" HTML
}
},
computed: {
safeHtml() {
// Здесь мы очищаем HTML перед вставкой
return DOMPurify.sanitize(this.rawHtml)
}
},
created() {
// Здесь может быть загрузка HTML с сервера
// Для примера положим возможный вредоносный код
this.rawHtml = `
<p>Нормальный текст</p>
<img src="x" onerror="alert('XSS')" />
`
}
})
app.mount('#app')
</script>
Комментарии:
rawHtmlможет содержать любой HTML;safeHtml— результат очистки, где удалены опасные атрибуты и теги;- в шаблоне вы всегда используете v-html только с
safeHtml, а не с "сырым"rawHtml.
Санитайз на бэкенде
Еще более надежный подход — очищать HTML на сервере:
- так вы защищаете и другие клиенты (мобильные приложения, админки и т.п.);
- не полагаетесь на реализацию фронтенда;
- можете хранить уже очищенный контент.
Во Vue тогда просто рендерите уже проверенный HTML:
<div v-html="post.safeBodyHtml"></div>
Здесь safeBodyHtml — подготовленное безопасное поле, которое выдает API.
Практические сценарии использования v-html
1. Вывод контента из CMS или блога
Представьте, что у вас есть блог, в котором статьи набираются в редакторе (WYSIWYG: TinyMCE, CKEditor и т.п.). В базе хранится HTML:
<article v-html="post.htmlBody"></article>
Где:
post.htmlBody— HTML, сгенерированный редактором;- вы можете дополнительно ограничивать набор допустимых тегов на уровне редактора (жесткие настройки WYSIWYG) и бэкенда.
Здесь v-html вполне оправдан, потому что контент:
- контролируется редакторами/админами;
- проходит проверку при сохранении.
2. Отображение форматированного текста пользователя с Markdown
Частый паттерн: вы даете пользователю писать в Markdown, а на сервере или фронтенде конвертируете это в HTML.
Схема:
- Пользователь вводит Markdown.
- Бэкенд (или фронтенд) превращает Markdown в HTML.
- Вы очищаете результат санитайзером.
- Вставляете HTML через v-html.
Пример на фронтенде с marked + DOMPurify:
<div id="app">
<!-- Выводим HTML, полученный из Markdown и очищенный -->
<div v-html="renderedMarkdown"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://unpkg.com/dompurify@3.1.3/dist/purify.min.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
markdown: '# Заголовок\n\n**Жирный текст**'
}
},
computed: {
renderedMarkdown() {
// Конвертируем Markdown в HTML
const html = marked.parse(this.markdown)
// Очищаем HTML
return DOMPurify.sanitize(html)
}
}
})
app.mount('#app')
</script>
Такой подход часто используют в форумах, wiki, документации.
3. Внедрение фрагментов HTML-шаблонов из конфигурации
Иногда кусочки HTML удобно хранить в конфиге или базе. Например, баннеры, рекламные блоки, HTML для модальных окон.
Здесь вы можете:
- хранить только "белый список" заранее одобренных шаблонов;
- не давать пользователю лезть в HTML руками;
- рендерить их через v-html:
<div v-html="bannerHtml"></div>
Чего не стоит делать с v-html
Не рендерить пользовательский ввод напрямую
Опасный антипример:
<div id="app">
<!-- НИКОГДА так не делайте с "сырым" вводом пользователя -->
<textarea v-model="userInput"></textarea>
<div v-html="userInput"></div>
</div>
<script>
const app = Vue.createApp({
data() {
return {
// Здесь содержится значение из текстового поля
userInput: ''
}
}
})
app.mount('#app')
</script>
Если пользователь введет:
<script>alert('XSS')</script>
или
<img src="x" onerror="alert('XSS')" />
этот код выполнится. Это классическая XSS.
Если вам нужно показывать пользователю то, что он написал, используйте интерполяцию:
<div>{{ userInput }}</div>
либо конвертируйте в безопасный формат (Markdown → HTML + санитайз).
Не пытаться делать через v-html "мини-шаблонизатор"
Иногда пытаются хранить шаблоны вида:
const template = '<p>Привет, {{ username }}</p>'
и надеются, что Vue подставит username внутри v-html. Как мы уже обсудили, этого не произойдет.
Если вам нужны шаблоны с подстановкой данных:
- используйте компоненты;
- используйте слоты;
- используйте обычный шаблон Vue, а не строки с HTML.
Альтернативы v-html
Шаблоны компонентов и слоты
Вместо того, чтобы хранить HTML как строку, вы можете описать структуру прямо в шаблоне:
<user-card>
<!-- Это слот, в который вы передаете разметку -->
<template #description>
<p>Привет, я разработчик Vue</p>
<p>Люблю понятные интерфейсы</p>
</template>
</user-card>
Такой подход:
- полностью управляем;
- компилируется Vue;
- поддерживает все директивы и реактивность.
Подходит, когда HTML известен на этапе разработки, а не приходит извне.
Генерация разметки через v-for
Вместо хранения HTML-строк, удобнее часто хранить структуру данных, а разметку генерировать:
<ul>
<!-- Давайте генерировать список по данным, а не из строки HTML -->
<li v-for="item in items" :key="item.id">
<strong>{{ item.title }}</strong>
<p>{{ item.text }}</p>
</li>
</ul>
Это:
- безопаснее;
- проще поддерживать;
- позволяет добавлять реактивные свойства, обработчики и условия.
v-html в Vue 2 и Vue 3 — отличия
В целом, поведение v-html в Vue 2 и Vue 3 одинаковое по смыслу:
- вставка "сырого" HTML;
- отсутствие компиляции Vue внутри вставленного HTML;
- те же риски безопасности.
Основные отличия касаются только "окружения":
- в Vue 3 вы чаще будете видеть
Vue.createApp, а неnew Vue; - шаблонная часть и семантика директив не менялась.
Пример на Vue 2:
<div id="app">
<div v-html="rawHtml"></div>
</div>
<script>
new Vue({
el: '#app',
data() {
return {
rawHtml: '<strong>Текст</strong>'
}
}
})
</script>
Пример на Vue 3:
<div id="app">
<div v-html="rawHtml"></div>
</div>
<script>
const app = Vue.createApp({
data() {
return {
rawHtml: '<strong>Текст</strong>'
}
}
})
app.mount('#app')
</script>
Сама директива работает одинаково.
Советы по проектированию с учетом v-html
Минимизируйте область применения v-html
Хороший подход — держать v-html максимально "локальным":
- создавайте отдельный компонент, который отвечает за показ HTML;
- внутри него реализуйте:
- очистку;
- защиту;
- дополнительные ограничения.
Например:
// Компонент SafeHtml.vue
export default {
name: 'SafeHtml',
props: {
html: {
type: String,
required: true
}
},
computed: {
safeHtml() {
// Здесь вы можете применить DOMPurify или серверный санитайз
return DOMPurify.sanitize(this.html)
}
},
template: `
<div v-html="safeHtml"></div>
`
}
И использовать так:
<safe-html :html="post.htmlBody" />
Так вы централизуете контроль безопасности и не размазываете логику очистки по всему приложению.
Четко разделяйте "данные" и "представление"
Если вы чувствуете, что в базу начинают попадать фрагменты HTML-шаблонов, которые могли бы быть представлены структурированными данными, это сигнал пересмотреть архитектуру.
Например, вместо хранения:
<p><strong>Важно</strong> Текст уведомления</p>
можно хранить:
{
"type": "warning",
"text": "Текст уведомления"
}
А потом в Vue отрисовывать это аккуратным компонентом:
<alert :type="message.type">
{{ message.text }}
</alert>
Так вы:
- уменьшаете риск XSS;
- упрощаете рефакторинг дизайна;
- контролируете возможные виды разметки.
Заключение
Директива v-html во Vue дает удобный способ рендерить динамический HTML, но вместе с этим перекладывает на вас ответственность за безопасность.
Ключевые моменты, которые стоит помнить:
- v-html выводит HTML без экранирования — это его основная суть;
- вставленный через v-html контент не компилируется Vue как шаблон:
- внутри не работают директивы;
- не подставляются
{{ }}; - не работают
@clickи прочие обработчики Vue;
- используйте v-html только для проверенного, контролируемого контента;
- для всего, что может прийти от пользователей или внешних систем, применяйте санитайзеры (DOMPurify или аналог на стороне сервера);
- по возможности предпочитайте компоненты, слоты и генерацию разметки по структуре данных, а не по строкам HTML.
Если вы будете относиться к v-html как к "инструменту повышенной опасности", он станет полезным помощником, а не источником скрытых уязвимостей.
Частозадаваемые технические вопросы по директиве v-html
1. Как добавить обработчик клика к элементу внутри v-html
Директивы Vue внутри v-html не работают, поэтому @click внутри строки HTML не сработает.
Варианты решения:
- Вешать нативные обработчики через делегирование:
mounted() {
// Здесь мы навешиваем обработчик на контейнер
this.$el.addEventListener('click', event => {
// Проверяем, кликнули ли по нужному элементу
if (event.target.matches('.dynamic-button')) {
// Выполняем нужное действие
this.handleDynamicClick()
}
})
}
- По возможности отказаться от v-html и собрать нужный HTML как обычный шаблон с
v-for,v-ifи компонентами.
2. Почему стили из scoped-стилей не применяются к контенту v-html
Если вы используете <style scoped>, Vue добавляет специальные атрибуты элементам в шаблоне. Контент, вставленный через v-html, эти атрибуты не получает, поэтому селекторы из scoped-стилей его "не видят".
Решения:
- вынесите нужные стили в глобальные (без scoped);
- или используйте более общие селекторы, привязанные к контейнеру:
.rich-content p {
margin-bottom: 8px;
}
и в шаблоне:
<div class="rich-content" v-html="htmlContent"></div>
3. Как ограничить список допустимых тегов в v-html
Vue сам по себе не фильтрует теги. Вам нужен слой фильтрации/санитайза.
На фронтенде:
const safeHtml = DOMPurify.sanitize(unsafeHtml, {
ALLOWED_TAGS: ['p', 'strong', 'em', 'ul', 'li', 'a']
})
На бэкенде — используйте библиотеки, аналогичные DOMPurify, с явным указанием разрешенных тегов и атрибутов, и храните уже очищенные строки.
4. Можно ли использовать v-html вместе с серверным рендерингом (SSR)
Да, v-html работает с SSR. Но:
- HTML должен быть безопасным уже на этапе рендеринга на сервере;
- избегайте вставки содержимого, зависящего от
windowили DOM; - помните, что содержимое v-html не будет "гидратировано" Vue как шаблон — события нужно навешивать отдельно (через делегирование или отдельные компоненты).
5. Как тестировать компоненты, использующие v-html
В юнит-тестах (например, с Vue Test Utils):
- монтируйте компонент;
- находите элемент с v-html через селектор;
- проверяйте
element.innerHTML.
Пример:
it('renders raw HTML', () => {
const wrapper = mount(MyComponent, {
props: { html: '<strong>Test</strong>' }
})
const container = wrapper.find('.html-container')
// Проверяем, что HTML вставился как есть
expect(container.element.innerHTML).toBe('<strong>Test</strong>')
})
Если вы используете санитайз, подставляйте "грязный" HTML и проверяйте, что опасные части удалены.
Постройте личный план изучения Vue до уровня Middle — бесплатно!
Vue — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Vue
Лучшие курсы по теме

Vue 3 и Pinia
Антон Ларичев
TypeScript с нуля
Антон Ларичев