Vue3 TypeScript Monaco Editor 网页编辑器

1,181 阅读5分钟

概述

Monaco Editor 是一款由微软开发的高性能、功能丰富的基于浏览器的代码编辑器,它是Visual Studio Code源生编辑器的核心组件,专为Web环境设计。Monaco Editor以其强大的代码编辑功能著称,包括语法高亮、代码自动完成(智能提示)、代码片段、代码折叠、错误标记、以及多种编程语言的支持等,为用户提供接近桌面级IDE的在线编码体验。

实现

技术栈为vue3 vite typescript monaco-editor

安装依赖

# 安装  monaco-editor
pnpm install monaco-editor

版本号

"monaco-editor": "^0.48.0",

创建组件

  • 增加锚点

    <div ref="codeEditBox" class="codeEditBox"></div>
    
  • 导入worker和monaco-editor

    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";
    import EditorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
    import * as monaco from "monaco-editor";
    
  • 创建 monaco-editor 环境

    self.MonacoEnvironment = {
        getWorker(_: string, label: string) {
          if (label === "json") {
            return new jsonWorker();
          }
          if (["css", "scss", "less"].includes(label)) {
            return new cssWorker();
          }
          if (["html", "handlebars", "razor"].includes(label)) {
            return new htmlWorker();
          }
          if (["typescript", "javascript"].includes(label)) {
            return new tsWorker();
          }
          return new EditorWorker();
        },
      };
    
  • 创建editor

    editor = monaco.editor.create(codeEditBox.value, {
      value: monacoEditorValue.value,
      language: props.language,
      theme: props.editorProps.theme,
      ...props.editorProps.options,
    });
    
  • 内容改变抛出事件

    editor.onDidChangeModelContent(() => {
      const value = editor.getValue();
      emit("update:modelValue", value);
      emit("change", value);
    });
    
  • 添加事件,主要是为了格式化代码

    editor.addAction({
      id: "formatDocument",
      label: "Format Document",
      keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.F9],
      run: (ed: monaco.editor.ICodeEditor) => {
        ed.getAction("editor.action.formatDocument")?.run();
      },
    });
    
  • 配置智能化提示

    provider = monaco.languages.registerCompletionItemProvider(props.language, {
      provideCompletionItems: function (model, position) {
        // 获取范围
        const range = getRange(model, position);
        const suggestions = props.suggestions.map((s: ISuggestions) => ({
          // 显示的提示内容
          label: s.label, 
          // 用来显示提示内容后的不同的图标
          kind: monaco.languages.CompletionItemKind[s.kind], 
          // 选择后粘贴到编辑器中的文字
          insertText: s.insertText, 
          // 提示内容后的说明
          detail: s.detail, 
          range: range,
        }));
        if (model.uri.toString() === editor.getModel()!.uri.toString()) {
          return {
            suggestions,
          };
        }
      },
    });
    

以上是关键代码

