一、问题背景与挑战分析
在现代前端开发中,国际化(i18n)已成为多语言应用的标配。Vue.js 生态中的 vue-i18n 是最广泛使用的国际化插件之一。然而,当开发者尝试在翻译文本中嵌入 HTML 标签(如 <strong>、<br /> 或自定义组件)时,会遇到一个核心问题:默认情况下,vue-i18n 将所有翻译内容视为纯文本输出,导致 HTML 被转义为字符实体,无法正确渲染。
例如:
{
"welcome": "欢迎使用我们的产品
请登录以继续"
}
若直接通过 {{ $t('welcome') }} 渲染,<br /> 将显示为文本而非换行。虽然可使用 v-html 指令绕过转义,但这种方式存在严重的安全风险——若翻译内容来自用户输入或不可信来源,可能引发 XSS(跨站脚本攻击)。
二、从浅入深的技术演进路径
- 初级方案:v-html + 手动转义 —— 最直接但风险最高,适用于完全可控的静态翻译资源。
- 中级方案:HTML 实体编码 + CSS 替代 —— 使用非语义化方式实现视觉效果,牺牲语义结构。
- 高级方案:插值标记 + 组件化渲染 —— 利用 vue-i18n 的命名/列表插值机制,结合动态组件和受控 HTML 注入。
- 企业级方案:自定义指令 + 白名单 sanitizer —— 构建安全的富文本渲染管道,支持动态参数与组件嵌套。
三、主流解决方案对比表
| 方案 | 安全性 | 灵活性 | 维护成本 | 适用场景 |
|---|
| v-html 直接渲染 | 低 | 高 | 低 | 内部系统、可信内容 |
| 预处理 HTML 实体 | 中 | 低 | 中 | 简单格式化需求 |
| 插值占位符 + slot 分发 | 高 | 高 | 高 | 复杂 UI 结构、动态数据 |
| Sanitizer + v-safe-html | 极高 | 中 | 中 | 用户生成内容、高安全要求 |
四、推荐实践:基于插值与组件封装的安全渲染
vue-i18n 提供了强大的 命名插值(Named Interpolation) 功能,允许我们在翻译字符串中预留占位符,并在运行时注入 Vue 组件或 HTML 片段。以下是一个典型实现模式:
// en.json
{
"terms": "By continuing, you accept our {tos} and {privacy}"
}
// Template
Terms of Service
Privacy Policy
上述代码利用了 <i18n-t> 组件(vue-i18n 9+ 推荐),它能自动识别插值槽位并将对应的内容插入指定位置,避免了直接操作 HTML 字符串的风险。
五、流程图:安全渲染决策路径
graph TD
A[是否需要渲染HTML?] -->|否| B[使用 $t() 直接输出]
A -->|是| C{内容来源是否可信?}
C -->|是| D[使用 i18n-t + 插槽组件]
C -->|否| E[启用 HTML Sanitizer]
E --> F[白名单过滤标签属性]
F --> G[通过 v-safe-html 渲染]
六、进阶技巧:动态参数与运行时组件绑定
对于更复杂的场景,如根据用户角色动态插入不同样式的强调标签,可以结合 computed 属性与动态组件:
computed: {
highlightComponent() {
return this.user.isAdmin ? 'admin-highlight' : 'user-emphasis'
},
linkProps() {
return {
to: '/guide',
target: '_blank'
}
}
}
模板中:
<i18n-t keypath="dynamicContent" tag="p">
<template #highlight>
<component :is="highlightComponent" class="text-accent">重要通知</component>
</template>
<template #link>
<router-link v-bind="linkProps">查看详情</router-link>
</template>
</i18n-t>
此方法实现了逻辑与表现分离,同时保障了 XSS 防护边界清晰。
七、安全加固:构建可复用的 v-safe-html 指令
针对必须使用原始 HTML 字符串的遗留系统,建议封装一个带 sanitizer 的自定义指令:
import DOMPurify from 'dompurify'
const safeHtml = {
mounted(el, binding) {
const clean = DOMPurify.sanitize(binding.value, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'br'],
ALLOWED_ATTR: []
})
el.innerHTML = clean
},
updated(el, binding) {
// 同上
}
}
app.directive('safe-html', safeHtml)
随后在模板中使用:v-safe-html="$t('htmlContent')",即可在保留基本格式的同时抵御恶意脚本注入。