在 Vue3 项目中集成 VSCode - 实现 Vite + Vue3 + MonacoEdit

8,167

概述

MonacoEdit 是微软提供的在线编辑器库,vscode 就是基于这个实现,现在实现将 MonacoEdit 集成到 vue3 项目中

方案采用 vite + vue3

实现方法

首先初始化一个 vue3 项目

npm init @vitejs/app editor-proj --template vue

创建完得到一个如下目录结构的项目

editor-proj
├─ public
│  └─ favicon.ico
├─ src
│  ├─ assets
│  │  └─ logo.png
│  ├─ components
│  │  └─ HelloWorld.vue
│  ├─ App.vue
│  └─ main.js
├─ index.html
├─ package.json
└─ vite.config.js

接下来安装 monaco-editor 依赖

yarn add monaco-editor

实现一个 json 编辑器组件并在 App.vue 中引用,最终目录结构如下

editor-proj
├─ public
│  └─ favicon.ico
├─ src
│  ├─ assets
│  │  └─ logo.png
│  ├─ components
│  │  └─ JsonEditor.vue
│  ├─ App.vue
│  └─ main.js
├─ index.html
├─ package.json
├─ vite.config.js
└─ yarn.lock

其中 JsonEditor.vue 的实现如下

<template>
  <div class="editor" ref="dom"></div>
</template>

<script setup>
import { onMounted, defineProps, defineEmit, ref } from 'vue';
import * as monaco from 'monaco-editor';

const props = defineProps({
  modelValue: String,
});

const emit = defineEmit(['update:modelValue']);

const dom = ref();

let instance;

onMounted(() => {
  const jsonModel = monaco.editor.createModel(
    props.modelValue,
    'json',
    monaco.Uri.parse('json://grid/settings.json')
  );

  instance = monaco.editor.create(dom.value, {
    model: jsonModel,
    tabSize: 2,
    automaticLayout: true,
    scrollBeyondLastLine: false,
  });

  instance.onDidChangeModelContent(() => {
    const value = instance.getValue();
    emit('update:modelValue', value);
  });
});
</script>

<style scoped>
.editor {
  height: 100%;
}
</style>

App.vue 的引用方法如下

<template>
  <div class="container">
    <JsonEditor v-model="code"></JsonEditor>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import JsonEditor from './components/JsonEditor.vue';

const code = ref('');
</script>

<style scoped>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}

.container {
  position: fixed;
  height: 100%;
  width: 100%;
}
</style>

这时候运行项目 yarn run dev 已经可以看到编辑器效果

但是存在一些问题,输入的 json 数据没法格式化,而且浏览器报错

Error: Unexpected usage
    at EditorSimpleWorker.loadForeignModule (editorSimpleWorker.js:454)
    at webWorker.js:38

这是由于 monaco-editor 的实现是基于 Web Worker 实现的,编辑器对语法的处理是通过开启 Worker 异步处理来提高效率,这时候还没有加载任何用来处理语法的 Worker

问题以及解决方案可以参考 github.com/vitejs/vite…

monaco-editor 会去获取全局变量里的 MonacoEnvironment 对象并执行 getWorker getWorkerUrl 来实现加载对应的语法处理

json 数据的语法处理,修改 JsonEditor.vue 文件,加入如下实现

import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';

self.MonacoEnvironment = {
  getWorker(workerId, label) {
    if (label === 'json') {
      return new JsonWorker();
    }
    return new EditorWorker();
  },
};

最终 JsonEditor.vue 的实现修改如下

<template>
  <div class="editor" ref="dom"></div>
</template>

<script setup>
import { onMounted, defineProps, defineEmit, ref } from 'vue';
import * as monaco 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';

self.MonacoEnvironment = {
  getWorker(workerId, label) {
    if (label === 'json') {
      return new JsonWorker();
    }
    return new EditorWorker();
  },
};

const props = defineProps({
  modelValue: String,
});

const emit = defineEmit(['update:modelValue']);

const dom = ref();

let instance;

onMounted(() => {
  const jsonModel = monaco.editor.createModel(props.modelValue, 'json');

  instance = monaco.editor.create(dom.value, {
    model: jsonModel,
    tabSize: 2,
    automaticLayout: true,
    scrollBeyondLastLine: false,
  });

  instance.onDidChangeModelContent(() => {
    const value = instance.getValue();
    emit('update:modelValue', value);
  });
});
</script>

<style scoped>
.editor {
  height: 100%;
}
</style>

再次运行项目时发现已经可以正常处理 json 数据,包括快捷键格式化都正常运行

img

如果想支持更多的语法只要引用相应 Worker

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';

填坑

项目在开发时可以运行,但是构建发布之后发现又出现异常

Uncaught ReferenceError: window is not defined
    at editor.worker.bfe8d272.js:1
Error: Unexpected usage
    at xm.loadForeignModule (editor.worker.bfe8d272.js:1)
    at editor.worker.bfe8d272.js:1

之前提到过,编辑器使用了 webworker 而在 webworker 里无法获取 window 和 document 对象,分析可能是打包的时候将功能独立的代码打包到一起使原本单独加载的模块放到 webworker 里跑了

vite 打包是基于 rollup 实现的,这里可以利用 rollup 的手动分片选项,将 worker 相关的单独打包来解决这个问题

vite.config.js 里最终实现如下

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import jsx from '@vitejs/plugin-vue-jsx';

const prefix = `monaco-editor/esm/vs`;

export default defineConfig({
  base: './',
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          jsonWorker: [`${prefix}/language/json/json.worker`],
          cssWorker: [`${prefix}/language/css/css.worker`],
          htmlWorker: [`${prefix}/language/html/html.worker`],
          tsWorker: [`${prefix}/language/typescript/ts.worker`],
          editorWorker: [`${prefix}/editor/editor.worker`],
        },
      },
    },
  },
  plugins: [vue(), jsx()],
});