MonacoEditor 组件技术方案
一、组件概述
MonacoEditor 组件封装了 monaco-editor 的基础使用能力,用于在项目中提供一个可复用的代码编辑器视图。
当前功能聚焦于:
- 初始化 Monaco 编辑器实例并挂载到指定容器中。
- 支持设置默认代码内容与语言类型(如
typescript、javascript、json等)。 - 对外暴露编辑器实例,方便在父组件中进行高级操作(如读取/设置内容、注册事件等)。
- 统一配置 Monaco Web Worker,保证不同语言特性正常工作。
二、依赖与运行环境
- 编辑器核心依赖:
monaco-editor - 运行环境:基于 Vite + Vue 3 组合,使用
<script setup lang="ts">语法。 - 自动导入:项目通过 unplugin 自动导入 Vue API,例如
useTemplateRef、watch、shallowRef等。
组件内部引用的 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 会使用默认语言(通常是纯文本或基于内容推断)。
- 作用:指定 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>标签作为容器。 - 模板 ref:
ref="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());
});
- 容器创建时:在
container从null变为 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:
json→JsonWorkercss/scss/less→CssWorkerhtml/handlebars/razor→HtmlWorkertypescript/javascript→TypeScriptWorker- 其他语言默认走
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 语言模式,可以将语言保存在响应式变量中,传给 MonacoEditor 的 language 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:
- 支持传入
theme(vs-dark/vs-light)、readOnly、minimap显示等。 - 封装一个
editorOptionsprops 直接透传给editor.create。
- 支持传入
- 尺寸自适应:
- 父容器尺寸变化时调用
editor.layout(),可通过监听ResizeObserver或封装指令实现。
- 父容器尺寸变化时调用
- 多语言模型管理:
- 封装对
editor.createModel、editor.setModel的操作,支持在一个编辑器中切换不同文件/语言。
- 封装对
在现有需求下,本文档描述的实现已经满足大多数代码编辑场景;如有新的业务需求,可以在保持对外 API 稳定的前提下,在组件内部逐步迭代能力。