二次元绘画创作
56.21M · 2026-02-04
MonacoEditor 组件封装了 monaco-editor 的基础使用能力,用于在项目中提供一个可复用的代码编辑器视图。
当前功能聚焦于:
typescript、javascript、json 等)。monaco-editor<script setup lang="ts"> 语法。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 能在浏览器环境下正确运行语言服务。
<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>
组件通过 defineProps 定义如下两个可选属性:
const props = defineProps<{
defaultCode?: string;
language?: string;
}>();
defaultCode?: string:
language?: string:
"typescript"、"javascript"、"json"、"css"、"html" 等。组件内部维护一个 shallowRef<editor.IStandaloneCodeEditor> 用来保存 Monaco 实例,并通过 defineExpose 暴露给父组件:
const instance = shallowRef<editor.IStandaloneCodeEditor>();
// ... 创建完成后赋值
instance.value = result;
// 对外暴露
defineExpose({ instance });
父组件可以通过模板 ref 获取到组件实例,然后访问 instance 字段调用 Monaco 的全部 API。
<template>
<article :class="$style.editor" />
</template>
<style module>
.editor {
overflow: hidden;
border: 1px solid var(--el-border-color);
}
</style>
<article> 标签作为容器。ref="container-element",配合 useTemplateRef("container-element") 获取 DOM 元素。<style module>),遵守项目“禁止 scoped,仅使用 module”的规范。组件使用 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(),避免内存泄漏。为了让 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 的模块。json → JsonWorkercss/scss/less → CssWorkerhtml/handlebars/razor → HtmlWorkertypescript/javascript → TypeScriptWorkerEditorWorker这一配置保证了 Monaco 各语言的语法高亮、自动补全、错误提示等特性可以正常工作。
以下示例演示如何在某个页面组件中使用 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() 获取当前编辑器中的代码内容。如果需要在父组件中根据业务逻辑设置 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>
要点:
instance.setValue 动态设置。若希望在父组件中通过下拉框切换 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 是一个 轻封装 组件,仅暴露核心的实例能力,后续可以按需扩展:
onChange(value: string)、onBlur 等,在组件内部通过 editorInstance.onDidChangeModelContent 等 API 触发。theme(vs-dark / vs-light)、readOnly、minimap 显示等。editorOptions props 直接透传给 editor.create。editor.layout(),可通过 ResizeObserver 或封装指令实现。editor.createModel、editor.setModel 的操作,支持在一个编辑器中切换不同文件/语言。在现有需求下,本文档描述的实现已经满足大多数代码编辑场景;如有新的业务需求,可以在保持对外 API 稳定的前提下,在组件内部逐步迭代能力。