Quasar(vue3) 添加 monaco-editor

项目要用到代码编辑器了,本来是一个 textarea 输入的,但体验实在太差,想了想还是集成编辑器吧。

首先,安装依赖包:

yarn add monaco-editor

然后封装一个编辑器组件:

<template>
  <q-resize-observer @resize="handleResize" />
  <div
    class="fit"
    ref="containerEl"
  ></div>
</template>

<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import type { editor as Editor } from 'monaco-editor'
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'

const props = defineProps<{
  modelValue: string
  editorConfig: Editor.IStandaloneDiffEditorConstructionOptions &
    Record<any, any>
}>()

const emit = defineEmits<{
  (e: 'update:modelValue', data: string): void
}>()

const containerEl = ref()

let editor: Editor.IStandaloneCodeEditor | null = null

const defaultConfig = {
  language: 'javascript',
  theme: 'vs',
}

onMounted(async () => {
  if (containerEl.value) {
    const monaco = await import('monaco-editor')
    self.MonacoEnvironment = {
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      getWorker: function (workerId, label) {
        switch (label) {
          case 'json':
            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
            // @ts-ignore
            return new jsonWorker()
          case 'css':
          case 'scss':
          case 'less':
            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
            // @ts-ignore
            return new cssWorker()
          case 'html':
          case 'handlebars':
          case 'razor':
            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
            // @ts-ignore
            return new htmlWorker()
          case 'typescript':
          case 'javascript':
            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
            // @ts-ignore
            return new tsWorker()
          default:
            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
            // @ts-ignore
            return new editorWorker()
        }
      },
    }
    editor = monaco.editor.create(containerEl.value, {
      ...defaultConfig,
      ...props.editorConfig,
      value: props.modelValue,
    })
    console.log(editor)
    editor.onDidChangeModelContent((e) => {
      console.log(e)
      emit('update:modelValue', editor?.getValue?.() ?? '')
    })
  }
})

onBeforeUnmount(() => {
  editor?.dispose?.()
})

watch(
  props.editorConfig,
  (newVal) => {
    if (editor) {
      editor.updateOptions(newVal)
    }
  },
  {
    deep: true,
  }
)

function handleResize() {
  editor?.layout?.()
}
</script>

为了减少包体积,也为了避免 ssr 服务端报错,我是在 onMounted 钩子里动态引入的 monaco-editor。然后需要设置 MonacoEnvironment 这个环境配置对象,否则编辑器出不来,控制台会报错:getWorkerUrl undefined 之类的。

我这里的配置是 vite 的,其他打包工具的配置参见文档:https://github.com/microsoft/monaco-editor/blob/main/docs/integrate-esm.md

上面这个文档里有关于 vite 的配置:

但实操下来发现文档里的配置无效,所以我是直接引入的相关 worker 文件,然后在 switch case 里直接实例化。

那为什么我不采用动态引入的方式呢?不是我不想,而是动态引入没生效。本来我是这么写的:

case 'scss':
case 'less':
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  // @ts-ignore
  return import('monaco-editor/esm/vs/language/css/css.worker?worker')

以及这么写:

const editorWorker = await import(
  'monaco-editor/esm/vs/editor/editor.worker?worker'
)
self.MonacoEnvironment = {
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  // @ts-ignore
  getWorker: function (workerId, label) {

    switch (label) {
      ……
      default:
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        return new editorWorker()
    }
  },
}

但这两种方法都无法实例化 worker,所以我只能在顶层引入相关包。