Skip to content

使用 Monaco Editor 的注意事项

在使用 monaco-editor 时,尤其是在基于 Vite 的项目中,可能会遇到 worker 加载失败的问题。这是因为 monaco-editor 默认使用 Webpack 的方式加载 worker,而 Vite 的打包机制与 Webpack 不同,需要手动配置 worker

解决方案

为了让 monaco-editor 正常工作,我们需要显式地引入各个语言的 worker 文件,并通过 MonacoEnvironmentgetWorker 方法进行配置。以下是具体的处理方式:

引入 worker

我们通过以下方式引入 worker 文件:

typescript
// @ts-ignore
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
// @ts-ignore
import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker';
// @ts-ignore
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker';
// @ts-ignore
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
// @ts-ignore
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';

通过 ?worker 的方式引入文件是 Vite 的特性,它会将这些文件打包为独立的 worker 文件,并返回一个可以用于实例化的 Worker 类。

配置 MonacoEnvironment

我们需要通过 self.MonacoEnvironment 来覆盖默认的 getWorker 方法,根据语言标签返回对应的 worker 实例:

typescript
// eslint-disable-next-line no-restricted-globals
self.MonacoEnvironment = {
  getWorker(_, label) {
    if (label === 'json') {
      return new jsonWorker();
    }
    if (label === 'css' || label === 'scss' || label === 'less') {
      return new cssWorker();
    }
    if (label === 'html' || label === 'handlebars' || label === 'razor') {
      return new htmlWorker();
    }
    if (label === 'typescript' || label === 'javascript') {
      return new tsWorker();
    }
    return new editorWorker();
  },
};

为什么需要这一步?

monaco-editor 的默认行为是动态加载 worker 文件,但在 Vite 中,动态加载的路径可能会出错,导致 worker 无法找到。通过手动引入并配置 getWorker 方法,我们可以确保 worker 文件被正确加载。

总结

通过上述方式,我们可以在 Vite 项目中正常使用 monaco-editor,并支持多种语言的代码编辑功能。完整的代码示例可以参考以下内容:

vue
<template>
  <div ref="editorContainer" class="editor-content" :class="[showBorder ? 'border-class' : '']" />
</template>

<script setup lang="ts">
import * as monaco from 'monaco-editor';
// @ts-ignore
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
// @ts-ignore
import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker';
// @ts-ignore
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker';
// @ts-ignore
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
// @ts-ignore
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';

const props = defineProps<{
  modelValue?: string;
  language?: string;
  theme?: string;
  readOnly?: boolean;
  showBorder?: boolean;
}>();
const emit = defineEmits<{
  (e: 'update:modelValue', value: string): void;
  (e: 'change', value: string): void;
}>();
// eslint-disable-next-line no-restricted-globals
self.MonacoEnvironment = {
  getWorker(_, label) {
    if (label === 'json') {
      // eslint-disable-next-line new-cap
      return new jsonWorker();
    }
    if (label === 'css' || label === 'scss' || label === 'less') {
      // eslint-disable-next-line new-cap
      return new cssWorker();
    }
    if (label === 'html' || label === 'handlebars' || label === 'razor') {
      // eslint-disable-next-line new-cap
      return new htmlWorker();
    }
    if (label === 'typescript' || label === 'javascript') {
      // eslint-disable-next-line new-cap
      return new tsWorker();
    }
    // eslint-disable-next-line new-cap
    return new editorWorker();
  },
};
const editorContainer = ref<HTMLElement | null>(null);
const editor = ref<monaco.editor.IStandaloneCodeEditor | null>(null);
const currentLanguage = computed(() => {
  return props.language || 'json';
});

// Initialize Monaco Editor
onMounted(() => {
  if (editorContainer.value) {
    editor.value = monaco.editor.create(editorContainer.value, {
      value: props.modelValue || '',
      language: currentLanguage.value,
      theme: props.theme || 'vs',
      automaticLayout: true,
      minimap: {
        enabled: false,
      },
      scrollBeyondLastLine: false,
      fontSize: 14,
      lineNumbers: 'on',
      readOnly: props.readOnly || false,
      tabSize: 2,
      wordWrap: 'on',
      folding: true,
      lineDecorationsWidth: 10,
      lineNumbersMinChars: 3,
      renderLineHighlight: 'all',
      scrollbar: {
        vertical: 'visible',
        horizontal: 'visible',
        useShadows: false,
        verticalScrollbarSize: 10,
        horizontalScrollbarSize: 10,
      },
    });

    // Handle editor content changes
    toRaw(editor.value).onDidChangeModelContent(() => {
      const value = toRaw(editor.value)?.getValue() || '';
      emit('update:modelValue', value);
      emit('change', value);
    });
  }
});

// Watch for language changes
watch(currentLanguage, (newLanguage) => {
  if (editor.value) {
    monaco.editor.setModelLanguage(toRaw(editor.value).getModel()!, newLanguage);
  }
});

// Watch for value changes from parent
watch(
  () => props.modelValue,
  (newValue) => {
    if (editor.value && newValue !== toRaw(editor.value)?.getValue()) {
      toRaw(editor.value)?.setValue(newValue || '');
    }
  },
);

// Watch for readOnly changes
watch(
  () => props.readOnly,
  (newReadOnly) => {
    if (editor.value) {
      editor.value.updateOptions({ readOnly: newReadOnly });
    }
  },
);

// Watch for theme changes
watch(
  () => props.theme,
  (newTheme) => {
    if (editor.value && newTheme) {
      monaco.editor.setTheme(newTheme);
    }
  },
);

// Cleanup
onBeforeUnmount(() => {
  if (editor.value) {
    toRaw(editor.value)?.dispose?.();
  }
});
</script>

<style scoped>
.editor-content {
  width: 100%;
  height: 100%;
}
.editor-content.border-class {
  border: 1px solid var(--bmos-first-level-border-color);
  border-radius: 4px;
}
</style>