最近在琢磨前端,因项目中需要在页面上编辑代码,所以需要写一个代码编辑器供用户使用。找了几个编辑器相关的组件,对比了下感觉还是vue2-ace-editor用着舒服,写了demo供大家参考。
由于我的项目使用的是vue2,二开鹅厂的bk-ci(蓝盾),因此基础组件使用的是bk组件。大家可以把选择框、按钮这些替换成自己使用的组件(如element-ui等)
组件已经封装好可以直接到手使用,支持更换主题、切换语言、缩进、搜索和替换(编辑器内Ctrl+F)、复制,json美化等...(后期加了字体大小选择和全屏功能,拉到底部查看代码)
效果图:
首先,到项目中安装插件:
npm install --save-dev vue2-ace-editor
安装成功后,引用组件,可在main.js中全局引用:
import Editor from 'vue2-ace-editor'
Vue.component('editor', Editor)
或直接在代码中局部引用(如我这里直接代码中引用):
<template>
<div>
<bk-card title="代码编辑器">
<div style="display: flex; width: 100%; margin-right: 20px; margin-bottom: 10px;">
<bk-select
:disabled="false"
placeholder="请选择主题"
search-placeholder="搜索主题"
v-model="valueTheme"
style="width: 150px; margin-right: 20px;"
@change="selectTheme"
searchable>
<bk-option v-for="option in listTheme"
:key="option"
:id="option"
:name="option">
</bk-option>
</bk-select>
<bk-select
:disabled="false"
placeholder="请选择语言"
search-placeholder="搜索语言"
v-model="valueCodeLang"
style="width: 150px; margin-right: 20px;"
@change="selectLang"
:font-size="large"
searchable>
<bk-option v-for="option in listCodeLang"
:key="option"
:id="option"
:name="option">
</bk-option>
</bk-select>
<bk-button theme="primary" icon="script-file" class="mr10" @click="copyCode()"> 复制代码</bk-button>
<bk-button v-if="valueCodeLang === 'json'" theme="primary" icon="eye" class="mr10" @click="formatCode()"> 美化代码</bk-button>
</div>
<editor
ref="aceEditor"
v-model="content"
@init="editorInit"
width="100%"
height="400px"
:lang="lang"
:theme="theme"
:options="{
enableBasicAutocompletion: true,
enableSnippets: true,
enableLiveAutocompletion: true,
tabSize: 6, fontSize: 14,
readOnly: readOnly,//设置是否只读
showPrintMargin: false //去除编辑器里的竖线
}"
></editor>
</bk-card>
</div>
</template>
<script>
export default {
components: {
editor: require('vue2-ace-editor')
},
props: {
// 是否只读
readOnly: {
type: Boolean,
default: false
},
// 要展示的代码
codeData: {
type: String,
default: ''
},
// 默认的主题
valueTheme: {
type: String,
default: 'dracula'
},
// 默认的语言
valueCodeLang: {
type: String,
default: 'json'
}
},
data () {
return {
listTheme: [
'dracula',
'chrome',
'chaos',
'clouds',
'clouds_midnight',
'xcode',
'monokai',
'ambiance',
'dreamweaver',
'eclipse',
'github',
'idle_fingers'
],
listCodeLang: [
'json',
'yaml',
'xml',
'java',
'text',
'javascript',
'scheme',
'lua',
'mysql',
'perl',
'powershell',
'python',
'ruby',
'sql',
'hjson',
'ini'
],
content: '',
theme: '',
lang: ''
}
},
mounted () {
// 初始化编辑器
this.editorInit()
// 初始化主题、语言
this.theme = this.valueTheme
this.lang = this.valueCodeLang
// 若传输代码,则展示代码
if (this.codeData) {
console.log(this.codeData)
this.$refs.aceEditor.editor.setValue(this.codeData)
}
},
methods: {
selectTheme (newValue, oldValue) {
if (newValue) {
this.theme = newValue
}
},
selectLang (newValue, oldValue) {
if (newValue) {
this.lang = newValue
}
},
editorInit () { // 初始化
require('brace/ext/language_tools')
require('brace/ext/beautify')
require('brace/ext/error_marker')
require('brace/ext/searchbox')
require('brace/ext/split')
// 循坏加载语言,通过点击按钮切换
for (let s = 0; s < this.listCodeLang.length; s++) {
require('brace/snippets/' + this.listCodeLang[s])
}
for (let j = 0; j < this.listCodeLang.length; j++) {
require('brace/mode/' + this.listCodeLang[j])
}
// 循坏加载主题,通过点击按钮切换
for (let i = 0; i < this.listTheme.length; i++) {
require('brace/theme/' + this.listTheme[i])
}
},
copyCode () {
const code = this.$refs.aceEditor.editor.getValue()
// 复制到剪切板
if (navigator.clipboard) {
navigator.clipboard.writeText(code)
// 复制成功 给提示 此处省略
} else {
// 复制失败 给提示 此处省略
alert('您的浏览器不支持自动复制,请手动复制')
}
},
formatCode () {
const string = JSON.stringify(JSON.parse(this.$refs.aceEditor.editor.getValue()), null, 2)
this.$refs.aceEditor.editor.setValue(string)
}
// getValue () { // 获取编辑器中的值
// console.log('编辑器中第一个换行符的位置:' + this.$refs.aceEditor.editor.getValue().indexOf('\n'))
// }
}
}
</script>
<style>
</style>
开发中遇到关于 'Unexpected token '<'' 的报错的话,要先加载 “brace/snippets/” 再加载 “brace/mode/” 。顺序不对就会报错!
由于这个组件用到其他弹框等页面时,编辑器会显的有点小,不方便看代码修改代码,因此加了一个全屏显示的功能,效果图如下:
注意点:全屏后由于页面z-index会被设为最大值,所以下拉框会被覆盖,点击无法正常显示,若使用的是element,vue2(element-ui)的select组件可以用 popper-append-to-body='false' 解决
vue3(element plus)的select组件可以用 teleported='false' 解决。可以根据是否全屏来更改值。若改不了,全屏时就把这几个下拉框隐藏掉。
遇到一个小bug,假如当前字体大小是20,全屏后字体会自动变为12。全屏时20,退出全屏一样会变为12,至今未找到原因。。。因此做此下策:大小改变时,就直接把字体大小定为12
代码如下:
<template>
<div ref="screenFull">
<bk-card title="代码编辑器" :show-head="false">
<div style="display: flex; width: 100%; margin-right: 20px; margin-bottom: 10px;">
<el-select
v-model="valueTheme"
:value-key="'valueTheme'"
placeholder="选择主题"
style="width: 130px; margin-right: 20px;"
filterable="true"
clearable="true"
@change="selectTheme"
:popper-append-to-body="false"
>
<el-option
v-for="item in listTheme"
:key="item"
:label="item"
:value="item">
</el-option>
</el-select>
<el-select
v-model="valueCodeLang"
:value-key="'valueCodeLang'"
placeholder="选择语言"
style="width: 120px; margin-right: 20px;"
filterable="true"
clearable="true"
@change="selectLang"
:popper-append-to-body="false"
>
<el-option
v-for="item in listCodeLang"
:key="item"
:label="item"
:value="item">
</el-option>
</el-select>
<el-select
v-model="valueFontSize"
:value-key="'valueFontSize'"
placeholder="字体大小"
style="width: 100px; margin-right: 20px;"
filterable="true"
clearable="true"
@change="selectFontSize"
:popper-append-to-body="false"
>
<el-option
v-for="item in listFontSize"
:key="item"
:label="item"
:value="item">
</el-option>
</el-select>
<bk-button theme="primary" icon="script-file" class="mr10" @click="copyCode()"> 复制</bk-button>
<bk-button v-if="valueCodeLang === 'json'" theme="primary" icon="eye" class="mr10" @click="formatCode()"> 美化</bk-button>
<bk-button theme="primary" :icon=" fullScreen ? 'un-full-screen' : 'full-screen' " class="mr10" @click="screen()"> {{ fullScreen ? '退出全屏' : '全屏' }}</bk-button>
</div>
<div style="display: flex; overflow: auto;">
<editor
ref="aceEditor"
v-model="content"
@init="editorInit"
width="100%"
style="min-height:300px;"
:height="editorHight"
:lang="lang"
:theme="theme"
:options="{
enableBasicAutocompletion: true,
enableSnippets: true,
enableLiveAutocompletion: true,
tabSize: 4, fontSize: fontSize,
readOnly: readOnly,//设置是否只读
showPrintMargin: false //去除编辑器里的竖线
}"
></editor>
</div>
</bk-card>
</div>
</template>
<script>
export default {
components: {
editor: require('vue2-ace-editor')
},
props: {
// 编辑框高度
editorHight: {
type: Number,
default: 300
},
// 是否只读
readOnly: {
type: Boolean,
default: false
},
// 要展示的代码
codeData: {
type: String,
default: ''
},
// 默认的主题
valueTheme: {
type: String,
default: 'clouds',
validator: function (value) { // 自定义验证函数
if (this.listTheme.includes(value)) {
return value
} else {
return 'clouds'
}
}
},
// 默认的语言
valueCodeLang: {
type: String,
default: 'json',
validator: function (value) { // 自定义验证函数
if (this.listCodeLang.includes(value)) {
return value
} else {
return 'text'
}
}
},
// 默认的语言
valueFontSize: {
type: Number,
default: 12
}
},
data () {
return {
listTheme: [
'clouds',
'clouds_midnight',
'dracula',
'chrome',
'chaos',
'xcode',
'monokai',
'ambiance',
'dreamweaver',
'eclipse',
'github',
'idle_fingers'
],
listCodeLang: [
'json',
'yaml',
'xml',
'java',
'text',
'javascript',
'scheme',
'lua',
'mysql',
'perl',
'powershell',
'python',
'ruby',
'sql',
'hjson',
'ini'
],
listFontSize: [
10, 12, 14, 16, 20
],
content: '',
theme: '',
lang: '',
fontSize: 12,
tmpFontSize: 12,
fullScreen: false,
unFullEditorHight: 0, // 非全屏状态下的编辑器高度
appendToBody: true
}
},
created () {
// 初始化主题、语言、大小
this.theme = this.valueTheme
this.lang = this.valueCodeLang
this.fontSize = this.tmpFontSize = this.valueFontSize
},
mounted () {
// 初始化编辑器
this.editorInit()
this.unFullEditorHight = this.editorHight // 缓存浏览器高度
// 若传输代码,则展示代码
if (this.codeData) {
console.log(this.codeData)
this.$refs.aceEditor.editor.setValue(this.codeData)
}
// ESC按键事件无法监听,监听窗口变化
window.onresize = () => {
this.fullScreen = this.checkFull()
}
},
methods: {
selectTheme (newValue) {
if (newValue) {
this.theme = newValue
}
},
selectLang (newValue) {
if (newValue) {
this.lang = newValue
}
},
selectFontSize (newValue) {
if (newValue) {
this.fontSize = newValue
this.tmpFontSize = newValue
}
},
editorInit () { // 初始化
require('brace/ext/language_tools')
require('brace/ext/beautify')
require('brace/ext/error_marker')
require('brace/ext/searchbox')
require('brace/ext/split')
// 循坏加载语言,通过点击按钮切换
for (let s = 0; s < this.listCodeLang.length; s++) {
require('brace/snippets/' + this.listCodeLang[s])
}
for (let j = 0; j < this.listCodeLang.length; j++) {
require('brace/mode/' + this.listCodeLang[j])
}
// 循坏加载主题,通过点击按钮切换
for (let i = 0; i < this.listTheme.length; i++) {
require('brace/theme/' + this.listTheme[i])
}
},
copyCode () {
const code = this.$refs.aceEditor.editor.getValue()
let message, theme
// 复制到剪切板
if (navigator.clipboard) {
navigator.clipboard.writeText(code)
message = '复制成功'
theme = 'success'
} else {
message = '您的浏览器不支持自动复制,请手动复制'
theme = 'error'
}
this.$bkMessage({
message,
theme
})
},
formatCode () {
const code = this.$refs.aceEditor.editor.getValue()
console.log(code)
if (code !== '') {
const string = JSON.stringify(JSON.parse(code), null, 2)
this.$refs.aceEditor.editor.setValue(string)
}
},
// getValue () {
// 获取编辑器中的值
// console.log('第一个换行符的位置:' + this.$refs.aceEditor.editor.getValue().indexOf('\n'))
// }
checkFull () { // 用来监听编辑器大小是否变化
// 火狐浏览器
const isFull
= document.mozFullScreen
|| document.fullScreen
// 谷歌浏览器及Webkit内核浏览器
|| document.webkitIsFullScreen
|| document.webkitRequestFullScreen
|| document.mozRequestFullScreen
|| document.msFullscreenEnabled
console.log(isFull)
console.log(!!isFull)
if (!isFull) { // false时 :非全屏
this.editorHight = this.unFullEditorHight
}
return !!isFull
},
screen () {
this.valueFontSize = this.fontSize = 12 // 大小变更后初始化文字大小(不定义也会变为12 组件bug)
const element = this.$refs.screenFull
if (this.fullScreen) {
this.appendToBody = true
this.editorHight = this.unFullEditorHight
// 关闭全屏
if (document.exitFullscreen) {
document.exitFullscreen()
} else if (document.webkitCancelFullScreen) {
document.webkitCancelFullScreen()
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen()
} else if (document.msExitFullscreen) {
document.msExitFullscreen()
}
} else {
this.appendToBody = false
// 全屏
this.editorHight = window.screen.availHeight - 50
if (element.requestFullscreen) {
element.requestFullscreen()
} else if (element.webkitRequestFullScreen) {
element.webkitRequestFullScreen()
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen()
} else if (element.msRequestFullscreen) {
// IE11
element.msRequestFullscreen()
}
}
this.fullScreen = !this.fullScreen
// setTimeout(() => {
// console.log(this.tmpFontSize)
// this.fontSize = this.tmpFontSize
// }, 200)
}
}
}
</script>
<style>
</style>