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