项目要用到代码编辑器了,本来是一个 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,所以我只能在顶层引入相关包。