基于Vue2.X/Vue3.X对Monaco Editor在线代码编辑器进行封装与使用

最近有个需求是显示日志模块的信息,用了好多在线代码编辑器,如各式各样的 markdown 啥的,都不太好使......

最后发现微软的 Monaco Editor 在线代码编辑器,这个插件就是牛!对此进行基于Vue2.X/Vue3.X的封装和使用。

// 查看 xxx 版本
格式:npm view xxx versions --json
举例:npm view monaco-editor versions --json

// monaco-editor 插件,必须
npm install monaco-editor --save-dev

// monaco-editor-webpack-plugin 插件,非必须
npm install monaco-editor-webpack-plugin --save-dev

// monaco-editor-nls 插件,非必须
npm install monaco-editor-nls --save-dev

// monaco-editor-esm-webpack-plugin 插件,非必须
npm install monaco-editor-esm-webpack-plugin --save-dev

// 引入 font-awesome 图标库,非必须
npm install font-awesome --save

父组件:index.vue

<template>
  <div class="monaco" style="padding: 100px;">
    <div class="monaco-left">
      <p align="center">
        <el-button type="primary" @click="handleMEContentChangeClick">OK</el-button>
      </p>
    </div>

    <div class="monaco-right">
      <MonacoEditor
        class="monaco-editor"
        ref="monacoEditorRef"
        :editorParams="editorParams">
      </MonacoEditor>
    </div>
  </div>
</template>

<script>
import MonacoEditor from './components/monacoEditor'

export default {
  components: {
    MonacoEditor
  },
  data: () => ({
    // editorParams 必填非空的代码编辑器参数
    editorParams: {
      id: 'monaco_ed_1',// 编辑器DOM节点ID
      title: '',// 编辑器标题
      content: '',// 编辑器内容
      height: '100%',// 编辑器高度
      readOnly: false,// 编辑器是否禁用
      isScrollToBottom: true // 是否滚动到底部
    }
  }),
  methods: {
    /**
     * 改变编辑器内容
     */
    handleMEContentChangeClick() {
      this.editorParams.content = 
        "\n" +
        "  .   ____          _            __ _ _\n" +
        " /\\\\ / ___'_ __ _ _(_)_ __  __ _ \\ \\ \\ \\\n" +
        "( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \\\n" +
        " \\\\/  ___)| |_)| | | | | || (_| |  ) ) ) )\n" +
        "  '  |____| .__|_| |_|_| |_\\__, | / / / /\n" +
        " =========|_|==============|___/=/_/_/_/\n" +
        " :: Spring Boot ::        (v2.2.5.RELEASE)\n" +
        "\n" +
        new Date() +
        "\n" +
        Math.random()
    }
  }
}
</script>

<style lang="less" scoped>
  .monaco {
    width: calc(100% - 200px);
    height: calc(100% - 200px);
    position: relative;
    display: flex;

    .monaco-left {
      width: 600px;
      height: 100%;
      background-color: #f8f8f8;
    }

    .monaco-right {
      width: calc(100% - 600px);
      height: 100%;
      background-color: #ff9b9b;

      .monaco-editor {
        height: 100%;
        overflow: hidden;
      }
    }
  }
</style>

子组件:monacoEditor.vue

<template>
  <div class="m-e" id="m-e-id">
    <div class="m-e-main">
      <div class="m-e-main_toolbar" :style="isThemeLightOrBlack ? 'background-color: #fff; box-shadow: 0px 2px 5px #ddd;' : 'background-color: #1e1e1e; box-shadow: 0px 2px 5px #111;'">
        <div class="m-e-main_toolbar_left">
          <span>日志 - {{ title }}</span>
        </div>
        
        <div class="m-e-main_toolbar_right" :style="isThemeLightOrBlack ? 'color: #000' : 'color: #fff'">
          <a title="查找" @click="findByKeyword"><i class="fa fa-search"/></a>
          <a title="回到顶部" @click="scrollToTop"><i class="fa fa-chevron-circle-up"/></a>
          <a title="回到底部" @click="scrollToBottom"><i class="fa fa-chevron-circle-down"/></a>
          <a title="是否截断换行" @click="setEditorWordWrap"><i class="fa fa-bars"/></a>
          <a title="切换白天或暗夜模式" @click="setEditorTheme"><i class="fa fa-adjust"/></a>
          <a :title="fullScreen ? '退出全屏' : '全屏显示'" @click="handleFullScreenClick"><i :class="fullScreen ? 'fa fa-compress' : 'fa fa-arrows-alt'"/></a>
          <a title="下载日志" @click="handleDownloadLogClick"><i class="fa fa-download" style="position: relative; top: 2px"/></a>
        </div>
      </div>
      <div :id="id" class="m-e-main_container" :style="'height: ' + height"></div>
    </div>
  </div>
