字符串处理是前端开发中频率极高的操作,Vue 3 在模板层面提供了插值表达式、指令绑定等机制,而具体业务中的拼接、转义、格式化则需要开发者自己把控。不少 XSS 漏洞的根源就是字符串没经过转义就被当作 HTML 渲染,同样,混乱的拼接方式也会让模板变得难以维护。本文从模板、转义、格式化三个角度出发,梳理 Vue 3 中字符串处理的正确姿势。

模板中的字符串:插值表达式与模板字符串的正确用法
Vue 3 的插值表达式使用双花括号语法,渲染时会自动将内容作为纯文本处理。这一点非常关键:即使数据里包含 <script> 这样的标签,页面上也只会显示原始文字,浏览器不会执行它。这种默认转义机制是 Vue 防御 XSS 的第一道防线,日常展示动态文本时应优先使用插值而不是 v-html。
在拼接字符串时,推荐使用 ES6 的模板字符串配合插值表达式,而不是用加号在模板里硬拼。对比下面两种写法:
<!-- 不推荐:加号拼接,可读性差 -->
<span>{{ user.lastName + ' ' + user.firstName }}</span>
<!-- 推荐:模板字符串,结构清晰 -->
<span>{{ `${user.lastName} ${user.firstName}` }}</span>当拼接逻辑变复杂时,更好的做法是把逻辑收进 computed 计算属性,让模板只负责展示。这样既便于单元测试,也避免了模板里塞满表达式。例如拼接一个带前缀的状态文案:
import { computed, ref } from 'vue'
const status = ref('pending')
const statusText = computed(() => {
const map = {
pending: '待处理',
done: '已完成',
error: '发生错误'
}
return `[${new Date().toLocaleDateString()}] ${map[status.value] ?? '未知状态'}`
})另一个细节是属性绑定。如果要动态绑定类名或标题等属性,插值表达式不能用在属性位置,必须使用 v-bind 的缩写写法,例如 :title="`点击查看${name}的详情`"。这里同样遵循自动转义规则,属性值中出现的引号或尖括号都会被正确编码,不会破坏标签结构。
转义与安全:v-html 的风险边界和替代方案
v-html 指令会把字符串当作原始 HTML 插入 DOM,Vue 不会对它做任何转义。官方文档明确警告:永远不要对用户提供的内容使用 v-html。一旦用户输入中混入了 <img onerror=...> 之类的 payload,就会直接执行,造成 XSS 攻击。
实际项目中确实存在需要渲染富文本的场景,比如后台管理系统展示管理员配置的公告。这时有两类处理思路。第一类是后端预过滤:在入库前用白名单方式清洗 HTML,只保留允许的标签和属性。第二类是前端兜底,引入 DOMPurify 这类成熟库做净化:
import DOMPurify from 'dompurify'
import { computed, ref } from 'vue'
const rawHtml = ref('<img src=x onerror=alert(1)><p>公告内容</p>')
const safeHtml = computed(() => DOMPurify.sanitize(rawHtml.value, {
ALLOWED_TAGS: ['p', 'strong', 'em', 'a', 'br'],
ALLOWED_ATTR: ['href']
}))然后在模板中使用 v-html="safeHtml",即便原始数据被污染,危险节点也会被剥离。需要注意 DOMPurify 的配置项要按业务最小化开放,比如不允许 script、iframe、事件属性,链接协议仅允许 http 和 https。
如果只是想把用户输入原样显示出来(比如评论区展示代码片段),完全不需要 v-html,插值表达式就够了。某些特殊需求下需要手动转义时,可以封装一个简单的工具函数:
function escapeHtml(str) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
}
return String(str).replace(/[<>&"']/g, ch => map[ch])
}这个函数把五个关键的 HTML 字符替换成实体,返回的字符串再交给插值表达式渲染即可,双层保险。此外,在拼接 URL 时也要警惕注入问题,动态参数务必经过 encodeURIComponent 处理,而不是直接拼进查询串。
格式化实践:数字、日期与金额的统一处理
格式化是工具库里最常见的一类字符串函数。数字千分位、金额补零、日期本地化这些需求几乎每个项目都有,建议统一封装成纯函数或组合式函数,而不是散落在各个组件里。
先看千分位格式化。除了手写正则,更可靠的方式是直接使用 Intl.NumberFormat,它是浏览器原生 API,支持多语言环境:
// 千分位格式化,保留两位小数
const money = new Intl.NumberFormat('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})
money.format(1234567.8) // '1,234,567.80'
money.format(0.5) // '0.50'日期格式化同样可以基于 Intl.DateTimeFormat 实现,避免引入体积较大的日期库。如果项目对日期操作要求复杂(时区换算、相对时间等),再考虑引入 Day.js,它的体积只有几 KB,API 也足够简洁:
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import 'dayjs/locale/zh-cn'
dayjs.extend(relativeTime)
dayjs.locale('zh-cn')
dayjs().format('YYYY-MM-DD HH:mm:ss') // '2024-06-01 09:30:00'
dayjs('2024-05-01').fromNow() // '一个月前'在组件层面使用格式化函数时,把结果放进 computed 而不是 methods,可以利用缓存避免每次渲染都重新计算。还有一种全局方案是自定义插件,把格式化函数挂载到全局属性上:
// formatters.js
export default {
install(app) {
app.config.globalProperties.$fmt = {
money: v => new Intl.NumberFormat('zh-CN', {
style: 'currency', currency: 'CNY'
}).format(v)
}
}
}
// 组件中使用
// {{ $fmt.money(998) }} 输出 ¥998.00这种方式的优点是调用方便,缺点是失去了 TypeScript 的类型提示,且不利于追踪数据流。更现代的做法是直接以具名导出的工具函数配合按需引入,打包工具会自动做 tree-shaking,未用到的格式化函数不会进入产物。
总结一下:展示层优先插值表达式,享受自动转义的安全保障;确需渲染 HTML 时必须经过 DOMPurify 净化;格式化逻辑统一封装为纯函数,配合 computed 使用。把这三条原则落地到工具库中,Vue 3 项目的字符串处理就能兼顾安全、性能和可维护性。