背景:在 Vue 3 (JS/TS) + Vite 项目中集成代码编辑器 Monaco Editor。
目标:实现代码高亮、自定义主题、中文汉化、语言切换、双向绑定等功能。
1. 方案选择与安装
在 Vite 中集成 Monaco Editor 主要有两种方式:
- 原生 Worker 方式:最稳定,利用 Vite 的
?worker特性,但汉化极其困难。 - 插件方式 (
vite-plugin-monaco-editor) :推荐。配置简单,自带汉化支持,但需要处理导入兼容性问题。
安装依赖
Bash
# 核心库
npm install monaco-editor
# Vite 插件 (用于处理 Worker 和汉化)
npm install -D vite-plugin-monaco-editor
2. 核心配置 (Vite)
🔴 常见报错与修复
在使用插件时,可能会遇到以下报错:
TypeError: monacoEditorPlugin is not a functionTypeError: Cannot read properties of undefined (reading 'entry')
这是因为 ESM/CommonJS 模块导入兼容性问题。
✅ 最佳配置 (vite.config.js)
JavaScript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import monacoEditorPlugin from 'vite-plugin-monaco-editor'
export default defineConfig({
plugins: [
vue(),
// 🟢 核心修复:兼容写法,防止报错
(monacoEditorPlugin.default || monacoEditorPlugin)({
// 需要加载 Worker 的语言 (JSON, TS/JS, HTML, CSS 有独立 Worker)
languageWorkers: ['json', 'editorWorkerService'],
// 🟢 开启中文汉化
locale: 'zh-cn',
})
],
})
注意:SQL 属于 Basic Language(基础语言),没有独立的 Worker,不需要加到
languageWorkers列表中。
3. 组件封装 (MonacoEditor.vue)
封装一个支持 双向绑定 (v-model) 、语言切换、自定义主题 的通用组件。
核心逻辑点:
- 主题生效顺序:必须先
defineTheme,再create实例,并在配置中显式指定theme。 - 语言切换:使用
monaco.editor.setModelLanguage动态切换。 - 双向绑定:同时支持内容 (
v-model) 和 语言 (v-model:language)。
完整代码
代码段
<template>
<div class="monaco-wrapper">
<select :value="language" class="lang-select" @change="handleLanguageChange">
<option value="json">JSON</option>
<option value="sql">SQL</option>
<option value="javascript">JS</option>
<option value="css">CSS</option>
</select>
<div ref="editorContainer" class="editor-container"></div>
</div>
</template>
<script setup>
import { onMounted, onBeforeUnmount, ref, watch, toRaw } from 'vue'
import * as monaco from 'monaco-editor'
// 定义 Props
const props = defineProps({
modelValue: { type: String, default: '' },
language: { type: String, default: 'json' },
readOnly: { type: Boolean, default: false }
})
// 定义 Emits (支持双 v-model)
const emit = defineEmits(['update:modelValue', 'update:language', 'change'])
const editorContainer = ref(null)
let editorInstance = null
// 1. 切换语言逻辑
const handleLanguageChange = (e) => {
const newLang = e.target.value
emit('update:language', newLang) // 通知父组件
if (editorInstance) {
monaco.editor.setModelLanguage(editorInstance.getModel(), newLang)
}
}
onMounted(() => {
if (!editorContainer.value) return
// 2. 定义自定义主题 (必须在 create 之前)
monaco.editor.defineTheme('my-dark-theme', {
base: 'vs-dark',
inherit: true,
rules: [
{ token: 'key', foreground: 'dddddd' },
{ token: 'string.key.json', foreground: 'dddddd' },
{ token: 'string.value.json', foreground: 'b4e98c' },
],
colors: {
'editor.background': '#0e1013', // 背景色
'editor.lineHighlightBackground': '#1f2329',
},
})
// 3. 创建编辑器实例
editorInstance = monaco.editor.create(editorContainer.value, {
value: props.modelValue,
language: props.language,
theme: 'my-dark-theme', // 🟢 显式引用主题
readOnly: props.readOnly,
automaticLayout: true, // 自动适应宽高
minimap: { enabled: false }, // 关闭小地图
scrollBeyondLastLine: false,
})
// 4. 监听内容变化 -> 通知父组件
editorInstance.onDidChangeModelContent(() => {
const value = editorInstance.getValue()
emit('update:modelValue', value)
emit('change', value)
})
})
// 5. 监听 Props 变化 (外部修改 -> 同步到编辑器)
watch(() => props.modelValue, (newValue) => {
if (editorInstance && newValue !== editorInstance.getValue()) {
// toRaw 避免 Vue 代理对象干扰 Monaco 内部逻辑
toRaw(editorInstance).setValue(newValue)
}
})
watch(() => props.language, (newLang) => {
if (editorInstance) {
monaco.editor.setModelLanguage(editorInstance.getModel(), newLang)
}
})
// 销毁
onBeforeUnmount(() => {
editorInstance?.dispose()
})
</script>
<style scoped>
.monaco-wrapper {
position: relative;
width: 100%;
height: 100%;
min-height: 300px;
}
.editor-container {
width: 100%;
height: 100%;
}
.lang-select {
position: absolute;
right: 15px;
top: 10px;
z-index: 20;
background: #1f2329;
color: #ddd;
border: 1px solid #555;
border-radius: 4px;
}
</style>
4. 疑难杂症 (Q&A)
Q1: 为什么在 node_modules 里找不到 SQL 的 Worker 文件?
-
原因:Monaco 将语言分为两类。
- Rich Languages (JSON, TS, CSS, HTML):有独立 Worker,支持高级语法检查。路径在
esm/vs/language。 - Basic Languages (SQL, Python, Java 等):没有独立 Worker,只依靠主线程进行简单高亮。路径在
esm/vs/basic-languages。
- Rich Languages (JSON, TS, CSS, HTML):有独立 Worker,支持高级语法检查。路径在
-
结论:配置插件时,
languageWorkers不需要加 SQL。
Q2: 为什么 import 'monaco-editor/esm/nls.messages.zh-cn.js' 汉化不生效?
- 原因:在 ESM 模式下,编辑器核心初始化往往早于语言包加载,或者直接被 Tree-shaking 忽略。
- 解决:使用
vite-plugin-monaco-editor并配置locale: 'zh-cn',插件会在编译构建阶段自动注入语言包。
Q3: 为什么 Ctrl+点击 @/... 路径无法跳转?
-
原因:VS Code 需要配置文件来理解别名。对于 Vue+JS 项目,根目录缺少
jsconfig.json。 -
解决:在根目录创建
jsconfig.json:JSON
{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] } }, "include": ["src/**/*"] }设置完后记得重启 VS Code。
5. 最佳实践:父组件调用
使用 Vue 3 的多 v-model 特性,代码语义最清晰:
HTML
<template>
<div class="page">
<MonacoEditor
v-model="codeContent"
v-model:language="currentLang"
/>
<button @click="runCode">运行</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
import MonacoEditor from '@/components/MonacoEditor/index.vue'
const codeContent = ref('SELECT * FROM users;')
const currentLang = ref('sql') // 切换下拉框会自动更新此变量
const runCode = () => {
console.log(`正在运行 ${currentLang.value} 代码:`, codeContent.value)
}
</script>