如果你正在寻找一个高性能的在线代码编辑器解决方案,Monaco Editor 绝对是一个绕不开的选择。作为 VS Code 背后的编辑器引擎,它提供了代码高亮、智能提示、错误检查等专业功能。而 Vite 和 Vue3 的组合,则能带来极致的开发体验和运行性能。
我在最近的一个项目中就采用了这个技术栈。当时需求是要做一个在线代码练习平台,用户可以在浏览器中直接编写和运行代码。尝试了几种方案后,发现 Monaco Editor 配合 Vite + Vue3 的组合最为理想。Vite 的快速冷启动和热更新特性,让开发过程中的每次修改都能即时反馈;Vue3 的组合式 API 则让编辑器状态的维护变得异常简单。
这个组合的优势主要体现在三个方面:
首先,我们需要创建一个基础的 Vite + Vue3 项目。打开终端,执行以下命令:
bash复制npm create vite@latest my-monaco-project --template vue
cd my-monaco-project
npm install
这个命令会创建一个标准的 Vite + Vue3 项目。我建议使用 pnpm 作为包管理器,它能更好地处理 Monaco Editor 这种有大量依赖的库:
bash复制pnpm create vite my-monaco-project --template vue
cd my-monaco-project
pnpm install
接下来安装 Monaco Editor 的核心包:
bash复制npm install monaco-editor
# 或者使用 pnpm
pnpm add monaco-editor
这里有个小坑需要注意:Monaco Editor 的体积较大,直接引入会导致打包后的文件过大。我们需要通过 Vite 插件来优化它的加载方式。
安装专门为 Vite 优化的 Monaco Editor 插件:
bash复制npm install vite-plugin-monaco-editor
# 或
pnpm add vite-plugin-monaco-editor
然后在 vite.config.js 中进行配置:
javascript复制import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import MonacoEditorPlugin from 'vite-plugin-monaco-editor'
export default defineConfig({
plugins: [
vue(),
MonacoEditorPlugin({
languageWorkers: ['editorWorkerService', 'typescript', 'json', 'html']
})
]
})
这个配置会确保 Monaco Editor 的核心功能能够正常工作,同时保持最佳的性能表现。我在实际项目中发现,如果不指定 languageWorkers,某些语言的高级功能可能无法使用。
现在我们来创建一个可复用的 Monaco Editor 组件。在 src/components 目录下新建 MonacoEditor.vue 文件:
vue复制<template>
<div ref="editorContainer" class="editor-container"></div>
</template>
<script setup>
import * as monaco from 'monaco-editor'
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
const props = defineProps({
modelValue: String,
language: {
type: String,
default: 'javascript'
},
theme: {
type: String,
default: 'vs-dark'
},
options: {
type: Object,
default: () => ({})
}
})
const emit = defineEmits(['update:modelValue'])
const editorContainer = ref(null)
let editor = null
onMounted(() => {
editor = monaco.editor.create(editorContainer.value, {
value: props.modelValue,
language: props.language,
theme: props.theme,
automaticLayout: true,
minimap: {
enabled: true
},
...props.options
})
editor.onDidChangeModelContent(() => {
const value = editor.getValue()
emit('update:modelValue', value)
})
})
onBeforeUnmount(() => {
if (editor) {
editor.dispose()
}
})
watch(() => props.modelValue, (newValue) => {
if (editor && editor.getValue() !== newValue) {
editor.setValue(newValue)
}
})
watch(() => props.language, (newLanguage) => {
if (editor) {
const model = editor.getModel()
monaco.editor.setModelLanguage(model, newLanguage)
}
})
watch(() => props.theme, (newTheme) => {
monaco.editor.setTheme(newTheme)
})
</script>
<style scoped>
.editor-container {
width: 100%;
height: 400px;
border: 1px solid #ddd;
border-radius: 4px;
}
</style>
这个组件实现了几个关键功能:
modelValue 和 update:modelValue)现在可以在任何父组件中使用这个编辑器了:
vue复制<template>
<div class="container">
<h2>Monaco Editor 示例</h2>
<div class="controls">
<select v-model="language">
<option value="javascript">JavaScript</option>
<option value="typescript">TypeScript</option>
<option value="html">HTML</option>
<option value="css">CSS</option>
</select>
<select v-model="theme">
<option value="vs">Light</option>
<option value="vs-dark">Dark</option>
<option value="hc-black">High Contrast</option>
</select>
</div>
<MonacoEditor
v-model="code"
:language="language"
:theme="theme"
:options="editorOptions"
/>
<div class="output">
<h3>代码内容:</h3>
<pre>{{ code }}</pre>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import MonacoEditor from '@/components/MonacoEditor.vue'
const code = ref('// 在这里输入你的代码\nconsole.log("Hello, Monaco Editor!");')
const language = ref('javascript')
const theme = ref('vs-dark')
const editorOptions = ref({
minimap: { enabled: true },
lineNumbers: 'on',
roundedSelection: false,
scrollBeyondLastLine: false,
fontSize: 14,
wordWrap: 'on'
})
</script>
<style scoped>
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.controls {
margin: 10px 0;
}
select {
margin-right: 10px;
padding: 5px;
}
.output {
margin-top: 20px;
padding: 10px;
background: #f5f5f5;
border-radius: 4px;
}
</style>
这个示例展示了如何通过下拉菜单动态切换编辑器的语言和主题,同时实时显示编辑器中的代码内容。
Monaco Editor 默认支持多种语言,但有时我们需要为特定领域语言(DSL)添加支持。下面以添加 SQL 语法高亮为例:
首先创建一个语言定义文件 src/monaco/sqlLang.js:
javascript复制export const SQL_LANG_ID = 'custom-sql'
monaco.languages.register({ id: SQL_LANG_ID })
monaco.languages.setMonarchTokensProvider(SQL_LANG_ID, {
defaultToken: '',
tokenPostfix: '.sql',
keywords: [
'SELECT', 'FROM', 'WHERE', 'AND', 'OR', 'NOT', 'NULL', 'LIKE', 'IN', 'IS',
'INSERT', 'INTO', 'VALUES', 'UPDATE', 'DELETE', 'JOIN', 'LEFT', 'RIGHT',
'INNER', 'OUTER', 'GROUP BY', 'HAVING', 'ORDER BY', 'ASC', 'DESC', 'LIMIT'
],
operators: [
'=', '>', '<', '!', '~', '?', ':', '==', '<=', '>=', '!=',
'&&', '||', '++', '--', '+', '-', '*', '/', '&', '|', '^', '%',
'<<', '>>', '>>>', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^='
],
symbols: /[=><!~?:&|+\-*\/\^%]+/,
tokenizer: {
root: [
[/[a-zA-Z_$][\w$]*/, {
cases: {
'@keywords': 'keyword',
'@default': 'identifier'
}
}],
{ include: '@whitespace' },
[/@symbols/, { cases: { '@operators': 'operator', '@default': '' } }],
[/\d*\.\d+([eE][\-+]?\d+)?/, 'number.float'],
[/0[xX][0-9a-fA-F]+/, 'number.hex'],
[/\d+/, 'number'],
[/[;,.]/, 'delimiter'],
[/'([^'\\]|\\.)*$/, 'string.invalid'],
[/'/, 'string', '@string'],
[/"/, 'string', '@dblstring']
],
whitespace: [
[/[ \t\r\n]+/, 'white'],
[/--.*$/, 'comment']
],
string: [
[/[^\\']+/, 'string'],
[/\\./, 'string.escape'],
[/'/, { token: 'string.quote', bracket: '@close', next: '@pop' }]
],
dblstring: [
[/[^\\"]+/, 'string'],
[/\\./, 'string.escape'],
[/"/, { token: 'string.quote', bracket: '@close', next: '@pop' }]
]
}
})
然后在主入口文件(如 main.js)中导入这个定义:
javascript复制import { SQL_LANG_ID } from '@/monaco/sqlLang'
现在你就可以在编辑器中使用 language="custom-sql" 来获得 SQL 语法高亮支持了。
Monaco Editor 支持自定义主题。下面是一个暗色主题的配置示例:
javascript复制monaco.editor.defineTheme('my-dark-theme', {
base: 'vs-dark',
inherit: true,
rules: [
{ token: 'keyword', foreground: '569CD6' },
{ token: 'string', foreground: 'CE9178' },
{ token: 'number', foreground: 'B5CEA8' },
{ token: 'comment', foreground: '6A9955', fontStyle: 'italic' }
],
colors: {
'editor.background': '#1E1E1E',
'editor.lineHighlightBackground': '#282828',
'editorLineNumber.foreground': '#858585'
}
})
定义好主题后,可以通过 editor.updateOptions({ theme: 'my-dark-theme' }) 来应用它。
Monaco Editor 的强大之处在于它的智能提示功能。下面是为 JavaScript 添加自定义补全的示例:
javascript复制monaco.languages.registerCompletionItemProvider('javascript', {
provideCompletionItems: (model, position) => {
const suggestions = [
{
label: 'console.log',
kind: monaco.languages.CompletionItemKind.Function,
documentation: '输出信息到控制台',
insertText: 'console.log(${1:"message"})',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
},
{
label: 'setTimeout',
kind: monaco.languages.CompletionItemKind.Function,
documentation: '在指定的延迟后执行函数',
insertText: 'setTimeout(() => {\n\t${1}\n}, ${2:delay})',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
}
]
return { suggestions }
}
})
Monaco Editor 支持多种语言,但全量加载会导致资源过大。我们可以按需加载语言支持:
javascript复制// 动态加载语言支持
async function loadLanguage(lang) {
switch (lang) {
case 'typescript':
await import('monaco-editor/esm/vs/language/typescript/monaco.contribution')
break
case 'css':
await import('monaco-editor/esm/vs/language/css/monaco.contribution')
break
case 'html':
await import('monaco-editor/esm/vs/language/html/monaco.contribution')
break
case 'json':
await import('monaco-editor/esm/vs/language/json/monaco.contribution')
break
default:
// JavaScript 是默认加载的
break
}
}
Monaco Editor 的语法分析和智能提示功能运行在 Web Worker 中。确保正确配置 Worker 路径:
javascript复制// 在 public 目录下创建 workers 目录,并复制必要的 worker 文件
self.MonacoEnvironment = {
getWorkerUrl: function (moduleId, label) {
if (label === 'json') {
return './workers/json.worker.js'
}
if (label === 'css' || label === 'scss' || label === 'less') {
return './workers/css.worker.js'
}
if (label === 'html' || label === 'handlebars' || label === 'razor') {
return './workers/html.worker.js'
}
if (label === 'typescript' || label === 'javascript') {
return './workers/ts.worker.js'
}
return './workers/editor.worker.js'
}
}
在单页应用中,不当的编辑器实例管理会导致内存泄漏。确保在组件销毁时正确清理:
javascript复制onBeforeUnmount(() => {
if (editor) {
editor.dispose()
const model = editor.getModel()
if (model) {
model.dispose()
}
}
})
在最近的一个在线教育平台项目中,我们深度集成了 Monaco Editor 来实现代码练习功能。过程中遇到了几个值得分享的问题和解决方案:
问题1:编辑器初始化闪烁
解决方案:在编辑器容器上设置 visibility: hidden,等编辑器完全初始化后再显示:
javascript复制onMounted(async () => {
editorContainer.value.style.visibility = 'hidden'
await nextTick()
editor = monaco.editor.create(editorContainer.value, {
// 配置项
})
editor.onDidLayoutChange(() => {
editorContainer.value.style.visibility = 'visible'
})
})
问题2:多标签编辑器状态保持
实现方案:为每个编辑器标签创建独立的模型并管理状态:
javascript复制const editorModels = new Map()
function createEditorModel(content, language) {
const model = monaco.editor.createModel(content, language)
editorModels.set(model.id, model)
return model
}
function switchToModel(editor, modelId) {
const model = editorModels.get(modelId)
if (model) {
editor.setModel(model)
}
}
问题3:大型文件性能问题
优化方案:对于超过一定行数的文件,禁用部分高开销功能:
javascript复制function configureForLargeFile(editor) {
const lineCount = editor.getModel().getLineCount()
if (lineCount > 1000) {
editor.updateOptions({
minimap: { enabled: false },
codeLens: false,
renderWhitespace: 'none',
scrollBeyondLastLine: false
})
}
}