最近有个需求是显示日志模块的信息,用了好多在线代码编辑器,如各式各样的 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();
},
好了,本次分享就到这里。