Vue3 + Monaco Editor 实现智能变量编辑器:隐藏花括号的魔法

前言

在现代 Web 应用中,我们经常需要实现模板编辑功能,比如邮件模板、消息模板等。用户需要在模板中填写变量(如 {``{用户名}}{``{订单号}}),但传统的文本编辑器体验并不友好:花括号会干扰视觉,变量不易识别和编辑。

本文将深入解析一个基于 Vue 3 + Monaco Editor 的智能变量编辑器组件,它能够:

  • 隐藏花括号 :让 {``{变量名}} 显示为 变量名
  • 🎨 高亮变量:变量文字带下划线和特殊颜色
  • 🖱️ 点击选中:点击变量自动全选,方便修改
  • 💡 悬停提示:鼠标悬停显示变量说明

一、核心技术栈

json 复制代码
{
  "vue": "3.x",
  "monaco-editor": "^0.x",
  "uuid": "用于生成唯一ID"
}

二、组件架构设计

2.1 整体结构

vue 复制代码
<template>
  <div style="width: 100%; height: 100%; display: flex; flex-direction: column">
    <div v-if="loading">正在加载编辑器...</div>
    <div ref="editorContainer" style="flex: 1; width: 100%"></div>
  </div>
</template>

组件采用简洁的布局:

  • 加载状态提示
  • 编辑器容器(flex 自适应)

2.2 Props 配置

组件支持高度自定义的 Monaco Editor 配置:

javascript 复制代码
const props = defineProps({
  modelValue: String,           // v-model 双向绑定
  variables: Array,             // 变量列表(预留)
  language: {                   // 语言模式
    type: String,
    default: 'markdown'
  },
  fontSize: {                   // 字体大小
    type: Number,
    default: 14
  },
  lineNumbers: {                // 行号显示
    type: String,
    default: 'off'              // 默认不显示行号
  },
  minimap: {                    // 小地图
    type: Object,
    default: () => ({ enabled: false })
  },
  wordWrap: {                   // 自动换行
    type: String,
    default: 'on'
  },
  // ... 更多配置
})

设计亮点

  • 默认配置针对模板编辑优化(无行号、无小地图、自动换行)
  • 保留完整的 Monaco Editor 配置能力

三、核心功能实现

3.1 变量识别:正则表达式解析

javascript 复制代码
const findVariableBlocks = (content) => {
  const blocks = []
  const lines = content.split('\n')
  const variableRegex = /\{\{([^}]+)\}\}/g

  lines.forEach((line, lineIndex) => {
    let match
    variableRegex.lastIndex = 0
    while ((match = variableRegex.exec(line)) !== null) {
      const variableName = match[1]
      const startColumn = match.index + 3  // 跳过 {{
      const endColumn = match.index + 3 + variableName.length  // }} 前

      blocks.push({
        id: uuidv4(),
        startLine: lineIndex + 1,
        startColumn: startColumn,
        endLine: lineIndex + 1,
        endColumn: endColumn,
        guideText: `请输入${variableName}`,
        presetText: variableName
      })
    }
  })

  return blocks
}

