前言
在现代 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
}
技术要点:
-
正则表达式
/\{\{([^}]+)\}\}/g:- 匹配
{``{和}}之间的内容 - 使用捕获组获取变量名
- 全局匹配(g 标志)
- 匹配
-
位置计算:
startColumn = match.index + 3:跳过左花括号{``{endColumn = startColumn + variableName.length:变量名结束位置- Monaco Editor 使用 1-based 行号
-
数据结构:
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)
})
流程:
- 点击变量 → 全选 → 设置
isFirstInputAfterClick = true - 用户输入 → 清空选中的旧变量名
- 继续输入新内容
- 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)
})
三层延迟:
- 50ms:等待 DOM 完全挂载
- 100ms:确保容器尺寸计算完成
- 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 变量编辑器,核心技术点包括:
- 正则表达式 :精准识别
{``{变量名}}格式 - Monaco Decorations API:实现花括号隐藏和变量高亮
- 交互优化:点击选中、悬停提示、首次输入清空
- 性能优化:延迟更新、shallowRef、资源清理
- 样式穿透 :全局样式 +
!important确保生效
适用场景:
- ✉️ 邮件模板编辑器
- 📱 短信/消息模板系统
- 📄 文档生成器
- 🤖 AI Prompt 编辑器
源码获取 :
本文所有代码均可在 GitHub 找到(请自行适配项目)
参考资料:
如果觉得本文对你有帮助,请点赞 👍 收藏 ⭐ 关注 📌,你的支持是我创作的最大动力!