DBA 组在实现 SQL 审核功能时,有一个场景是期望在 Web 页面实现 SQL 编辑器的效果(用户使用更友好),比如代码高亮、格式化以及关键字自动补全等。经调研,发现monaco-editor
较为符合需求,它是微软开源的一款 vscode 全功能代码编辑器,功能强大、配置灵活,且支持各种自定义的扩展。以下记录一下使用方法。
一:初始化编辑器
用 Vite 快速启动一个 Vue 项目,创建单文件组件
pnpm create vite@latest
// 安装需要的包
pnpm install ant-design-vue@latest --save
pnpm install unplugin-vue-components -D
pnpm install monaco-editor

参考 monaco-editor
官方文档,初始化一个 代码编辑器
<template>
<div class="editor"></div>
</template>
<script setup>
import * as monaco from 'monaco-editor'
import { onMounted} from 'vue'
// 初始化编辑器
const editor = (element) => monaco.editor.create(element, {
// 默认值
value: 'MySQL SQL Editor',
// 指定支持的编程语言
language: 'sql',
theme: 'vs',
fontSize: 18,
folding: true,
readOnly: false
})
onMounted(() => {
const elements = document.getElementsByClassName('editor')
elements.length > 0 && editorCreate(elements[0])
})
</script>
<style scoped>
/* 编辑器样式 */
.editor {
width: auto;
height: 300px;
border: 1px solid #f1eeeeea
}
</style>
配置 vue 路由 访问 web 编辑器

自此我们已有了一个在线 SQL 编辑器。不过功能还比较弱,比如不支持语法补全,自定义关键字、主题设置等。
二:编辑器设置
2.1 关键字补全
参考官方示例:Playground

查看示例代码,关键字方法 registerCompletionItemProvider 需要一个 createDependencyProposals(range) 这样的类型:(所以我们只需要按 createDependencyProposals(range) 的方式提供关键词即可)
return {suggestions: createDependencyProposals(range)}; // 关键点
[
{
label: '"lodash"',
kind: monaco.languages.CompletionItemKind.Function,
documentation: "The Lodash library exported as Node.js modules.",
insertText: '"lodash": "*"',
range: range,
},
{
label: '"express"',
kind: monaco.languages.CompletionItemKind.Function,
documentation: "Fast, unopinionated, minimalist web framework",
insertText: '"express": "*"',
range: range,
}
]
monaco-editor
已经内置了非常丰富的代码关键字库,java,sql,c, json, javascript 等 ,vscode 支持的它均能支持。这里我们直接引入内置的 sql 关键字库,组装数据。
// 关键字补全
monaco.languages.registerCompletionItemProvider("sql", {
provideCompletionItems: function (model, position) {
let suggestions = []
const keywords = language.keywords
// console.log('sql keywords:', language.keywords)
// 确保自动补全能够准确地插入到当前光标位置处用
var word = model.getWordUntilPosition(position)
var range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn,
}
keywords.map(item => {
if(item) {
suggestions.push({
// 显示的文本
label: item,
// 文本类型, 对 类、函数、变量、进行分类
kind: monaco.languages.CompletionItemKind['Keyword'],
// 插入关键字后添加空格
insertText: item + ' ',
// 关键字备注
documentation: 'DBA 的关键字库',
range: range
})
}
})
return {
suggestions: suggestions,
}
},
})
测试 SQL 代码补全功能,可见我们常用的 SQL 语法关键字已能够自动提示。

2.2 主题设置
美观的主题使代码看起来更有个性 ,当然 Monaco Editor 是支持的。以下定义一个名为 myCustomTheme 的主题。
// 自定义主题
monaco.editor.defineTheme('myCustomTheme', {
// 基于 vs-dark 主题
base: 'vs-dark',
inherit: true,
rules: [
{ token: '', background: '#3A006F' }, // 缩略图背景色
//{ token: 'keyword', foreground: '#C586C0' }, // 关键字颜色
//{ token: 'string', foreground: '#6959CD' }, // 字符串颜色
],
colors: {
'editor.background': '#000000', // 编辑器背景色
}
})
const editor = (element) => monaco.editor.create(element, {
// ...
theme: 'myCustomTheme', // 引用自定义主题
// ...
})
暗黑主题测试

