news 2026/2/24 17:07:35

Vue3+Monaco Editor封装及SQL编辑器实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue3+Monaco Editor封装及SQL编辑器实现

原文链接:Vue3+Monaco Editor封装及SQL编辑器实现 < Ping通途说

0. 前言

最近收到需求,老板想要在前端自定义SQL语句然后查询。安全性我强调了几次,仍然拗不过老板,那就干吧...只能在语句检查和权限上注意一下,例如严格的语句检查和创建一个仅有单表查权限的数据库用户执行语句。

项目其他地方都使用了Monaco Editor,所以在这里直接复用之前封装的组件,文章也会贴出封装的代码,有需要可以自取。

若你没有听过这个编辑器,那应该听过VSCode吧,Monaco就是VSCode的核心编辑器组件。

目前编辑器内置全套配件(语法检查、格式化等)的语言有:TypeScript、JavaScript、HTML和JSON,其他语言就要自己去配置。Monaco开放的API:Monaco Editor

1. 基础功能实现

有些教程(官方也这么教)会教直接用import * as monaco from 'monaco-editor';来将整个依赖库导入,这样的后果就是打包出来的Monaco整整占用4MB(左图),而我们按需导入后体积骤降2.5MB(右图)

因此如果你的项目(和服务器带宽)对加载速度有要求的,可以使用以下方式导入Monaco:

import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';

这样做的代价就是除了最基础的编辑框,所有的右键菜单、语言高亮提示、语法提示等所有扩展功能都需要自己手动导入。

直接来看最基础的Monaco Editor组件封装:

<template> <div ref="container" :class="'w-full min-h-[450px]'" :style="{ height: height }"></div> </template> <script setup> import 'monaco-editor/esm/vs/editor/contrib/contextmenu/browser/contextmenu.js'; // 右键显示菜单 import 'monaco-editor/esm/vs/editor/contrib/folding/browser/folding.js'; // 折叠 import 'monaco-editor/esm/vs/editor/contrib/format/browser/formatActions.js'; // 格式化代码 import 'monaco-editor/esm/vs/editor/contrib/suggest/browser/suggestController.js'; // 代码联想提示 import 'monaco-editor/esm/vs/editor/contrib/tokenization/browser/tokenization.js'; // 代码联想提示 import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; import JSONWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'; import "monaco-editor/esm/vs/language/json/monaco.contribution"; // JSON代码高亮&提示 import { nextTick, onMounted, ref, toRaw, watch } from 'vue'; // 容器对象 const container = ref(null) // 编辑器对象 const editor = ref(null) // input 事件 const emit = defineEmits(['update:value']) const props = defineProps({ value: { type: String, default: '', }, language: { type: String, default: 'json', }, theme: { type: String, default: 'vs', }, readOnly: { type: Boolean, default: false, }, height: { type: String, default: '450px', }, wordWrap: { type: String, default: "on", }, }) self.MonacoEnvironment = { getWorker(workId, label) { if (label === 'json') { return new JSONWorker() } return new EditorWorker() }, getWorkerUrl(moduleId, label) { if (label === 'json') { return './json.worker.bundle.js' } return './editor.worker.bundle.js' }, } function updateValue(value) { // 更新值 提供给父组件使用 if (value) { const model = monaco.editor.getModels()[0] model.setValue(value.toString()) } } defineExpose({ updateValue, }) onMounted(() => { // 确保容器已经渲染完成后再创建编辑器 nextTick(() => { editor.value = monaco.editor.create(container.value, { value: props.value, language: props.language, scrollBeyondLastLine: false, theme: props.theme, wordWrap: props.wordWrap, automaticLayout: true, minimap: { enabled: false, }, readOnly: props.readOnly, }) editor.value.onDidChangeModelContent(() => { const value = toRaw(editor.value).getValue() emit('update:value', value) }) }) }) // 监听value prop变化,更新编辑器内容 watch( () => props.value, (newValue) => { if (editor.value) { const model = toRaw(editor.value).getModel() if (model && newValue !== toRaw(editor.value).getValue()) { model.setValue(newValue || '') } } }, ) watch( () => props.readOnly, (newValue) => { toRaw(editor.value).updateOptions({ readOnly: newValue, }) }, ) </script>

封装的组件实现了以下功能:

  • 支持JSON语言模式,可通过language参数指定支持的编程语言
  • JSON语法高亮、代码自动折叠、右键上下文菜单、JSON代码格式化、JSON智能代码提示和自动补全
  • 支持传入配置:
    • value:编辑器内容值,支持双向绑定
    • language:编程语言类型(默认JSON),需要预先配置好对应语言的参数才能使用语言高亮和智能提示
    • theme:编辑器主题,默认vs,暗黑模式就是vs-dark
    • readonly:动态调整是否只读模式
    • height:编辑框容器高度
    • wordWrap:是否自动换行
  • 数据同步:在编辑框输入和内部修改的内容由v-model进行双向绑定