详细代码

  • 组件文件
    <template>
      <div ref="codeEditBox" class="codeEditBox"></div>
    </template>   
    <script setup lang="ts">
      import {
        defaultEditorProps,
        type IEditorProps,
        type ISuggestions,
      } from "./monaco-editor-type";
      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";
      import EditorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
      import * as monaco from "monaco-editor";
      import { useVModel, watchImmediate, watchDeep } from "@vueuse/core";
      import { onBeforeUnmount, onMounted, ref } from "vue";
    
      const props = withDefaults(defineProps<IEditorProps>(), {
        editorProps: defaultEditorProps,
        registerLanguage: null,
        modelValue: "",
        width: "100%",
        height: "100%",
        language: "javascript",
        suggestions: () => [],
      });
    
      const emit = defineEmits(["update:modelValue", "change", "editor-mounted"]);
      const monacoEditorValue = useVModel(props, "modelValue", emit);
      self.MonacoEnvironment = {
        getWorker(_: string, label: string) {
          if (label === "json") {
            return new jsonWorker();
          }
          if (["css", "scss", "less"].includes(label)) {
            return new cssWorker();
          }
          if (["html", "handlebars", "razor"].includes(label)) {
            return new htmlWorker();
          }
          if (["typescript", "javascript"].includes(label)) {
            return new tsWorker();
          }
          return new EditorWorker();
        },
      };
      let editor: monaco.editor.IStandaloneCodeEditor;
    
      const codeEditBox = ref();
      let provider:any = null
      const init = () => {
    
        registerLanguage()
    
        editor = monaco.editor.create(codeEditBox.value, {
          value: monacoEditorValue.value,
          language: props.language,
          theme: props.editorProps.theme,
          ...props.editorProps.options,
        });
        editor.onDidChangeModelContent(() => {
          const value = editor.getValue();
          emit("update:modelValue", value);
          emit("change", value);
        });
    
        editor.addAction({
          id: "formatDocument",
          label: "Format Document",
          keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.F9],
          run: (ed: monaco.editor.ICodeEditor) => {
            ed.getAction("editor.action.formatDocument")?.run();
          },
        });
        provider = monaco.languages.registerCompletionItemProvider(props.language, {
          provideCompletionItems: function (model, position) {
            // 获取范围
            const range = getRange(model, position);
            const suggestions = props.suggestions.map((s: ISuggestions) => ({
              // 显示的提示内容
              label: s.label, 
              // 用来显示提示内容后的不同的图标
              kind: monaco.languages.CompletionItemKind[s.kind], 
              // 选择后粘贴到编辑器中的文字
              insertText: s.insertText, 
              // 提示内容后的说明
              detail: s.detail, 
              range: range,
            }));
            if (model.uri.toString() === editor.getModel()!.uri.toString()) {
              return {
                suggestions,
              };
            }
          },
        });
        emit("editor-mounted", editor);
      };
    
      const getRange = (
        model: monaco.editor.ITextModel,
        position: monaco.Position
      ) => {
        // 获取当前行数 当前列数
        const [line, column] = [position.lineNumber, position.column];
        // 获取当前输入行的所有内容
        const content = model.getLineContent(line);
        // 通过下标来获取当前光标后一个内容,即为刚输入的内容
        const sym = content[column - 2];
        const word = model.getWordUntilPosition(position);
        return {
          startLineNumber: position.lineNumber,
          endLineNumber: position.lineNumber,
          startColumn: word.startColumn,
          endColumn: word.endColumn,
        };
      };
      const registerLanguage = () => {
        if(props.registerLanguage) {
          monaco.languages.register({ id: props.language })
          monaco.languages.setMonarchTokensProvider(props.language, props.registerLanguage.tokens)
        }
      }
      watchImmediate(monacoEditorValue, (newValue) => {
        if (editor) {
          const value = editor.getValue();
          if (newValue !== value) {
            editor.setValue(newValue);
          }
        }
      });
      const reload = () => {
        editor.dispose();
        provider.dispose()
        init();
      };
      watchDeep(
        () => props.language,
        (newValue) => {
          monaco.editor.setModelLanguage(editor.getModel()!, newValue);
          reload();
        }
      );
      onBeforeUnmount(() => {
        editor.dispose();
      });
      onMounted(() => {
        init();
      });
    </script>
    <style lang="scss" scoped>
      .codeEditBox {
        height: 100%;
      }
    </style> 
    
  • 类型文件
      import { type PropType } from "vue";
      export type Theme = "vs" | "hc-black" | "vs-dark";
      export type FoldingStrategy = "auto" | "indentation";
      export type RenderLineHighlight = "all" | "line" | "none" | "gutter";
      export interface IEditorProps {
        editorProps?: any;
        registerLanguage?: any;
        modelValue: string;
        width?: string | number;
        height?: string | number;
        language: string;
        suggestions?: ISuggestions[];
      }
      export interface ISuggestions {
        label: string;
        kind: string;
        insertText: string;
        detail: string;
        range?: {};
      }
      export interface Options {
        automaticLayout?: boolean;
        foldingStrategy?: FoldingStrategy;
        renderLineHighlight?: RenderLineHighlight;
        selectOnLineNumbers?: boolean;
        minimap?: {
          enabled: boolean;
        };
        readOnly?: boolean;
        fontSize?: number;
        scrollBeyondLastLine?: boolean;
        overviewRulerBorder?: boolean;
      }
    
      export interface IEditor {
        theme: Theme;
        options: Options;
      }
    
      export const defaultEditorProps: IEditor = {
        theme: 'vs-dark',
        options:  {
          automaticLayout: true,
          foldingStrategy: "indentation",
          renderLineHighlight: "all",
          selectOnLineNumbers: true,
          minimap: {
            enabled: true,
          },
          readOnly: false,
          fontSize: 16,
          scrollBeyondLastLine: false,
          overviewRulerBorder: false,
        }
      };
    
    
  • 使用

