MonacoEditor 组件技术方案

一、组件概述

MonacoEditor 组件封装了 monaco-editor 的基础使用能力,用于在项目中提供一个可复用的代码编辑器视图。

当前功能聚焦于:

  • 初始化 Monaco 编辑器实例并挂载到指定容器中。
  • 支持设置默认代码内容语言类型(如 typescriptjavascriptjson 等)。
  • 对外暴露编辑器实例,方便在父组件中进行高级操作(如读取/设置内容、注册事件等)。
  • 统一配置 Monaco Web Worker,保证不同语言特性正常工作。

二、依赖与运行环境

  • 编辑器核心依赖monaco-editor
  • 运行环境:基于 Vite + Vue 3 组合,使用 <script setup lang="ts"> 语法。
  • 自动导入:项目通过 unplugin 自动导入 Vue API,例如 useTemplateRefwatchshallowRef 等。

组件内部引用的 Monaco 相关模块:

import { editor } from "monaco-editor";
import EditorWorker from "monaco-editor/esm/vs/editor/editor.worker.js?worker";
import CssWorker from "monaco-editor/esm/vs/language/css/css.worker.js?worker";
import HtmlWorker from "monaco-editor/esm/vs/language/html/html.worker.js?worker";
import JsonWorker from "monaco-editor/esm/vs/language/json/json.worker.js?worker";
import TypeScriptWorker from "monaco-editor/esm/vs/language/typescript/ts.worker.js?worker";

这些 worker 通过 Vite 的 ?worker 语法构建为 Web Worker,使 Monaco 能在浏览器环境下正确运行语言服务。

三、组件 API 设计

完整组件代码示例

<script setup lang="ts">
import { editor } from "monaco-editor";
import EditorWorker from "monaco-editor/esm/vs/editor/editor.worker.js?worker";
import CssWorker from "monaco-editor/esm/vs/language/css/css.worker.js?worker";
import HtmlWorker from "monaco-editor/esm/vs/language/html/html.worker.js?worker";
import JsonWorker from "monaco-editor/esm/vs/language/json/json.worker.js?worker";
import TypeScriptWorker from "monaco-editor/esm/vs/language/typescript/ts.worker.js?worker";

const props = defineProps<{
  defaultCode?: string;
  language?: string;
}>();

const container = useTemplateRef("container-element");

if (!globalThis.MonacoEnvironment) {
  globalThis.MonacoEnvironment = {
    getWorker(workerId, label) {
      switch (label) {
        case "json":
          return new JsonWorker({ name: label });
        case "css":
        case "scss":
        case "less":
          return new CssWorker({ name: label });
        case "html":
        case "handlebars":
        case "razor":
          return new HtmlWorker({ name: label });
        case "typescript":
        case "javascript":
          return new TypeScriptWorker({ name: label });
        default:
          return new EditorWorker({ name: label });
      }
    },
  };
}

const instance = shallowRef<editor.IStandaloneCodeEditor>();

watch(container, (el) => {
  if (!el) return;

  const editorInstance = editor.create(el, {
    value: props.defaultCode,
    language: props.language,
  });

  instance.value = editorInstance;

  onWatcherCleanup(() => {
    editorInstance.dispose();
  });
});

defineExpose({
  instance,
});
</script>

<template>
  <article ref="container-element" :class="$style.editor" />
</template>

<style module>
.editor {
  overflow: hidden;
  border: 1px solid var(--el-border-color);
}
</style>

1. Props

组件通过 defineProps 定义如下两个可选属性:

const props = defineProps<{
  defaultCode?: string;
  language?: string;
}>();
  • defaultCode?: string
    • 作用:指定编辑器初始化时显示的默认代码内容。
    • 为空时:编辑器内容为空字符串。
  • language?: string
    • 作用:指定 Monaco 编辑器的语言模式,例如:"typescript""javascript""json""css""html" 等。
    • 为空时:Monaco 会使用默认语言(通常是纯文本或基于内容推断)。

2. 暴露实例

组件内部维护一个 shallowRef<editor.IStandaloneCodeEditor> 用来保存 Monaco 实例,并通过 defineExpose 暴露给父组件:

const instance = shallowRef<editor.IStandaloneCodeEditor>();

// ... 创建完成后赋值
instance.value = result;

// 对外暴露
defineExpose({ instance });

父组件可以通过模板 ref 获取到组件实例,然后访问 instance 字段调用 Monaco 的全部 API。

3. 模板结构

<template>
  <article :class="$style.editor" />
</template>

<style module>
.editor {
  overflow: hidden;
  border: 1px solid var(--el-border-color);
}
</style>
  • 根元素:使用 <article> 标签作为容器。
  • 模板 refref="container-element",配合 useTemplateRef("container-element") 获取 DOM 元素。
  • 样式:使用 CSS Modules(<style module>),遵守项目“禁止 scoped,仅使用 module”的规范。

四、核心实现原理

1. 模板 Ref 与容器

组件使用 useTemplateRef 获取 DOM 容器,并通过 watch 该容器何时挂载完成:

const container = useTemplateRef("container-element");

watch(container, (container) => {
  if (!container) return;
  const result = editor.create(container, {
    value: props.defaultCode,
    language: props.language,
  });
  instance.value = result;
  onWatcherCleanup(() => result.dispose());
});
  • 容器创建时:在 containernull 变为 DOM 元素时,通过 editor.create 创建 IStandaloneCodeEditor 实例。
  • 配置项
    • value:初始化文本内容,从 props.defaultCode 读取。
    • language:语言模式,从 props.language 读取。
  • 销毁逻辑:使用 onWatcherCleanup 在组件卸载或 container 变更时调用 editor.dispose(),避免内存泄漏。

