Monaco 编辑器是一个功能强大的代码编辑器,它广泛应用于 Visual Studio Code 中。通过其提供的 API,可以在 Web 项目中集成强大的代码编辑功能。本文将介绍如何在 Vue 3 中构建一个 Monaco 编辑器组件,并提供相关的使用示例、依赖和源码。

1. 项目依赖

在使用 Monaco 编辑器之前,你需要安装 monaco-editor 包:

npm install monaco-editor

2. 组件源码

我们将构建一个 Monaco 编辑器的 Vue 3 组件,它支持动态设置编辑器的代码内容和语言,并暴露接口供父组件获取或修改编辑器中的代码。

2.1 组件模板

<template>
  <div class="codemirror">
    <div id="monacoEditor" class="monaco-editor" ref="monacoEditor"></div>
  </div>
</template>

这个模板定义了一个 div 元素作为 Monaco 编辑器的容器,并使用 ref 来引用它,以便在 JavaScript 中访问和操作。

2.2 组件脚本

<template>
  <div class="codemirror">
    <div id="monacoEditor" class="monaco-editor" ref="monacoEditor"></div>
  </div>
</template>
  
  <script setup>
import { ref, watch, onMounted, onBeforeUnmount , defineProps, toRaw} from "vue";
import * as monaco from "monaco-editor";

// 动态引入 Monaco 编辑器的 Worker 文件
const loadMonacoWorker = (workerId, label) => {
  if (label === "json")
    return import("monaco-editor/esm/vs/language/json/json.worker?worker");
  if (label === "css" || label === "scss" || label === "less")
    return import("monaco-editor/esm/vs/language/css/css.worker?worker");
  if (label === "html")
    return import("monaco-editor/esm/vs/language/html/html.worker?worker");
  if (["typescript", "javascript"].includes(label))
    return import("monaco-editor/esm/vs/language/typescript/ts.worker?worker");
  return import("monaco-editor/esm/vs/editor/editor.worker?worker");
};

// 配置 Monaco 的 Worker 环境
self.MonacoEnvironment = {
  getWorker(workerId, label) {
    return loadMonacoWorker(workerId, label).then(
      (workerModule) => new workerModule.default()
    );
  },
};

// 定义从父组件接收的属性
const props = defineProps({
  option: {
    type: Object,
    default: () => ({
      options: {
        language: "javascript", // 默认语言
        code: "// Write your code here", // 默认代码
      },
    }),
  },
});

// 定义响应式变量
const monacoEditor = ref(null); // 编辑器容器
const editor = ref(null); // 编辑器实例

// 初始化编辑器
function initEditor(language, code) {
  if (!monacoEditor.value) return;

  editor.value = monaco.editor.create(monacoEditor.value, {
    value: code,
    theme: "vs-dark", // 主题
    language: language,
    automaticLayout: true,
    folding: true,
    scrollBeyondLastLine: false,
    colorDecorators: true, // 颜色装饰器
    accessibilitySupport: "on", // 辅助功能支持  "auto" | "off" | "on"
    colors: {
      "editor.background": "#131722", // 修改编辑器背景颜色
      "editor.lineHighlightBackground": "#131722", // 当前行背景高亮颜色
      "editorCursor.foreground": "#131722", // 光标颜色
    },
  });

  // 添加自定义补全项
  registerCustomCompletionProvider(language);
}

// 注册自定义补全提供者
function registerCustomCompletionProvider(language) {
  monaco.languages.registerCompletionItemProvider(language, {
    provideCompletionItems: (model, position) => {
      const suggestions = [
        {
          label: "console.log", // 提示标签
          kind: monaco.languages.CompletionItemKind.Function, // 类型
          insertText: "console.log(${1:message});", // 插入的内容,支持占位符
          insertTextRules:
            monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, // 插入规则
          documentation: "输出到控制台", // 文档描述
        },
        {
          label: "for loop",
          kind: monaco.languages.CompletionItemKind.Snippet,
          insertText: "for (let i = 0; i < ${1:array}.length; i++) {\n\t$0\n}",
          insertTextRules:
            monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
          documentation: "简单的 for 循环代码模板",
        },
      ];
      return { suggestions };
    },
  });
}