vue文件

  <script lang="ts" setup>
    import MonacoEditor from "./components/monaco-editor/index.vue";
    import { type ISuggestions } from "./components/monaco-editor/monaco-editor-type";
    import { onMounted, ref } from "vue";
    import {
      showJsonValue,
      showJavaScriptValue,
      javaScriptSuggestions,
      jsonSuggestions,
      showCssValue,
      cssSuggestions,
      showGroovyValue,
      groovySuggestions,
      editorProps,
      registerLanguage,
      options
    } from "./app-type.ts";
    const language = ref("javascript");
    const showValue = ref(showJsonValue);
    const change = () => {
      switch (language.value) {
        case "javascript":
          showValue.value = showJavaScriptValue;
          suggestions.value = javaScriptSuggestions;
          break;
        case "json":
          showValue.value = showJsonValue;
          suggestions.value = jsonSuggestions;
          break;
        case "css":
          showValue.value = showCssValue;
          suggestions.value = cssSuggestions;
          break;
        case "groovy":
          showValue.value = showGroovyValue;
          suggestions.value = groovySuggestions;
          break;
        default:
          break;
      }
    };

    const suggestions = ref<ISuggestions[] | undefined>([]);

    onMounted(() => {
      change();
    });
  </script>
  <template>
      <div class="container">
        <monaco-editor
          v-model="showValue"
          :language="language"
          :editor-props=editorProps
          :suggestions="suggestions"
          :register-language="registerLanguage"
        ></monaco-editor>

        <el-select
          style="margin-top: 16px"
          v-model="language"
          class="m-2"
          placeholder="Select"
          size="large"
          @change="change"
        >
          <el-option
            v-for="item in options"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
        </el-select>
      </div>
  </template>
  <style lang="scss" scoped>
    .container {
      height: 500px;
    }
  </style>

涉及的变量

import type { IEditor } from "./components/monaco-editor/monaco-editor-type";

export const showJsonValue = `{
    "star_male": [
        {
            "name": "鹿晗",
            "age": 26
        },
        {
            "name": "李易峰",
            "age": 29
        },
        {
            "name": "陈赫",
            "age": 31
        }
    ]
}`;

export const showJavaScriptValue = `function test() {
    console.log('hello world')
}`;

export const showCssValue = `body {
    background-color: #f0f0f0;
}`;

export const showGroovyValue = `pipeline {
    agent any
    stages {
        stage('Build') {
            steps {
                sh 'echo "Hello World"'
            }
        }
    }
}`;

export const options = [
  {
    value: "javascript",
    label: "JavaScript",
  },
  {
    value: "json",
    label: "JSON",
  },
  {
    value: "css",
    label: "CSS",
  },
  {
    value: "groovy",
    label: "Groovy",
  },
];

