使用 Monaco Editor 开发 SQL 编辑器
- 2022 年 8 月 05 日
本文字数:5052 字
阅读完需:约 17 分钟
安装
安装依赖,这里请特别注意下版本
yarn add monaco-editor@0.29.1
yarn add monaco-editor-webpack-plugin@5.0.0
复制代码
配置 webpack 插件
// vue.config.js
...
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin')
module.export = {
...
configureWebpack: {
name: name,
resolve: {
alias: {
'@': resolve('src'),
},
},
plugins: [new MonacoWebpackPlugin()],
},
...
}
复制代码
请注意 monaco-editor-webpack-plugin 和 monaco-editor 的对应关系,否则可能会出现无法运行的情况。
简易 SQL 编辑器
先给大家上干货!
<template>
<div ref="codeContainer" class="editor-container" :style="{ height: height + 'px' }" />
</template>
<script>
import * as monaco from 'monaco-editor'
/**
* VS Code 编辑器
*
* 通过 getEditorVal 函数向外传递编辑器即时内容
* 通过 initValue 用于初始化编辑器内容。
* 编辑器默认 sql 语言,支持的语言请参考 node_modules\monaco-editor\esm\vs\basic-languages 目录下~
* 编辑器样式仅有 'vs', 'vs-dark', 'hc-black' 三种
*/
export default {
name: 'MonacoEditor',
props: {
initValue: {
type: String,
default: '',
},
readOnly: Boolean,
language: {
type: String,
default: 'sql',
},
height: {
type: Number,
default: 300,
},
theme: {
type: String,
default: 'vs',
},
},
data() {
return {
monacoEditor: null, // 语言编辑器
}
},
computed: {
inputVal() {
return this.monacoEditor?.getValue()
},
},
watch: {
inputVal() {
if (this.monacoEditor) {
this.$emit('change', this.monacoEditor.getValue())
}
},
theme() {
this.setTheme(this.theme)
},
height() {
this.layout()
},
},
mounted() {
this.initEditor()
},
beforeDestroy() {
if (this.monacoEditor) {
this.monacoEditor.dispose()
}
},
methods: {
initEditor() {
if (this.$refs.codeContainer) {
this.registerCompletion()
// 初始化编辑器,确保dom已经渲染
this.monacoEditor = monaco.editor.create(this.$refs.codeContainer, {
value: '', // 编辑器初始显示文字
language: 'sql', // 语言
readOnly: this.readOnly, // 是否只读 Defaults to false | true
automaticLayout: true, // 自动布局
theme: this.theme, // 官方自带三种主题vs, hc-black, or vs-dark
minimap: {
// 关闭小地图
enabled: false,
},
tabSize: 2, // tab缩进长度
})
}
this.setInitValue()
},
focus() {
this.monacoEditor.focus()
},
layout() {
this.monacoEditor.layout()
},
getValue() {
return this.monacoEditor.getValue()
},
// 将 initValue Property 同步到编辑器中
setInitValue() {
this.monacoEditor.setValue(this.initValue)
},
setTheme() {
monaco.editor.setTheme(this.theme)
},
getSelectionVal() {
const selection = this.monacoEditor.getSelection() // 获取光标选中的值
const { startLineNumber, endLineNumber, startColumn, endColumn } = selection
const model = this.monacoEditor.getModel()
return model.getValueInRange({
startLineNumber,
startColumn,
endLineNumber,
endColumn,
})
},
setPosition(column, lineNumber) {
this.monacoEditor.setPosition({ column, lineNumber })
},
getPosition() {
return this.monacoEditor.getPosition()
},
},
}
</script>
<style lang="scss" scoped></style>
复制代码
相关功能
获取选中代码
getSelectionVal() {
const selection = this.monacoEditor.getSelection() // 获取光标选中的值
const { startLineNumber, endLineNumber, startColumn, endColumn } = selection
const model = this.monacoEditor.getModel()
return model.getValueInRange({
startLineNumber,
startColumn,
endLineNumber,
endColumn,
})
},
复制代码
替换选中代码
insertStringInTemplate(str) {
const selection = this.monacoEditor.getSelection() // 获取光标选中的值
const { startLineNumber, endLineNumber, startColumn, endColumn } = selection
const model = this.monacoEditor.getModel()
const textBeforeSelection = model.getValueInRange({
startLineNumber: 1,
startColumn: 0,
endLineNumber: startLineNumber,
endColumn: startColumn,
})
const textAfterSelection = model.getValueInRange({
startLineNumber: endLineNumber,
startColumn: endColumn,
endLineNumber: model.getLineCount(),
endColumn: model.getLineMaxColumn(model.getLineCount()),
})
this.monacoEditor.setValue(textBeforeSelection + str + textAfterSelection)
this.monacoEditor.focus()
this.monacoEditor.setPosition({
lineNumber: startLineNumber,
column: startColumn + str.length,
})
},
复制代码
处理光标位置
setPosition(column, lineNumber) {
this.monacoEditor.setPosition({ column, lineNumber })
},
getPosition() {
return this.monacoEditor.getPosition()
},
复制代码
自定义 SQL 库表提示,并保留原有 SQL 提示
首先由后端提供具体的库表信息:
export const hintData = {
adbs: ['dim_realtime_recharge_paycfg_range', 'dim_realtime_recharge_range'],
dimi: ['ads_adid', 'ads_spec_adid_category'],
}
复制代码
然后根据已有库表信息进行自定义 AutoComplete
import * as monaco from 'monaco-editor'
import { language } from 'monaco-editor/esm/vs/basic-languages/sql/sql'
const { keywords } = language
export default {
...
mounted() {
this.initEditor()
},
methods: {
...
registerCompletion() {
const _that = this
monaco.languages.registerCompletionItemProvider('sql', {
triggerCharacters: ['.', ...keywords],
provideCompletionItems: (model, position) => {
let suggestions = []
const { lineNumber, column } = position
const textBeforePointer = model.getValueInRange({
startLineNumber: lineNumber,
startColumn: 0,
endLineNumber: lineNumber,
endColumn: column,
})
const tokens = textBeforePointer.trim().split(/\s+/)
const lastToken = tokens[tokens.length - 1] // 获取最后一段非空字符串
if (lastToken.endsWith('.')) {
const tokenNoDot = lastToken.slice(0, lastToken.length - 1)
if (Object.keys(_that.hintData).includes(tokenNoDot)) {
suggestions = [..._that.getTableSuggest(tokenNoDot)]
}
} else if (lastToken === '.') {
suggestions = []
} else {
suggestions = [..._that.getDBSuggest(), ..._that.getSQLSuggest()]
}
return {
suggestions,
}
},
})
},
// 获取 SQL 语法提示
getSQLSuggest() {
return keywords.map((key) => ({
label: key,
kind: monaco.languages.CompletionItemKind.Enum,
insertText: key,
}))
},
getDBSuggest() {
return Object.keys(this.hintData).map((key) => ({
label: key,
kind: monaco.languages.CompletionItemKind.Constant,
insertText: key,
}))
},
getTableSuggest(dbName) {
const tableNames = this.hintData[dbName]
if (!tableNames) {
return []
}
return tableNames.map((name) => ({
label: name,
kind: monaco.languages.CompletionItemKind.Constant,
insertText: name,
}))
},
initEditor() {
if (this.$refs.codeContainer) {
this.registerCompletion()
// 初始化编辑器,确保dom已经渲染
this.monacoEditor = monaco.editor.create(this.$refs.codeContainer, {
value: '', // 编辑器初始显示文字
language: 'sql', // 语言
readOnly: this.readOnly, // 是否只读 Defaults to false | true
automaticLayout: true, // 自动布局
theme: this.theme, // 官方自带三种主题vs, hc-black, or vs-dark
minimap: {
// 关闭小地图
enabled: false,
},
tabSize: 2, // tab缩进长度
})
}
this.setValue(this.value)
},
}
}
复制代码
编辑器 resize
resize() {
this.monacoEditor.layout()
},
复制代码
编辑器设置主题
注意!设置主题并非在编辑器实例上修改的哦!
setTheme() {
monaco.editor.setTheme(this.theme)
},
复制代码
SQL 代码格式化
编辑器自身不支持 sql 格式化(试了下 JavaScript 是支持的),所以用到了 sql-formatter 这个库。
import { format } from 'sql-formatter'
...
format() {
this.monacoEditor.setValue(
format(this.monacoEditor.getValue(), {
indentStyle: 'tabularLeft',
}),
)
},
...
复制代码
右键菜单汉化
需要安装以下两个库
npm install monaco-editor-nls --save
npm install monaco-editor-esm-webpack-plugin --save-dev
复制代码
具体用法可以直接去 www.npmjs.com/package/mon… 里面看,我就不搬运了~
记得销毁编辑器对象哦
beforeDestroy() {
if (this.monacoEditor) {
this.monacoEditor.dispose()
}
},
复制代码
踩坑
下面是我遇到的几个坑。
最新版本的 Monaco Editor 已经使用了 ES2022 的语法,所以老项目可能会出现编译不过的问题。所以我把版本调低了一些。
在最初调试编辑器的时候出现了无法编辑的情况,后来发现是同事用到了
default-passive-events
这个库来关闭 chrome 的Added non-passive event listener to a scroll-blocking <some> event. Consider marking event handler as 'passive' to make the page more responsive
警告。结果拦截一些 event。
如何快速去看懂 Monaco Editor
一开始我看它的官方文档是非常懵的,各种接口、函数、对象的定义,完全不像是个前端库那么好理解。鼓捣了好久才慢慢找到门路。
先看示例查看它的 playground,上面其实是有一些功能可以直接找到的。查看它在 github 上的 /samples 目录,里面也有不少示例。去掘金这类网站上找别人写的示例,能有不少启发。
再看 API 了解了自己所需要的功能相关的代码,再去看它文档的 API 就会发现容易理解多了。逐步发散理解更多关联功能。
源码附件已经打包好上传到百度云了,大家自行下载即可~
链接: https://pan.baidu.com/s/14G-bpVthImHD4eosZUNSFA?pwd=yu27
提取码: yu27
百度云链接不稳定,随时可能会失效,大家抓紧保存哈。
如果百度云链接失效了的话,请留言告诉我,我看到后会及时更新~
开源地址
码云地址:http://github.crmeb.net/u/defu
Github 地址:http://github.crmeb.net/u/defu
CRMEB
还未添加个人签名 2021.11.02 加入
CRMEB就是客户关系管理+营销电商系统实现公众号端、微信小程序端、H5端、APP、PC端用户账号同步,能够快速积累客户、会员数据分析、智能转化客户、有效提高销售、会员维护、网络营销的一款企业应用
评论