// 监听 props.option 的变化
watch(
  () => props.option,
  (newOption) => {
    if (editor.value) {
      // 只更新编辑器内容和语言,而不是每次都重新初始化
      if (newOption.options.code !== editor.value.getValue()) {
        editor.value.setValue(newOption.options.code);
      }
      if (
        newOption.options.language !== editor.value.getModel().getLanguageId()
      ) {
        monaco.editor.setModelLanguage(
          editor.value.getModel(),
          newOption.options.language
        );
      }
    } else {
      // 仅在第一次初始化时创建编辑器
      initEditor(newOption.options.language, newOption.options.code);
    }
  },
  { immediate: true, deep: false } // 禁用 deep 监听,避免每个嵌套的变化都触发
);

// 挂载时初始化编辑器
onMounted(() => {
  initEditor(props.option.options.language, props.option.options.code);
});

// 销毁前清理编辑器实例
onBeforeUnmount(() => {
  if (editor.value) {
    editor.value.dispose();
  }
});
// 在父组件中调用这个方法来获取代码内容
const getCode = () => {
  // 确保编辑器已经初始化
  if (editor.value) {
    try {
      // 使用 Monaco 编辑器实例的 getValue() 方法获取代码内容
      let demo = toRaw(editor.value).getValue(); //获取编辑器中的文本
      return demo
    } catch (error) {
      console.error('获取代码内容失败:', error);
      return '';
    }
  }
  return '';  // 返回空字符串,如果 editor 没有初始化
};

const setCode = (code) =>{
    // 确保编辑器已经初始化
    if (editor.value) {
    try {
      // 使用 Monaco 编辑器实例的 getValue() 方法获取代码内容
      toRaw(editor.value).setModel(monaco.editor.createModel(code, props.option.options.language))
    } catch (error) {
      console.error('获取代码内容失败:', error);
      return '';
    }
  }
  return '';  // 返回空字符串,如果 editor 没有初始化
}
// 通过暴露 `getCode` 方法供父组件调用
defineExpose({
  getCode,setCode
});
</script>
  
  <style scoped>
.codemirror,
.monaco-editor {
  width: 100%;
  height: 100%;
  background-color: var(--background-color);
}
</style>
  

2.3 组件样式

<style scoped>
.codemirror,
.monaco-editor {
  width: 100%;
  height: 100%;
  background-color: var(--background-color);
}
</style>

3. 使用方式

该组件的使用方式非常简单,你只需要将它作为子组件引入,并通过 option 属性动态传递语言和代码内容。

3.1 父组件使用示例

<template>
  <div>
    <MonacoEditor :option="editorOptions" ref="codeEditor"/>
    <button @click="handleGetCode">获取代码</button>
    <button @click="handleSetCode">设置新代码</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import MonacoEditor from './MonacoEditor.vue'; // 引入 Monaco 编辑器组件

const editorOptions = ref({
  options: {
    language: 'javascript',
    code: '// Type your JavaScript code here',
  },
});

const codeEditor = ref(null);

// 获取编辑器中的代码内容
const handleGetCode = () => {
  const code = codeEditor.value.getCode();
  console.log('编辑器中的代码:', code);
};

// 设置编辑器中的代码内容
const handleSetCode = () => {
  codeEditor.value.setCode('console.log("Hello World!");');
};
</script>

4. 关键功能说明

  1. 动态设置语言和代码:通过 props.option,你可以传递不同的语言和初始代码到 Monaco 编辑器。编辑器会根据这些参数动态加载语言模式并展示相应的代码内容。

  2. 获取和设置代码:通过暴露的 getCodesetCode 方法,父组件可以获取或设置 Monaco 编辑器中的代码内容。这使得编辑器的使用更加灵活。

  3. 自定义代码补全:我们注册了 console.logfor loop 作为自定义的代码补全项。你可以根据需要,添加更多的补全功能。

  4. 自动布局和主题支持:编辑器支持自动布局和自定义的主题。你可以根据需要更改编辑器的外观和行为。

5. 总结

通过上述方式,你可以在 Vue 3 项目中轻松集成 Monaco 编辑器,

构建功能强大的代码编辑器。组件提供了灵活的 API,可以方便地获取和设置代码内容,并支持语言切换和自定义补全等功能。