export const jsonSuggestions = [
  {
    label: "images:17",
    kind: "Value",
    insertText: "image: nginx: 1.17.3",
    detail: "提供的默认镜像 1.17.3",
  },
  {
    label: "images:18",
    kind: "Value",
    insertText: "image: nginx: 1.18.3",
    detail: "提供的默认镜像 1.18.3",
  },
  {
    label: "images:19",
    kind: "Value",
    insertText: "image: nginx: 1.19.3",
    detail: "提供的默认镜像 1.19.3",
  },
];
export const javaScriptSuggestions = [
  { label: "vue", kind: "Value", insertText: "vue: 2", detail: "vue 版本号 2" },
  {
    label: "html",
    kind: "Value",
    insertText: "html: 5",
    detail: "html 版本号 5",
  },
  { label: "jsp", kind: "Value", insertText: "jsp: 2", detail: "jsp 版本号 2" },
];
export const cssSuggestions = [
  {
    label: "css1",
    kind: "Value",
    insertText: "css: 1",
    detail: "css 版本号 1",
  },
  {
    label: "css2",
    kind: "Value",
    insertText: "css: 2",
    detail: "css 版本号 2",
  },
  {
    label: "css3",
    kind: "Value",
    insertText: "css: 3",
    detail: "css 版本号 3",
  },
];
export const groovySuggestions = [
  {
    label: "pipeline",
    kind: "Keyword",
    insertText: "pipeline",
    detail: "关键字 pipeline",
  },
  {
    label: "agent",
    kind: "Keyword",
    insertText: "agent",
    detail: "关键字 agent",
  },
  {
    label: "node",
    kind: "Keyword",
    insertText: "node",
    detail: "关键字 node",
  },
  {
    label: "label",
    kind: "Keyword",
    insertText: "label",
    detail: "关键字 label",
  },
  {
    label: "stages",
    kind: "Keyword",
    insertText: "stages",
    detail: "关键字 stages",
  },
  {
    label: "container",
    kind: "Keyword",
    insertText: `container {
  
  }`,
    detail: "关键字 container",
  },
];

export const registerLanguage = {
  language: "groovy",
  tokens: {
    tokenizer: {
      root: [
        [
          /\b(?:pipeline|agent|node|label|stages|stage|steps|git|url|credentialsId|branch|changelog|poll|container|withSonarQubeEnv|sh|timeout|time|unit|waitForQualityGate)\b/,
          "keyword",
        ],
        [
          /\b(?:if|else|for|while|switch|case|break|continue|return|try|catch|finally|throw|throws|private|protected|public|static|class|interface|extends|implements|import|package|void|new|instanceof|this|super|true|false|null)\b/,
          "keyword",
        ],
        [
          /\b(?:def|boolean|byte|char|short|int|long|float|double|void|Boolean|Byte|Character|Short|Integer|Long|Float|Double|String|Object|void)\b/,
          "keyword",
        ],
        [/\b(?:true|false|null)\b/, "keyword"],
        [/\b(?:String|List|Map|Set|ArrayList|HashMap|HashSet)\b/, "keyword"],
        [
          /\b(?:println|print|printf|sprintf|format|assert|delete|remove|add|contains|size|length|charAt|indexOf|lastIndexOf|substring|split|join|replace|replaceAll|replaceFirst|matches|contains|startsWith|endsWith|toLowerCase|toUpperCase|trim|valueOf|parseInt|parseLong|parseDouble|parseBoolean|toString|getClass|wait|notify|notifyAll|clone|equals|hashCode|finalize|getClass|notify|toString|wait)\b/,
          "keyword",
        ],
        [/\b(?:null|true|false)\b/, "keyword"],
        [/\b(?:it|args|this|super)\b/, "keyword"],
        [/\b(?:import|package)\b/, "keyword"],
        [
          /\b(?:class|interface|enum|trait|extends|implements|static|public|protected|private|abstract|final|native|synchronized|transient|volatile|strictfp)\b/,
          "keyword",
        ],
      ],
    },
  },
};

export const editorProps: IEditor = {
  theme: "vs-dark",
  options: {
    minimap: {
      enabled: false,
    },
  },
};

结语

Monaco Editor是一个强大且灵活的解决方案,适用于那些需要在Web应用中提供代码编辑能力的场景,比如在线代码编辑器、教育平台、云IDE等。