2.3 其他设置 (提升用户体验)
列编辑、代码搜索、批量替换、缩略图、代码折叠、鼠标滚轮缩放字体、开启右键菜单、粘贴代码格式化等实用的的功能也给他开启一下。
const editor = (element) => monaco.editor.create(element, {
// 默认值
value: 'MySQL SQL Editor',
language: 'sql',
theme: 'myCustomTheme',
fontSize: 18,
// 允许代码折叠
folding: true,
// 代码引用显示
codeLens: true,
// 自动布局
automaticLayout: true,
// 开启缩略图功能
minimap: { enabled: true },
// 粘贴时自动格式化
formatOnPaste: true,
// 鼠标滚轮缩放
mouseWheelZoom: true,
// 禁用层次提示
disableLayerHinting: true,
// 接受输入建议
acceptSuggestionOnEnter: 'on',
// 开启辅助功能
accessibilitySupport: 'on',
// 开启右键菜单
contextmenu: true,
// 允许列编辑
columnSelection: true,
readOnly: false
})

三:自定义关键字
内置的语法关键字库存在部分缺失(例如 auto_increment、varchar2 等);此外,库名、表名、字段名、索引名等元数据均存储于后端数据库。要实现 DBeaver、Navicat 自动补全的效果,需要自定义关键字。eg:(在 2.1 中已有答案,参考 createDependencyProposals 方法即可)
// 定义两个 Keywords
const customKeywords = (range) => {
return [
{
label: 'auto_increment',
kind: monaco.languages.CompletionItemKind.Keyword,
documentation: "MySQL 自增属性.",
insertText: ' ',
range: range,
},
{
label: 'DHGate DMS()',
kind: monaco.languages.CompletionItemKind.Keyword,
documentation: "欢迎使用 DHGate DMS",
insertText: ' ',
range: range,
}
]
}
// 扩充内置的关键字库
monaco.languages.registerCompletionItemProvider("sql", {
provideCompletionItems: function (model, position) {
// ...
let suggestions = []
// 添加自定义的关键字
const cts = customKeywords(range)
return {
suggestions: [...suggestions, ...cts],
}
},
})
// 以上基于内置词库扩容,当然也可以独立创建一套词库