</template>
 
<script>
// 引用 font-awesome 资源
import 'font-awesome/css/font-awesome.min.css';

// 先汉化 monaco
import { setLocaleData } from "monaco-editor-nls"
import zh_CN from "monaco-editor-nls/locale/zh-hans"
setLocaleData(zh_CN)

// 再加载 monaco ,才能汉化成功
import * as me from 'monaco-editor'

export default {
  props:[
    'editorParams'
  ],
  data: () => ({
    editor: null,// 编辑器对象
    id: null,// 编辑器DOM节点ID
    title: '',// 编辑器标题
    content: '',// 编辑器内容
    height: 'auto',// 编辑器高度
    readOnly: true,// 编辑器是否禁用
    isScrollToBottom: false, // 是否滚动到底部

    // 其他配置项...
    fullScreen: false,// 是否全屏状态
    wordWrap: false,// 当单行文本太长时截断换行,true 为换行,false 为不换行
    isThemeLightOrBlack: false,// 明亮或暗夜模式,true 为白天模式,false 为暗夜模式
    
  }),
  mounted() {
    /**
     * 监听全屏显示状态
     */
    let that = this;
    window.onresize = function() {
      if (!document.fullscreenElement) {
        that.fullScreen = false;
      } else {
        that.fullScreen = true;
      } 
    }
  },
  watch: {
    /**
     * 深度监听富文本参数
     */
    "editorParams": {
        handler: function(newVal, oldVal) {
          // console.log('newVal =>', newVal, ' | oldVal =>', oldVal);
          if (oldVal == null && newVal != null) {
            // 首次变化
            this.id = newVal.id;
            this.title = newVal.title;
            this.content = newVal.content;
            this.height = 'calc(' + newVal.height + ' - 42px)';
            this.readOnly = newVal.readOnly;
            this.isScrollToBottom = newVal.isScrollToBottom;
            this.initEditor(this.id, this.content, this.readOnly);
            if (this.isScrollToBottom) {
              this.scrollToBottom(); // 滚动到底部
            }
          } else if (newVal != null && newVal != null) {
            // 二次变化
            this.title = newVal.title;
            this.content = newVal.content;
            this.isScrollToBottom = newVal.isScrollToBottom;
            if (this.isScrollToBottom) {
              this.editor.setValue(this.content);
              this.scrollToBottom(); // 滚动到底部
            } else {
              this.editor.setValue(this.content);
            }
            // this.setEditorContent(this.content);
          }
        },
        immediate: true,
        deep: true
    },
  },
  methods: {
    /**
     * 实例化在线代码编辑器
     * 
     * 文档地址:https://microsoft.github.io/monaco-editor/api/index.html
     */
    async initEditor(id, content, readOnly) {
      // 异步获取节点,确保 Dom 节点已经渲染完成,不可删
      let dom = await document.getElementById(this.id);
      const monaco = require("monaco-editor/esm/vs/editor/editor.api");
      this.editor = monaco.editor.create(document.getElementById(id), {
          value: content,// 编辑器内容
          language: 'python',// 选择支持语言
          automaticLayout: true,// 是否自动布局
          theme: 'vs-dark',// 官方自带三种主题:vs、hc-black、vs-dark
          readOnly: readOnly,// 设置是否只读
          wordWrap: this.wordWrap ? 'on' : 'off',// 设置启用截断功能
          scrollBeyondLastLine: false,// 滚动完最后一行后再滚动一屏幕

          // 滚动条
          // scrollbar: {
          //   verticalScrollbarSize: 15,
          //   horizontalScrollbarSize: 15
          // },

          // 是否开启小地图
          minimap: {
            enabled: true
          },
      });

      // 设置编辑器滚动到最底部
      // this.scrollToBottom();
    },

    /**
     * 设置编辑器的内容且滚动到最底部
     */
    setEditorContent(val) {
      this.editor.setValue(val);
      this.scrollToBottom();
    },

    /**
     * 获取编辑器的内容
     */
    getEditorContent() {
      this.editor.getValue();
    },

    /**
     * 打开编辑器查找功能
     */
    findByKeyword() {
      try {
        // 先聚焦编辑器
        this.editor.focus();

        // 从模型中获取要查找的字符串范围 new Range(startLineNumber, startColumn, endLineNumber, endColumn)
        this.editor.setSelection(new me.Range(1, 9999, 1, 10000));

        // 触发查找操作
        // this.editor.getAction('actions.find').run();// 查找方式一
        this.editor.trigger('', 'actions.find');// 查找方式二
      } catch(error) {
        console.log(error);
      }
    },

    /**
     * 设置编辑器从只读变成可写
     */
    setEditorRW() {
      this.editor.updateOptions({readOnly: false});
    },

    /**
     * 设置编辑器开关截断功能
     */
    setEditorWordWrap() {
      this.wordWrap = this.wordWrap ? false : true;
      if (this.wordWrap) {
        this.editor.updateOptions({wordWrap: 'on'});
      } else {
        this.editor.updateOptions({wordWrap: 'off'});
      }
    },

    /**
     * 设置编辑器明亮或暗夜模式
     */
    setEditorTheme() {
      this.isThemeLightOrBlack = this.isThemeLightOrBlack ? false : true;
      if (this.isThemeLightOrBlack) {
        this.editor.updateOptions({theme: 'vs'});
      } else {
        this.editor.updateOptions({theme: 'vs-dark'});
      }
    },

    /**
     * 设置编辑器滚动到最顶部
     */
    scrollToTop() {
      this.editor.setScrollPosition({scrollTop: 0});
    },

    /**
     * 设置编辑器滚动到最底部
     */
    scrollToBottom() {
      // this.editor.revealLineInCenter(99999);
      this.editor.revealLine(this.editor.getModel().getLineCount());
    },

    /**
     * 全屏显示句柄
     */
    handleFullScreenClick () {
			const element = document.getElementById('m-e-id');
      if (!document.fullscreenElement) {
        element.requestFullscreen();
      } else {
        document.exitFullscreen();
      } 
    },

    /**
     * 下载日志句柄
     */
    handleDownloadLogClick() {
      this.exportFile(this.title, this.content);
    },

    /**
     * 下载日志
     */
    exportFile(name, data) {
      let url = window.URL || window.webkitURL || window;
      let blob = new Blob([data]);
      let event = document.createEvent("MouseEvents");
      event.initMouseEvent("click", true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
      let link = document.createElementNS("http://www.w3.org/1999/xhtml", "a");
      link.href = url.createObjectURL(blob);
      link.download = name;
      link.dispatchEvent(event);
    }
  },
  /**
   * 销毁在线代码编辑器
   */
  beforeDestroy() {
    if (this.monacoEditor) {
      this.monacoEditor.dispose()
    }
  },
}
</script>
 
<style lang="less" scoped>
  .m-e {
    width: 100%;
    height: 100%;

    .m-e-main {
      width: 100%;
      height: 100%;

      .m-e-main_toolbar {
        width: 100%;
        height: 40px;
        box-shadow: 0px 2px 5px #000;
        display: flex;
        position: relative;
        z-index: 99;
    
        .m-e-main_toolbar_left {
          flex: 1;
          overflow: hidden;
    
          span {
            display: block;
            font-size: 15px;
            padding-left: 10px;
            line-height: 26px;
            line-height: 40px;
            white-space:nowrap;/* 不换行 */
            overflow:hidden;/* 内容超出宽度时隐藏超出部分的内容 */
            text-overflow:ellipsis;/* 当对象内文本溢出时显示省略标记(...) ;需与overflow:hidden;一起使用。*/
            // user-select: none;
          }
        }
    
        .m-e-main_toolbar_right {
          margin-right: 15px;
    
          a {
            width: 16px;
            height: 16px;
            line-height: 16px;
            transition: ease all 0.3s ;
            text-align: center;
            display: inline-block;
            padding: 5px;
            cursor: pointer;
            border-radius: 2px;
            margin: 7px 0 7px 5px;
    
            i {
              font-size: 15px;
            }
    
            &:hover {
              background-color: rgba(255, 255, 255, 0.1);
            }
          }
        }
      }
    }
  }
</style>

最终效果:

以上代码在Vue3.X项目运行是有点问题的,页面会卡死,因为Vue3不再暴露子组件实例的属性和方法,但是却可以用toRaw方法获取到原始数据,也就是可以获取到子组件的实例,自然也就可以获取到实例的属性和方法。

例如获取编辑器内容方法,this.editor改为toRaw(this.editor)即可,还有其他方法也要改一下哦。

// 引入获取原始数据组件
import { toRaw } from 'vue'

/**
 * 获取编辑器的内容
 */
getEditorContent() {
  toRaw(this.editor).getValue();
},

好了,本次分享就到这里。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值