Monaco Editor Vue组件封装,支持全屏编辑、函数签名、占位符等功能

727 阅读4分钟

概述

这是一个基于Monaco EditorVue 2组件封装,Monaco EditorVisual Studio Code使用的代码编辑器,具有强大的代码高亮、智能提示、语法检查等功能。本组件对其进行了封装,添加了许多实用的功能特性,使其更适合在Vue项目中使用。

image.png

封装思路

1. 组件化设计

Monaco Editor包装成一个可复用的Vue组件,通过props传递配置参数,通过events向外传递变化,遵循Vue组件的设计规范:

// 核心Props设计
props: {
  value: String,           // 编辑器内容
  language: String,        // 编程语言
  customLanguages: Array,  // 自定义语言扩展
  theme: String,          // 主题
  minimap: Boolean,       // 小地图
  readOnly: Boolean,      // 只读模式
  functionParams: Array,  // 函数参数显示
  placeholder: String     // 占位符
}

2. 生命周期管理

合理处理Monaco Editor的创建、更新和销毁:

  • mounted: 初始化编辑器实例
  • watch: 监听props变化并同步到编辑器
  • beforeDestroy: 清理资源,防止内存泄漏

3. 功能增强策略

在原有Monaco Editor基础上,增加了多个实用功能:

  • 全屏编辑模式
  • 函数签名显示
  • 占位符支持
  • 多语言扩展机制

主要功能特性

1. 多语言支持

组件内置了三种常用语言的语法高亮:

const defaultLanguages = [
  { id: 'java', tokenProvider: monacoJava },
  { id: 'javascript', tokenProvider: monacoJavaScript },
  { id: 'http', tokenProvider: monacoHttp }
];

同时支持通过customLanguages属性动态扩展更多语言。

2. 全屏编辑功能

提供完整的全屏编辑体验:

toggleFullscreen() {
  this.isFullscreen = !this.isFullscreen;
  
  if (this.isFullscreen) {
    document.body.classList.add('monaco-editor-fullscreen');
    document.addEventListener('keydown', this.handleEscKey);
  } else {
    document.body.classList.remove('monaco-editor-fullscreen');
    document.removeEventListener('keydown', this.handleEscKey);
  }
}

特点:

  • 支持ESC键退出全屏
  • 全屏时自动隐藏页面滚动条
  • 响应式布局自动适配

3. 函数参数智能显示

当编辑函数代码时,可以在编辑器顶部显示函数签名:

<div v-if="functionParams.length" class="function-header flex">
  <span class="function-keyword ml5 mr5">function</span>
  (<span class="function-params flex">
    <span v-for="param in functionParams" :key="param.tip">
      <qz-tips :tips-content="param.tip" placement="top">
        <span>{{ param.label }}</span>
      </qz-tips>
    </span>
  </span>)
  <span class="function-brace ml5">{</span>
</div>

功能亮点:

  • 支持参数提示显示
  • 语法高亮的函数签名
  • 自动调整编辑区域高度

image.png

4. 占位符

computed: {
  formattedPlaceholder() {
    if (!this.placeholder) return '';
    return this.placeholder
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/\n/g, '<br/>');
  }
}

特性:

  • 支持HTML转义,防止XSS
  • 支持多行占位符显示
  • 仅在编辑器为空且非只读时显示

image.png

使用示例

<template>
  <monaco-editor
    v-model="code"
    language="javascript"
    :function-params="params"
    :show-fullscreen-btn="true"
    placeholder="请输入JavaScript代码..."
    @input="handleCodeChange"
  />
</template>

<script>
export default {
  data() {
    return {
      code: '',
      params: [
        { label: 'context', tip: '上下文对象,包含请求信息' },
        { label: 'data', tip: '业务数据对象' }
      ]
    }
  },
  methods: {
    handleCodeChange(newCode) {
      console.log('代码变更:', newCode);
    }
  }
}
</script>

组件代码

