• 使用 Monaco Editor 开发 SQL 编辑器


    安装

    安装依赖,这里请注意版本

    yarn add monaco-editor@0.29.1
    yarn add monaco-editor-webpack-plugin@5.0.0
    
    • 1
    • 2

    配置 webpack 插件

    // vue.config.js
    ...
    const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin')
    
    module.export = {
      ...
      configureWebpack: {
        name: name,
        resolve: {
          alias: {
            '@': resolve('src'),
          },
        },
        plugins: [new MonacoWebpackPlugin()],
      },
      ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    请注意 monaco-editor-webpack-plugin 和 monaco-editor 的对应关系,否则可能会出现无法运行的情况。

    monaco-editor-webpack-pluginmonaco-editor
    7.*.*>= 0.31.0
    6.*.*0.30.*
    5.*.*0.29.*
    4.*.*0.25.*, 0.26.*, 0.27.*, 0.28.*
    3.*.*0.22.*, 0.23.*, 0.24.*
    2.*.*0.21.*
    1.9.*0.20.*
    1.8.*0.19.*
    1.7.*0.18.*

    简易 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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126

    相关功能

    获取选中代码

        getSelectionVal() {
          const selection = this.monacoEditor.getSelection() // 获取光标选中的值
          const { startLineNumber, endLineNumber, startColumn, endColumn } = selection
          const model = this.monacoEditor.getModel()
    
          return model.getValueInRange({
            startLineNumber,
            startColumn,
            endLineNumber,
            endColumn,
          })
        },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    替换选中代码

    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,
          })
        },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    处理光标位置

      setPosition(column, lineNumber) {
          this.monacoEditor.setPosition({ column, lineNumber })
        },
        getPosition() {
          return this.monacoEditor.getPosition()
        },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    自定义 SQL 库表提示,并保留原有 SQL 提示

    首先由后端提供具体的库表信息:

    export const hintData = {
      adbs: ['dim_realtime_recharge_paycfg_range', 'dim_realtime_recharge_range'],
      dimi: ['ads_adid', 'ads_spec_adid_category'],
    }
    
    • 1
    • 2
    • 3
    • 4

    然后根据已有库表信息进行自定义 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)
        },
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95

    编辑器 resize

        resize() {
          this.monacoEditor.layout()
        },
    
    • 1
    • 2
    • 3

    编辑器设置主题

    注意!设置主题并非在编辑器实例上修改的哦!

        setTheme() {
          monaco.editor.setTheme(this.theme)
        },
    
    • 1
    • 2
    • 3

    SQL 代码格式化

    编辑器自身不支持 sql 格式化(试了下 JavaScript 是支持的),所以用到了 sql-formatter 这个库。

    import { format } from 'sql-formatter'
    
    ...
        format() {
          this.monacoEditor.setValue(
            format(this.monacoEditor.getValue(), {
              indentStyle: 'tabularLeft',
            }),
          )
        },
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    右键菜单汉化

    需要安装以下两个库

    npm install monaco-editor-nls --save
    npm install monaco-editor-esm-webpack-plugin --save-dev
    
    • 1
    • 2

    具体用法可以直接去 https://www.npmjs.com/package/monaco-editor-esm-webpack-plugin 里面看,我就不搬运了~

    记得销毁编辑器对象哦

      beforeDestroy() {
        if (this.monacoEditor) {
          this.monacoEditor.dispose()
        }
      },
    
    • 1
    • 2
    • 3
    • 4
    • 5

    踩坑

    下面是我遇到的几个坑。

    • 最新版本的 Monaco Editor 已经使用了 ES2022 的语法,所以老项目可能会出现编译不过的问题。所以我把版本调低了一些。
    • 在最初调试编辑器的时候出现了无法编辑的情况,后来发现是同事用到了 default-passive-events 这个库来关闭 chrome 的 Added non-passive event listener to a scroll-blocking event. Consider marking event handler as 'passive' to make the page more responsive 警告。结果拦截一些 event。

    如何快速去看懂 Monaco Editor

    一开始我看它的官方文档是非常懵的,各种接口、函数、对象的定义,完全不像是个前端库那么好理解。鼓捣了好久才慢慢找到门路。

    • 先看示例
      • 查看它的 playground,上面其实是有一些功能可以直接找到的。
      • 查看它在 github 上的 /samples 目录,里面也有不少示例。
      • 去掘金这类网站上找别人写的示例,能有不少启发。
    • 再看 API
      • 了解了自己所需要的功能相关的代码,再去看它文档的 API 就会发现容易理解多了。逐步发散理解更多关联功能。

    参考资料

    • 官方文档
      • https://microsoft.github.io/monaco-editor/index.html
    • 相关库
      • Monaco Editor https://www.npmjs.com/package/monaco-editor
      • 右键菜单汉化 https://www.npmjs.com/package/monaco-editor-nls
      • webpack 插件 https://www.npmjs.com/package/monaco-editor-webpack-plugin
      • 汉化 webpack 插件 https://www.npmjs.com/package/monaco-editor-esm-webpack-plugin
      • SQL 代码格式化 https://www.npmjs.com/package/sql-formatter
    • 博客
      • https://blog.csdn.net/m0_37986789/article/details/121135519
      • https://juejin.cn/post/6986907628937379871
  • 相关阅读:
    菜谱小程序源码免费分享【推荐】
    八、class 与 style 绑定(2)
    Redis学习笔记15:基于spring data redis及lua脚本发送到redis服务器多久过期
    前端性能优化方法与实战11 工具实践:如何进行性能专项测试
    python开发面试-20240715
    sqlserver查询多个分类的最新时间数据
    【Java 进阶篇】深入了解 JavaScript 的 innerHTML 属性
    模糊测试面面观 | 模糊测试是如何发现异常情况的?
    Golang string 常用方法
    动物静态HTML网页作业作品 大学生野生动物保护网页设计制作成品 简单DIV CSS布局网站
  • 原文地址:https://blog.csdn.net/violetjack0808/article/details/138083552