要使用这个组件,先假设当前组件名称为“MonacoEditor.vue”,然后就能在需要使用中的组件导入,然后绑定值并传入参数。

需要注意的是:传入值必须为字符串,传回值也是字符串,这就意味着当前页面需要正常使用JSON就需要使用computed实时计算

<template> <div class="mx-auto w-full h-[450px]"> <MonacoEditor language="json" :value="formatRawText" v-model:value="editText" :theme="theme === 'dark' ? 'vs-dark' : 'vs'" :readOnly="readOnly" /> </div> </template> <script setup> import MonacoEditor from '@/components/editor/MonacoEditor.vue' import { computed, ref, watch } from 'vue' const rawText = ref({}) const editText = ref({}) const readOnly = ref(false) const theme = ref('dark') const formatRawText = computed(() => { try { if (typeof rawText === 'object') { return JSON.stringify(JSON.parse(rawText), null, 2) } return config } catch { return "{}" } }) watch(editText.value,()=>{ if(editText.value && editText.value != '{}'){ try { const parsedText = JSON.parse(editText.value) editText.value = parsedText } catch(e){ console.log("解析发生错误:",e) } } })

另外,如果你仔细观察组件的导入,支持JSON功能的库有:

import JSONWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'; import "monaco-editor/esm/vs/language/json/monaco.contribution"; // JSON代码高亮&提示

文章一开始也提到,官方提供了4种语言的深度支持,如果你需要使用他们,可以模仿JSON引用库的方式来引用其他三项的依赖

2. MySQL语言支持

<template> <div ref="container" :class="'w-full min-h-[450px]'" :style="{ height: height }"></div> </template> <script setup> import 'monaco-editor/esm/vs/basic-languages/mysql/mysql.contribution.js'; import 'monaco-editor/esm/vs/basic-languages/mysql/mysql.js'; import { language as mysqlLanguage } from 'monaco-editor/esm/vs/basic-languages/mysql/mysql.js'; import 'monaco-editor/esm/vs/editor/contrib/contextmenu/browser/contextmenu.js'; // 右键显示菜单 import 'monaco-editor/esm/vs/editor/contrib/folding/browser/folding.js'; // 折叠 import 'monaco-editor/esm/vs/editor/contrib/format/browser/formatActions.js'; // 格式化代码 import 'monaco-editor/esm/vs/editor/contrib/suggest/browser/suggestController.js'; // 代码联想提示 import 'monaco-editor/esm/vs/editor/contrib/tokenization/browser/tokenization.js'; // 代码联想提示 import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; import JSONWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'; import "monaco-editor/esm/vs/language/json/monaco.contribution"; // JSON代码高亮&提示 import { nextTick, onMounted, ref, toRaw, watch } from 'vue'; // 容器对象 const container = ref(null) // 编辑器对象 const editor = ref(null) // input 事件 const emit = defineEmits(['update:value']) const props = defineProps({ value: { type: String, default: '', }, language: { type: String, default: 'json', }, theme: { type: String, default: 'vs', }, readOnly: { type: Boolean, default: false, }, height: { type: String, default: '450px', }, wordWrap: { type: String, default: "on", }, }) self.MonacoEnvironment = { getWorker(workId, label) { if (label === 'json') { return new JSONWorker() } if (label === 'mysql') { return new MySQLWorker() } return new EditorWorker() }, getWorkerUrl(moduleId, label) { if (label === 'json') { return './json.worker.bundle.js' } if (label === 'mysql') { return './mysql.worker.bundle.js' } return './editor.worker.bundle.js' }, } function updateValue(value) { // 更新值 提供给父组件使用 if (value) { const model = monaco.editor.getModels()[0] model.setValue(value.toString()) } } defineExpose({ updateValue, }) onMounted(() => { // 确保容器已经渲染完成后再创建编辑器 nextTick(() => { editor.value = monaco.editor.create(container.value, { value: props.value, language: props.language, scrollBeyondLastLine: false, theme: props.theme, wordWrap: props.wordWrap, automaticLayout: true, minimap: { enabled: false, }, readOnly: props.readOnly, }) monaco.languages.register({ id: 'mysql' }) if (props.language == 'mysql') { //注册片段 const keywords = mysqlLanguage.keywords.map(item => ({ "label": item, "kind": monaco.languages.CompletionItemKind.Keyword, "insertText": item, "detail": "MySQL Keyword" })) monaco.languages.registerCompletionItemProvider('mysql', { // 设置触发自动补全的字符 triggerCharacters: [' ', '.', '(', '`'], provideCompletionItems: (model, position) => { // 获取当前行的所有文本 const word = model.getWordUntilPosition(position); const range = { startLineNumber: position.lineNumber, endLineNumber: position.lineNumber, startColumn: word.startColumn, endColumn: word.endColumn }; // 返回所有关键词和自定义片段 return { suggestions: [{ label: 'queryUser', kind: monaco.languages.CompletionItemKind.Snippet, insertText: 'SELECT `${1:user_id}`,`${2:open_id}`,`${3:age}`,${4:params},`${5:created_at}` FROM `user`', insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, documentation: '查询用户信息', range: range }, ...keywords.map(keyword => ({ ...keyword, range: range }))] } }, }) } editor.value.onDidChangeModelContent(() => { const value = toRaw(editor.value).getValue() emit('update:value', value) }) }) }) // ... exist // 监听value prop变化,更新编辑器内容 watch( () => props.value, (newValue) => { if (editor.value) { const model = toRaw(editor.value).getModel() if (model && newValue !== toRaw(editor.value).getValue()) { model.setValue(newValue || '') } } }, ) watch( () => props.readOnly, (newValue) => { toRaw(editor.value).updateOptions({ readOnly: newValue, }) }, ) </script>

