普通视图

发现新文章,点击刷新页面。
昨天 — 2026年2月5日首页

Vue 3 + Vite 集成 Monaco Editor 开发笔记

作者 昭阳
2026年2月5日 15:25

背景:在 Vue 3 (JS/TS) + Vite 项目中集成代码编辑器 Monaco Editor。

目标:实现代码高亮、自定义主题、中文汉化、语言切换、双向绑定等功能。

1. 方案选择与安装

在 Vite 中集成 Monaco Editor 主要有两种方式:

  1. 原生 Worker 方式:最稳定,利用 Vite 的 ?worker 特性,但汉化极其困难。
  2. 插件方式 (vite-plugin-monaco-editor)推荐。配置简单,自带汉化支持,但需要处理导入兼容性问题。

安装依赖

Bash

# 核心库
npm install monaco-editor

# Vite 插件 (用于处理 Worker 和汉化)
npm install -D vite-plugin-monaco-editor

2. 核心配置 (Vite)

🔴 常见报错与修复

在使用插件时,可能会遇到以下报错:

  1. TypeError: monacoEditorPlugin is not a function
  2. TypeError: 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)语言切换自定义主题 的通用组件。

核心逻辑点:

  1. 主题生效顺序:必须先 defineTheme,再 create 实例,并在配置中显式指定 theme
  2. 语言切换:使用 monaco.editor.setModelLanguage 动态切换。
  3. 双向绑定:同时支持内容 (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
  • 结论:配置插件时,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>
❌
❌