2. MonacoEnvironment 与 Worker 配置

为了让 monaco-editor 能在浏览器环境正确加载语言服务,需要配置全局的 MonacoEnvironment.getWorker

if (!globalThis.MonacoEnvironment) {
  globalThis.MonacoEnvironment = {
    getWorker(workerId, label) {
      switch (label) {
        case "json":
          return new JsonWorker({ name: label });
        case "css":
        case "scss":
        case "less":
          return new CssWorker({ name: label });
        case "html":
        case "handlebars":
        case "razor":
          return new HtmlWorker({ name: label });
        case "typescript":
        case "javascript":
          return new TypeScriptWorker({ name: label });
        default:
          return new EditorWorker({ name: label });
      }
    },
  };
}
  • 幂等性:仅在 globalThis.MonacoEnvironment 尚未定义时进行设置,避免多次注册影响其他使用 Monaco 的模块。
  • 按 label 路由到不同 worker
    • jsonJsonWorker
    • css/scss/lessCssWorker
    • html/handlebars/razorHtmlWorker
    • typescript/javascriptTypeScriptWorker
    • 其他语言默认走 EditorWorker

这一配置保证了 Monaco 各语言的语法高亮、自动补全、错误提示等特性可以正常工作。

五、使用方式与代码示例

示例 1:在页面中基础使用

以下示例演示如何在某个页面组件中使用 MonacoEditor,并通过模板 ref 访问其暴露的 instance

<script setup lang="ts">
import MonacoEditor from "@/components/MonacoEditor/MonacoEditor.vue";

// 使用 useTemplateRef 获取子组件实例
const monacoRef = useTemplateRef("monaco-editor");

const initialCode = `function hello(name: string) {
  console.log('Hello ' + name);
}`;

function logCurrentCode() {
  const editorInstance = monacoRef.value?.instance;
  if (!editorInstance) return;

  const value = editorInstance.getValue();
  console.log("当前编辑器内容:", value);
}
</script>

<template>
  <section class="page">
    <MonacoEditor ref="monaco-editor" :default-code="initialCode" language="typescript" />

    <ElButton type="primary" @click="logCurrentCode"> 打印当前代码 </ElButton>
  </section>
</template>

要点:

  • 组件引入路径使用 @/components/MonacoEditor/MonacoEditor.vue,符合项目别名规范。
  • 通过 ref="monaco-editor" 获取子组件实例,并读取其暴露的 instance 属性。
  • 调用 instance.getValue() 获取当前编辑器中的代码内容。

示例 2:外部控制代码内容

如果需要在父组件中根据业务逻辑设置 Monaco 的内容,可以利用暴露的 instance 调用 setValue

<script setup lang="ts">
import MonacoEditor from "@/components/MonacoEditor/MonacoEditor.vue";

const monacoRef = useTemplateRef("monaco-editor");

function loadTemplate() {
  const editorInstance = monacoRef.value?.instance;
  if (!editorInstance) return;

  const templateCode = `interface User { id: number; name: string; }
    const user: User = { id: 1, name: 'Alice' };
`;

  editorInstance.setValue(templateCode);
}
</script>

<template>
  <section>
    <MonacoEditor ref="monaco-editor" language="typescript" />

    <ElButton @click="loadTemplate">加载示例模板</ElButton>
  </section>
</template>

要点:

  • 无需通过 props 传入默认内容,可以在任意时机通过 instance.setValue 动态设置。
  • 更复杂场景下还可以使用 Monaco 的模型管理、多文件编辑等高级特性,方式与原生 Monaco API 完全一致。

示例 3:根据语言切换编辑器模式

若希望在父组件中通过下拉框切换 Monaco 语言模式,可以将语言保存在响应式变量中,传给 MonacoEditorlanguage props:

<script setup lang="ts">
import MonacoEditor from "@/components/MonacoEditor/MonacoEditor.vue";

const language = ref("typescript");

const options = [
  { label: "TypeScript", value: "typescript" },
  { label: "JavaScript", value: "javascript" },
  { label: "JSON", value: "json" },
  { label: "CSS", value: "css" },
];
</script>

<template>
  <section>
    <ElSelect v-model="language">
      <ElOption v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
    </ElSelect>

    <MonacoEditor :language="language" />
  </section>
</template>

说明:

  • 组件内部会将最新的 language 传给 editor.create,在首次创建时生效。
  • 如需动态切换已创建实例的语言,可在父组件中通过 instance.getModel() + monaco.editor.setModelLanguage 方式扩展,此部分可按具体业务需要另行封装。

六、扩展与优化建议

当前 MonacoEditor 是一个 轻封装 组件,仅暴露核心的实例能力,后续可以按需扩展:

  • 增加事件回调 Props
    • 如:onChange(value: string)onBlur 等,在组件内部通过 editorInstance.onDidChangeModelContent 等 API 触发。
  • 增加配置项 Props
    • 支持传入 themevs-dark / vs-light)、readOnlyminimap 显示等。
    • 封装一个 editorOptions props 直接透传给 editor.create
  • 尺寸自适应
    • 父容器尺寸变化时调用 editor.layout(),可通过 ResizeObserver 或封装指令实现。
  • 多语言模型管理
    • 封装对 editor.createModeleditor.setModel 的操作,支持在一个编辑器中切换不同文件/语言。

在现有需求下,本文档描述的实现已经满足大多数代码编辑场景;如有新的业务需求,可以在保持对外 API 稳定的前提下,在组件内部逐步迭代能力。

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com