通过代码高亮可以发现,我们导入了mysql的语法高亮支持,然后在111行注册语言。

在113行中遍历官方提供关键字列表并加入到编辑器中,因为直接导入是没有用的,打字没有提示。

第120行我们定义了在什么时候触发关键词提示,目前空格,括号,反引号等这些在MySQL语句一般接的就是关键词了。在这里你也可以将从服务器获取的表名也导入进去。

然后136行,我们自定义了一个片段,用于快捷输入我们常用的SQL片段,设置完成后我们可以通过输入queryUser来快捷使用SQL片段。片段中使用了${1:user_id}这种代表插槽,用户可按Tab键快速切换到下一个插槽中填数据,如果不填就使用插槽内部默认的字段,非常的方便

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/23 15:16:48

MiniCPM-V 4.5

目录 1. 引言 问题背景 解决方案 性能亮点 贡献 2. 方法 2.1 模型架构&#xff08;Architecture&#xff09; 2.2 预训练&#xff08;Pre-training&#xff09; 2.3 监督微调&#xff08;Supervised Fine-Tuning, SFT&#xff09; 2.4 强化学习&#xff08;Reinforce…

作者头像 李华
网站建设 2026/2/24 11:42:10

Flutter工程化与协作实践指南

欢迎大家加入开源鸿蒙跨平台开发者社区&#xff0c;一起共建开源鸿蒙跨平台生态。 Flutter工程化与协作实践指南 工程化核心要素 模块化设计 采用feature-first架构设计&#xff0c;每个功能模块独立封装业务逻辑、数据层和UI组件&#xff0c;通过Dart的export机制统一管理…

作者头像 李华
网站建设 2026/2/23 14:21:24

Excel技巧:提取身份证号码中的出生年月日

制作excel表格中总是少不了要从一组数据中提取部分数据出来&#xff0c;比如在身份证号码中提取出生日期&#xff0c;今天分享方法给大家。 在出生日期单元格内输入公式&#xff1a;MID(B2,7,8) 公式注释&#xff1a;内容提取单元格位置&#xff0c;第几位开始&#xff0c;取…

作者头像 李华
网站建设 2026/2/24 13:52:19

软工毕业设计创新的开题分享

0 选题推荐 - 大数据篇 毕业设计是大家学习生涯的最重要的里程碑&#xff0c;它不仅是对四年所学知识的综合运用&#xff0c;更是展示个人技术能力和创新思维的重要过程。选择一个合适的毕业设计题目至关重要&#xff0c;它应该既能体现你的专业能力&#xff0c;又能满足实际应…

作者头像 李华
网站建设 2026/2/22 17:07:44

Oracle数据库物理备份与恢复实战指南

1. RMAN基础概念1.1 什么是RMANRMAN&#xff08;Recovery Manager&#xff09;是Oracle 8i以后DBA的重要工具&#xff0c;位于$ORACLE_HOME/bin目录下&#xff0c;主要用于备份、还原和恢复操作。imageRMAN组成&#xff1a;可执行文件&#xff1a;rman库文件&#xff1a;recove…

作者头像 李华