<template>
  <div
    ref="editorContainer"
    class="qz-monaco-editor"
    :class="{ 'is-fullscreen': isFullscreen }"
  >
    <!-- 函数声明显示区域 -->
    <div v-if="functionParams.length" class="function-header flex">
      <span class="function-keyword ml5 mr5">function</span>
      (<span class="function-params flex">
        <span v-for="param in functionParams" :key="param.tip">
          <qz-tips :tips-content="param.tip" placement="top">
            <span>{{ param.label }}</span>
          </qz-tips>
        </span> </span
      >)
      <span class="function-brace ml5">{</span>
    </div>

    <div class="editor-toolbar" v-if="showFullscreenBtn">
      <el-tooltip
        effect="dark"
        :content="isFullscreen ? '退出全屏' : '全屏'"
        placement="top"
      >
        <span class="pointer" @click="toggleFullscreen">
          <qz-icon
            :class="
              isFullscreen
                ? 'icon-24gl-fullScreenExit3'
                : 'icon-24gl-fullScreenEnter3'
            "
          ></qz-icon>
        </span>
      </el-tooltip>
    </div>

    <!-- 占位文本 -->
    <div v-if="placeholder && showPlaceholder" class="editor-placeholder">
      <div v-html="formattedPlaceholder"></div>
    </div>

    <!-- 编辑器区域 -->
    <div ref="editorContent" class="editor-content"></div>

    <!-- 函数结束大括号显示区域 -->
    <div v-if="functionParams.length" class="function-footer">
      <span class="function-brace ml5">}</span>
    </div>
  </div>
</template>

<script>
import { monacoJava, monacoJavaScript, monacoHttp } from '@/utils/code-utils';
import monaco from 'monaco-editor';

const defaultLanguages = [
  { id: 'java', tokenProvider: monacoJava },
  { id: 'javascript', tokenProvider: monacoJavaScript },
  { id: 'http', tokenProvider: monacoHttp }
];

export default {
  props: {
    value: {
      type: String,
      required: true
    },
    language: {
      type: String,
      default: 'javascript'
    },
    customLanguages: {
      type: Array,
      default: () => []
    },
    theme: {
      type: String,
      default: 'vs-light'
    },
    minimap: {
      type: Boolean,
      default: false
    },
    readOnly: {
      type: Boolean,
      default: false
    },
    showFullscreenBtn: {
      type: Boolean,
      default: true
    },
    // 函数参数
    functionParams: {
      type: Array,
      default: () => []
    },
    // 占位文本
    placeholder: {
      type: String,
      default: ''
    }
  },
  data() {
    return {
      editor: null,
      isFullscreen: false,
      isEmpty: true
    };
  },
  computed: {
    showPlaceholder() {
      return this.isEmpty && !this.readOnly;
    },
    // 格式化占位文本,将换行符转换为HTML换行
    formattedPlaceholder() {
      if (!this.placeholder) return '';
      return this.placeholder
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/\n/g, '<br/>');
    }
  },
  watch: {
    value(newValue) {
      if (this.editor && newValue !== this.editor.getValue()) {
        this.editor.setValue(newValue);
      }
      this.updateEmptyState(newValue);
    },
    language(newLanguage) {
      if (this.editor) {
        monaco.editor.setModelLanguage(this.editor.getModel(), newLanguage);
      }
    },
    customLanguages: {
      handler(newLanguages) {
        newLanguages.forEach((lang) => {
          this.registerLanguages(lang);
        });
      },
      deep: true
    },
    theme(newTheme) {
      if (this.editor) {
        monaco.editor.setTheme(newTheme);
      }
    },
    minimap(newMinimap) {
      if (this.editor) {
        this.editor.updateOptions({ minimap: { enabled: newMinimap } });
      }
    },
    readOnly(newReadOnly) {
      if (this.editor) {
        this.editor.updateOptions({ readOnly: newReadOnly });
      }
    },
    isFullscreen() {
      this.$nextTick(() => {
        if (this.editor) {
          // 重新计算和渲染布局
          this.editor.layout();
        }
      });
    }
  },
  methods: {
    registerLanguages(languages) {
      languages.forEach((lang) => {
        monaco.languages.register({ id: lang.id });
        monaco.languages.setMonarchTokensProvider(lang.id, lang.tokenProvider);
      });
    },
    toggleFullscreen() {
      this.isFullscreen = !this.isFullscreen;

      if (this.isFullscreen) {
        // 进入全屏模式
        document.body.classList.add('monaco-editor-fullscreen');
        document.addEventListener('keydown', this.handleEscKey);
      } else {
        // 退出全屏模式
        document.body.classList.remove('monaco-editor-fullscreen');
        document.removeEventListener('keydown', this.handleEscKey);
      }
    },
    handleEscKey(event) {
      if (event.key === 'Escape' && this.isFullscreen) {
        // 阻止事件冒泡
        event.preventDefault();
        event.stopPropagation();
        this.toggleFullscreen();
      }
    },
    // 更新空状态
    updateEmptyState(value) {
      this.isEmpty = !value.length;
    }
  },
  mounted() {
    this.registerLanguages(defaultLanguages.concat(this.customLanguages));

    // 初始化空状态
    this.updateEmptyState(this.value);

    this.editor = monaco.editor.create(this.$refs.editorContent, {
      value: this.value,
      language: this.language,
      theme: this.theme,
      automaticLayout: true,
      minimap: {
        enabled: this.minimap
      },
      readOnly: this.readOnly,
      stickyScroll: { enabled: false }
    });

    // 监听内容变化
    this.editor.onDidChangeModelContent(() => {
      const value = this.editor.getValue();
      this.updateEmptyState(value);
      this.$emit('input', value);
    });
  },
  beforeDestroy() {
    if (this.editor) {
      this.editor.dispose();
    }

    // 清理全屏相关的事件和样式
    document.body.classList.remove('monaco-editor-fullscreen');
    document.removeEventListener('keydown', this.handleEscKey);
  }
};
</script>