上面自定义的关键字是静态形式,若要加载 DB 元数据,则需与后端相结合以动态扩充 suggestions 词库。方法同上 ,本文不再赘述。
四:获取编辑器内容
SQL审核需要获取 web 编辑器中的内容发送给后端处理,获取 model 内容用 onDidChangeModelContent(), getValue() 方法。eg:
<template>
<div class="editor"></div>
<!-- 点击按钮获取编辑器内容 -->
<a-button type="primary" @click="getContent">getContent</a-button>
<!-- 响应式 -->
<a-comment>
<span style="color: blue; font-weight: bold;font-size: 22px;">newContent: </span>
{{ editorContent }}
</a-comment>
</template>
<script setup>
// ...
const editorCreate = (element) => {
editor = monaco.editor.create(element, {
// ...
})
// 编辑器内容变化,回调 onDidChangeModelContent
// getValue(): Get value of the current model attached to this editor.
editor.onDidChangeModelContent(() => {
editorContent.value = editor.getValue()
})
}
// getContent 获取编辑器中的内容
const getContent = () => {
// implement it ...
// 请求 API ,发送 SQL 代码
console.log('newContent:', editorContent.value)
}
// ...
</script>
五:新增上下文菜单项
新增一个上下文菜单动作,用于扩展编辑器功能。
<script setup>
// ...
const editorCreate = (element) => {
editor = monaco.editor.create(element, {
// ...
})
// 新增上下文菜单项
editor.addAction({
id: 'dhcoder',
label: 'DHcoder',
keybindings: monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyU,
precondition: null,
keybindingContext: 'editorTextFocus',
contextMenuGroupId: 'DHcoder',
contextMenuOrder: 1.2,
run: function(ed) {
alert(ed.getValue())
return null
}
})
</script>

六:组件封装
封装组件,方便复用 SQL Editor。 example:
<template>
<div class="editor"></div>
<!-- 点击获取编辑器内容 -->
</template>
<script setup>
import * as monaco from 'monaco-editor'
import { onMounted, ref } from 'vue'
import { language } from 'monaco-editor/esm/vs/basic-languages/sql/sql.js'
let editor = null
const editorContent = ref()
const emits = defineEmits(['editorContent'])
// 初始化编辑器
const editorCreate = (element) => {
editor = monaco.editor.create(element, {
// 默认值
value: 'MySQL SQL Editor',
language: 'sql',
theme: 'myCustomTheme',
fontSize: 18,
// 允许代码折叠
folding: true,
// 代码引用显示
codeLens: true,
// 自动布局
automaticLayout: true,
// 开启缩略图功能
minimap: { enabled: true },
// 粘贴时自动格式化
formatOnPaste: true,
// 鼠标滚轮缩放
mouseWheelZoom: true,
// 禁用层次提示
disableLayerHinting: true,
// 接受输入建议
acceptSuggestionOnEnter: 'on',
// 开启辅助功能
accessibilitySupport: 'on',
// 开启右键菜单
contextmenu: true,
// 允许列编辑
columnSelection: true,
readOnly: false
})
// 编辑器内容变化,回调 onDidChangeModelContent
// getValue(): Get value of the current model attached to this editor.
editor.onDidChangeModelContent(() => {
editorContent.value = editor.getValue()
emits('editorContent', editorContent.value)
})
// 新增上下文菜单项
editor.addAction({
id: 'dhcoder',
label: 'DHcoder',
keybindings: monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyU,
precondition: null,
keybindingContext: 'editorTextFocus',
contextMenuGroupId: 'DHcoder',
contextMenuOrder: 1.2,
run: function(ed) {
alert(ed.getValue())
return null
}
})
}
// getContent 获取编辑器中的内容
const getContent = () => {
console.log('newContent:', editorContent.value)
}
// 关键字补全
monaco.languages.registerCompletionItemProvider("sql", {
provideCompletionItems: function (model, position) {
let suggestions = []
const keywords = language.keywords
// console.log('sql keywords:', language.keywords)
// 确保自动补全建议能够准确地插入到当前光标位置处用
var word = model.getWordUntilPosition(position)
var range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn,
}
keywords.map(item => {
if(item) {
suggestions.push({
// 显示的文本
label: item,
// 文本类型, 对 类、函数、变量、进行分类
kind: monaco.languages.CompletionItemKind['Keyword'],
// 插入关键字后添加空格
insertText: item + ' ',
// 关键字备注
documentation: 'DBA 的关键字库',
range: range
})
}
})
const cts = customKeywords(range)
return {
suggestions: [...suggestions, ...cts],
}
},
})
const customKeywords = (range) => {
// 定义两个 Keywords
return [
{
label: 'auto_increment',
kind: monaco.languages.CompletionItemKind.Keyword,
documentation: "MySQL 自增属性.",
insertText: ' ',
range: range,
},
{
label: 'DHGate DMS()',
kind: monaco.languages.CompletionItemKind.Keyword,
documentation: "欢迎使用 DHGate DMS",
insertText: ' ',
range: range,
}
]
}
// 自定义主题
monaco.editor.defineTheme('myCustomTheme', {
// 基于 vs-dark 主题
base: 'vs-dark',
inherit: true,
rules: [
{ token: '', background: '#3A006F' }, // 缩略图背景色
//{ token: 'keyword', foreground: '#C586C0' }, // 关键字颜色
//{ token: 'string', foreground: '#6959CD' }, // 字符串颜色
],
colors: {
'editor.background': '#000000', // 编辑器背景色
}
})
onMounted(() => {
const elements = document.getElementsByClassName('editor')
elements.length > 0 && editorCreate(elements[0])
})
</script>
<style scoped>
/* 编辑器样式 */
.editor {
width: auto;
height: 500px;
border: 1px solid #f1eeeeea
}
</style>
组件使用示例:
<template>
<label for="tags-select">Database: </label>
<a-select
v-model:value="value"
style="width: 20%"
allowClear
placeholder="Tags Mode"
:options="options">
</a-select>
<!-- 编辑器组件 -->
<Editor @editorContent="handleEditorContent" style="margin-top: 10px;"/>
</template>
<script setup>
import { ref, watch } from 'vue';
import Editor from '../components/editor.vue';
const value = ref([])
const options = [...Array(25)].map((_, i) => ({
value: ('172.16.10.10') + (i + 1) + ('/schema1') + (i + 1),
}))
// 编辑器内容处理
const newContent = ref('')
const handleEditorContent = (text) => {
newContent.value = text
// console.log(text)
}
watch(newContent, (newVal, oldVal) => {
// 处理编辑器内容变化
console.log('editorContent changed from', oldVal, 'to', newVal)
})
</script>
<style scoped>
</style>

【实践2 中 我们将介绍如何接入大模型,实现web 在线编辑器自动补全代码的功能】
References
https://cn.vuejs.org/guide/essentials/component-basics.html
https://www.antdv.com/components/overview-cn/
https://vitejs.cn/vite3-cn/guide/using-plugins.html#adding-a-plugin
https://microsoft.github.io/monaco-editor/playground.html?source=v0.52.2#example-creating-the-editor-web-component
https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.ICodeEditor.html#getValue
https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.ICodeEditor.html#onDidChangeModelContent
https://github.com/cookieY/gemini-next/blob/next/src/components/editor/editor.vue
https://juejin.cn/post/7126852961245855775
关于 monaco-editor 的更多功能需求,请参考:https://microsoft.github.io/monaco-editor/docs.html




