返回博客

Element UI $confirm 把 h 函数渲染成源代码字符串的真相

某次需要在后台管理系统的确认弹窗里展示一段多行文本——用户名 + 若干条记录拼在一起,中间要有换行。顺手在 options 里加了个 message: h => h('div', ...) 以为万事大吉。打开一看,弹窗里赫然印着:

function message(h) { return h('div', { style: 'white-space: pre-wrap' }, fullText); }

函数源代码原封不动被当成文本显示出来了。

现象

调用代码如下:

this.$confirm(fullText, '存在同名记录', {
  confirmButtonText: '继续',
  cancelButtonText: '取消',
  type: 'warning',
  message: h => h('div', { style: 'white-space: pre-wrap' }, fullText)
})

预期:弹窗 body 渲染出带样式的多行内容。 实际:弹窗 body 显示 function message(h) { return h('div', ...) } 这一串字面量。

根因

Element UI 2.x 有两种调用 MessageBox 的方式,签名完全不同:

配置式$msgbox):接受一个 options 对象,message 字段可以是字符串、VNode 或 h => VNode 函数:

$msgbox(options: ElMessageBoxOptions)
// options.message: string | VNode | ((h: CreateElement) => VNode)

快捷式$confirm / $alert / $prompt):前两个位置参数已经固定为 messagetitle

$confirm(message: string | VNode | ((h) => VNode), title: string, options?: ElMessageBoxOptions)

在快捷式里,第一个位置参数才是真正的 message。如果同时在 options.message 里再传一个 h 函数,Element UI 优先用第一参数(已经是字符串 fullText),而 options.message 里的函数对象在 options merge 阶段某个渲染路径上被 toString() 打印出来。结果就是把函数源码显示在弹窗里。

坑:快捷式方法不支持 options.message

$confirm / $alert / $prompt$msgbox 的快捷封装,签名第一位已经是 message。在 options 里再传 message: h => ... 是无效重复——行为不可靠,可能被 toString 打印为函数源代码字符串。

正解

方案 A(首选)— dangerouslyUseHTMLString + 手动 escape

简单可控,适合绝大多数多行/带格式场景:

const escapeHtml = (s) =>
  String(s == null ? '' : s).replace(
    /[&<>"']/g,
    c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])
  )
 
const html = `姓名「${escapeHtml(name)}」已有 <b>${count}</b> 条记录:<br/><br/>
${list.map(item => escapeHtml(item.phone)).join('<br/>')}<br/><br/>
是否继续?`
 
await this.$confirm(html, '存在同名记录', {
  confirmButtonText: '继续',
  cancelButtonText: '取消',
  type: 'warning',
  dangerouslyUseHTMLString: true   // ← 关键
})

escapeHtml 不可省略——用户输入字段如果含 <script><img onerror> 就是 XSS。

方案 B — $msgbox 配置式 + h 函数

如果确实需要 VNode 渲染,改用 $msgbox,让 message 字段在正确的位置发挥作用:

await this.$msgbox({
  title: '存在同名记录',
  message: h => h('div', { style: 'white-space: pre-wrap' }, fullText),
  showCancelButton: true,   // $msgbox 默认不显示取消,需手动开
  confirmButtonText: '继续',
  cancelButtonText: '取消',
  type: 'warning'
})

方案 C — $confirm 第一参数直接传 h 函数

await this.$confirm(
  h => h('div', { style: 'white-space: pre-wrap' }, fullText),
  '存在同名记录',
  { confirmButtonText: '继续', cancelButtonText: '取消', type: 'warning' }
)

Element UI 会调用这个箭头函数并传入内部的 $createElement,正确渲染 VNode。

正解:区分 $msgbox(配置式)和 $confirm(快捷式)

需要 VNode/HTML 内容时:优先用方案 A(dangerouslyUseHTMLString + escapeHtml),最稳;需要 h 函数则用 $msgbox 配置式或把 h 函数作为 $confirm 第一参数传入,不要放在 options.message 里。

一句话外卖

Element UI 快捷式 $confirm(message, title, options) 的第一位参数就是 message,options.message 是配置式专属字段——两者签名不同,混用会把函数对象 toString 打印为源码。