<style lang="less" scoped>
.qz-monaco-editor {
  position: relative;
  height: 100%;
  border: 1px solid #dcdfe6;

  .function-header,
  .function-footer {
    font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
    font-size: 12px;
    height: 18px;
    line-height: 18px;
    background: #fff;

    .function-keyword {
      color: #0000ff;
      font-weight: bold;
    }

    .function-params {
      color: #1677ff;
    }

    .function-brace {
      color: #333;
    }
  }

  .editor-toolbar {
    position: absolute;
    top: 0px;
    right: 14px;
    z-index: 10;
    color: #b8babf;
  }

  // 占位文本样式
  .editor-placeholder {
    height: 100%;
    overflow: hidden;
    position: absolute;
    top: 0px;
    left: 65px;
    color: #999;
    font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
    font-size: 12px;
    line-height: 18px;
    z-index: 1;
    opacity: 0.6;
    transition: opacity 0.2s ease;
    white-space: pre-wrap;
  }

  .editor-content {
    height: 100%;
    position: relative;
  }

  // 当显示函数头部时,调整编辑器内容高度和占位文本位置
  &:has(.function-header) {
    .editor-content {
      height: calc(100% - 36px);
    }

    .editor-placeholder {
      top: 19px;
    }
  }

  &.is-fullscreen {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 9999;
    background: #fff;
    border: none;

    .function-header {
      position: relative;
      top: 0;
    }

    .editor-toolbar {
      top: 0px;
      right: 16px;
    }

    .editor-content {
      height: 100vh;
      width: 100vw;
    }

    // 全屏模式下有函数头部时的高度调整
    &:has(.function-header) .editor-content {
      height: calc(100vh - 36px);
    }
  }
}
</style>

<style lang="less">
// 全局样式,防止全屏时页面滚动
body.monaco-editor-fullscreen {
  overflow: hidden;
}
</style>

总结

  1. 丰富的功能扩展 - 在保持Monaco Editor核心能力基础上,增加了实用的业务功能

  2. 良好的可配置性 - 通过props提供灵活的配置选项

  3. 优秀的用户体验 - 全屏模式、智能占位符等提升了编辑体验

源代码在此,如有需要,自取。