概述
这是一个基于Monaco Editor的Vue 2组件封装,Monaco Editor是Visual Studio Code使用的代码编辑器,具有强大的代码高亮、智能提示、语法检查等功能。本组件对其进行了封装,添加了许多实用的功能特性,使其更适合在Vue项目中使用。
封装思路
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>
功能亮点:
- 支持参数提示显示
- 语法高亮的函数签名
- 自动调整编辑区域高度
4. 占位符
computed: {
formattedPlaceholder() {
if (!this.placeholder) return '';
return this.placeholder
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/\n/g, '<br/>');
}
}
特性:
- 支持HTML转义,防止XSS
- 支持多行占位符显示
- 仅在编辑器为空且非只读时显示
使用示例
<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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.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>
总结
-
丰富的功能扩展 - 在保持Monaco Editor核心能力基础上,增加了实用的业务功能
-
良好的可配置性 - 通过props提供灵活的配置选项
-
优秀的用户体验 - 全屏模式、智能占位符等提升了编辑体验
源代码在此,如有需要,自取。