技术要点

  1. 正则表达式 /\{\{([^}]+)\}\}/g

    • 匹配 {``{}} 之间的内容
    • 使用捕获组获取变量名
    • 全局匹配(g 标志)
  2. 位置计算

    • startColumn = match.index + 3:跳过左花括号 {``{
    • endColumn = startColumn + variableName.length:变量名结束位置
    • Monaco Editor 使用 1-based 行号
  3. 数据结构

    javascript 复制代码
    {
      id: "唯一ID",
      startLine: 1,
      startColumn: 3,
      endLine: 1,
      endColumn: 9,
      guideText: "请输入用户名",
      presetText: "用户名"
    }

3.2 视觉魔法:Monaco Decorations API

Monaco Editor 的 Decorations 是实现花括号隐藏的核心:

javascript 复制代码
const applyDecorations = (blocks) => {
  if (!editor.value) return

  const decorations = []

  blocks.forEach((block) => {
    // 1. 变量内容装饰(高亮 + 提示)
    decorations.push({
      range: new monaco.Range(
        block.startLine, block.startColumn, 
        block.endLine, block.endColumn
      ),
      options: {
        inlineClassName: 'edit-block-decoration',
        hoverMessage: {
          value: `📝 ${block.guideText}\n预设值:${block.presetText}`
        },
        beforeContentClassName: 'edit-block-before',
        afterContentClassName: 'edit-block-after'
      }
    })

    // 2. 隐藏左花括号 {{
    decorations.push({
      range: new monaco.Range(
        block.startLine, block.startColumn - 2, 
        block.endLine, block.startColumn
      ),
      options: {
        inlineClassName: 'hidden-bracket'
      }
    })

    // 3. 隐藏右花括号 }}
    decorations.push({
      range: new monaco.Range(
        block.startLine, block.endColumn, 
        block.endLine, block.endColumn + 2
      ),
      options: {
        inlineClassName: 'hidden-bracket'
      }
    })
  })

  decorationIds = editor.value.deltaDecorations(decorationIds, decorations)
}

关键机制

1️⃣ 变量高亮装饰
  • inlineClassName:应用 CSS 类
  • hoverMessage:悬停提示(Markdown 格式)
  • beforeContentClassName / afterContentClassName:内容前后的装饰
2️⃣ 花括号隐藏

通过 CSS 实现:

css 复制代码
.hidden-bracket {
  opacity: 0 !important;        /* 透明 */
  font-size: 0 !important;      /* 字体大小为 0 */
  width: 0 !important;          /* 宽度为 0 */
  overflow: hidden !important;  /* 隐藏溢出 */
}

为什么不直接删除花括号?

  • 保留源数据完整性
  • 用户复制粘贴时仍能得到 {``{变量名}} 格式
  • 方便数据持久化
3️⃣ deltaDecorations 增量更新
javascript 复制代码
decorationIds = editor.value.deltaDecorations(decorationIds, decorations)
  • 第一个参数:旧的装饰 ID 列表(用于移除)
  • 第二个参数:新的装饰配置
  • 返回值:新装饰的 ID 列表(供下次更新使用)

3.3 交互体验优化

点击变量自动选中
javascript 复制代码
editor.value.onMouseDown((e) => {
  const position = e.target.position
  if (!position) return

  editBlocks.value.forEach((block) => {
    const blockRange = new monaco.Range(
      block.startLine, block.startColumn, 
      block.endLine, block.endColumn
    )

    if (blockRange.containsPosition(position)) {
      setTimeout(() => {
        editor.value.setSelection(blockRange)
        editor.value.focus()
        isFirstInputAfterClick = true
        emit('variableClick', block)
      }, 10)
    }
  })
})

用户体验

  • 点击 用户名(变量部分)
  • 自动选中整个变量文字
  • 开始输入时清空选中内容
  • 发出 variableClick 事件供父组件监听
首次输入清空选中
javascript 复制代码
editor.value.onDidChangeModelContent((e) => {
  if (isFirstInputAfterClick && e.changes.length > 0) {
    isFirstInputAfterClick = false
    const selection = editor.value.getSelection()
    if (selection && !selection.isEmpty()) {
      editor.value.executeEdits('', [{
        range: selection,
        text: '',
        forceMoveMarkers: true
      }])
      return
    }
  }

  // 延迟更新装饰
  setTimeout(() => {
    const content = editor.value.getValue()
    const newBlocks = findVariableBlocks(content)
    editBlocks.value = newBlocks
    applyDecorations(newBlocks)
    emit('update:modelValue', content)
  }, 200)
})

流程

  1. 点击变量 → 全选 → 设置 isFirstInputAfterClick = true
  2. 用户输入 → 清空选中的旧变量名
  3. 继续输入新内容
  4. 200ms 后重新解析变量并更新装饰

为什么延迟 200ms?

  • 避免频繁触发正则匹配和装饰更新
  • 优化性能(防抖)

四、Monaco Editor Worker 配置

javascript 复制代码
self.MonacoEnvironment = {
  getWorker(_, label) {
    if (label === 'json') return new jsonWorker()
    if (label === 'css' || label === 'scss' || label === 'less') return new cssWorker()
    if (label === 'html' || label === 'handlebars' || label === 'razor') return new htmlWorker()
    if (label === 'typescript' || label === 'javascript') return new tsWorker()
    return new editorWorker()
  }
}

作用

  • Monaco Editor 使用 Web Worker 处理语言服务(语法高亮、自动补全等)
  • 通过 Vite 的 ?worker 后缀导入 Worker
  • 支持多种语言:JSON、CSS、HTML、TypeScript 等

Vite 配置(必需)

javascript 复制代码
// vite.config.js
export default {
  optimizeDeps: {
    include: ['monaco-editor']
  }
}

五、样式设计

5.1 变量装饰样式

css 复制代码
.edit-block-decoration {
  border-bottom: 1px solid #1890ff !important;  /* 蓝色下划线 */
  cursor: pointer !important;                   /* 鼠标指针 */
  font-weight: 500;                             /* 字体加粗 */
}

.edit-block-decoration,
.edit-block-decoration * {
  color: #419df3 !important;                    /* 变量文字颜色 */
}

5.2 为什么使用全局样式?

vue 复制代码
<style>  <!-- 注意:不是 scoped -->
  .edit-block-decoration { ... }
</style>

原因

  • Monaco Editor 的内容渲染在 Shadow DOM 或动态插入的 DOM 中
  • Vue 的 scoped 样式无法穿透到编辑器内部
  • 必须使用全局样式 + !important 确保生效

六、生命周期与性能优化

6.1 延迟加载策略

javascript 复制代码
onMounted(() => {
  setTimeout(() => {
    loading.value = false
    
    setTimeout(() => {
      editor.value = monaco.editor.create(editorContainer.value, {
        // ... 配置
      })
      
      const defaultBlocks = findVariableBlocks(props.modelValue)
      editBlocks.value = defaultBlocks
      applyDecorations(defaultBlocks)
    }, 100)
  }, 50)
})

三层延迟

  1. 50ms:等待 DOM 完全挂载
  2. 100ms:确保容器尺寸计算完成
  3. Monaco 初始化需要容器有明确的宽高

6.2 资源清理

javascript 复制代码
onBeforeUnmount(() => {
  if (editor.value) {
    editor.value.dispose()  // 释放 Monaco 实例
  }
})

重要性

  • Monaco Editor 实例占用大量内存
  • 不清理会导致内存泄漏
  • 尤其在 SPA 中频繁切换页面时

6.3 shallowRef 性能优化

javascript 复制代码
const editor = shallowRef(null)  // 不是 ref(null)

原因

  • Monaco Editor 实例是复杂对象,不需要深度响应式
  • shallowRef 只追踪 .value 本身的变化
  • 避免性能损耗

七、使用示例

7.1 基础用法

vue 复制代码
<template>
  <VariableEditor
    v-model="templateContent"
    language="markdown"
    @variableClick="handleVariableClick"
  />
</template>

<script setup>
import { ref } from 'vue'
import VariableEditor from '@/components/MonacoEditor/VariableEditor.vue'

const templateContent = ref('尊敬的{{用户名}},您的订单{{订单号}}已发货。')

const handleVariableClick = (block) => {
  console.log('点击了变量:', block.presetText)
}
</script>

7.2 效果展示

编辑前

复制代码
尊敬的{{用户名}},您的订单{{订单号}}已发货。

编辑器显示

复制代码
尊敬的用户名,您的订单订单号已发货。
     ----      ----
     (蓝色下划线,可点击)

实际内容(复制时):

复制代码
尊敬的{{用户名}},您的订单{{订单号}}已发货。

八、进阶扩展

8.1 变量列表面板

可以结合组件实现变量选择器:

vue 复制代码
<template>
  <div class="editor-with-variables">
    <div class="variable-panel">
      <div 
        v-for="variable in variables" 
        :key="variable.key"
        @click="insertVariable(variable)"
      >
        {{ variable.label }}
      </div>
    </div>
    <VariableEditor ref="editorRef" v-model="content" />
  </div>
</template>

<script setup>
const variables = [
  { key: 'userName', label: '用户名' },
  { key: 'orderNo', label: '订单号' }
]

const insertVariable = (variable) => {
  const editor = editorRef.value.getEditor()
  const position = editor.getPosition()
  const text = `{{${variable.label}}}`
  editor.executeEdits('', [{
    range: new monaco.Range(
      position.lineNumber, position.column,
      position.lineNumber, position.column
    ),
    text: text
  }])
}
</script>

8.2 变量校验

javascript 复制代码
const validateVariables = (content) => {
  const blocks = findVariableBlocks(content)
  const errors = []
  
  blocks.forEach(block => {
    const validVariables = ['用户名', '订单号', '金额']
    if (!validVariables.includes(block.presetText)) {
      errors.push({
        line: block.startLine,
        message: `未知变量:${block.presetText}`
      })
    }
  })
  
  return errors
}

8.3 主题定制

javascript 复制代码
monaco.editor.defineTheme('customTheme', {
  base: 'vs',
  inherit: true,
  rules: [
    { token: '', foreground: '333333' }
  ],
  colors: {
    'editor.background': '#f9f9f9',
    'editor.foreground': '#333333',
    'editor.lineHighlightBackground': '#f0f0f0'
  }
})

editor.value = monaco.editor.create(editorContainer.value, {
  theme: 'customTheme',
  // ... 其他配置
})

九、常见问题与解决方案

9.1 花括号没有隐藏

原因

  • CSS 样式未生效(检查是否使用 scoped
  • 装饰范围计算错误(检查 startColumn / endColumn

解决

css 复制代码
/* 确保是全局样式,不要加 scoped */
<style>
.hidden-bracket {
  opacity: 0 !important;
  /* ... */
}
</style>

9.2 编辑器高度为 0

原因

  • 父容器没有明确高度
  • Flex 布局配置错误

解决

vue 复制代码
<div style="height: 500px;">
  <VariableEditor v-model="content" />
</div>

9.3 中文输入法问题

现象

  • 输入拼音时触发内容变化

解决

javascript 复制代码
editor.value.onDidChangeModelContent((e) => {
  // 添加防抖
  clearTimeout(timer)
  timer = setTimeout(() => {
    // 更新逻辑
  }, 200)
})

十、性能优化建议

10.1 大文件处理

javascript 复制代码
// 限制正则匹配范围
const findVariableBlocks = (content) => {
  const MAX_LINES = 1000
  const lines = content.split('\n').slice(0, MAX_LINES)
  // ... 解析逻辑
}

10.2 装饰批量更新

javascript 复制代码
// 收集所有变化,一次性更新
let pendingUpdate = null
editor.value.onDidChangeModelContent(() => {
  if (pendingUpdate) return
  
  pendingUpdate = setTimeout(() => {
    applyDecorations(findVariableBlocks(editor.value.getValue()))
    pendingUpdate = null
  }, 200)
})

十一、总结

本文深入剖析了一个功能完整的 Vue 3 + Monaco Editor 变量编辑器,核心技术点包括:

  1. 正则表达式 :精准识别 {``{变量名}} 格式
  2. Monaco Decorations API:实现花括号隐藏和变量高亮
  3. 交互优化:点击选中、悬停提示、首次输入清空
  4. 性能优化:延迟更新、shallowRef、资源清理
  5. 样式穿透 :全局样式 + !important 确保生效

适用场景

  • ✉️ 邮件模板编辑器
  • 📱 短信/消息模板系统
  • 📄 文档生成器
  • 🤖 AI Prompt 编辑器

源码获取

本文所有代码均可在 GitHub 找到(请自行适配项目)

参考资料


如果觉得本文对你有帮助,请点赞 👍 收藏 ⭐ 关注 📌,你的支持是我创作的最大动力!

相关推荐
ONLYOFFICE7 小时前
ONLYOFFICE 桌面编辑器正式成为 ShaniOS 默认办公套件
linux·编辑器·github·onlyoffice
zhyongrui7 小时前
SnipTrip:贴纸画布编辑器与“光晕动效”的交互细节
编辑器·交互
山峰哥7 小时前
SQL调优实战:从索引到执行计划的深度优化指南
大数据·开发语言·数据库·sql·编辑器·深度优先
山峰哥1 天前
破解SQL性能瓶颈:索引优化核心策略
大数据·数据库·sql·oracle·编辑器·深度优先·数据库架构
何亚告1 天前
VScode引入claude+deepseek
ide·vscode·编辑器
芝芝葡萄1 天前
VsCode中使用Codex
前端·ide·vscode·编辑器·ai编程
GuiltyFet1 天前
CKEditor副本编辑器CVE-2021-33829漏洞复现
安全·编辑器
历程里程碑1 天前
Linux 9:GCC编译全流程详解
linux·运维·服务器·c语言·笔记·编辑器·vim