@emtt/vue-monaco-ide
@emtt/vue-monaco-ide
English | 简体中文
代码仓库:github.com/emtiantian/vue-monaco-editor · 演示:GitHub Pages
一个基于 Monaco Editor 与 monaco-pyright-lsp 的 Vue 3 代码编辑器工作台组件:文件树 + 多页签 + 编辑器一体,集成 Pyright Python LSP,开箱即用。
运行时只依赖 monaco-editor 与 monaco-pyright-lsp(及 vscode-languageserver 协议库)——不依赖任何 UI 组件库、CSS 框架。
目录
特性一览
编辑器内核(Monaco)
- ✅ TypeScript / JavaScript:内置 TS 语言服务(IntelliSense、跨文件 goToDefinition、findAllReferences、重命名),预配置了利于跨文件解析的 compilerOptions(
checkJs、全量 model 同步等) - ✅ Python:内置 Pyright LSP(Web Worker 版),提供
- 补全(含 resolve 后的详细文档)、悬停文档、签名帮助
- 跳转定义(支持跨文件,Cmd/Ctrl+点击)
- 重命名(跨文件 WorkspaceEdit)
- 实时类型检查与诊断(红色波浪线),带 typeshed 内置类型
- ✅ JSON:内置 JSON 语言服务(格式化、schema 校验)
- ✅ YAML:默认启用完整语言服务(schema 校验、补全、悬停),首次打开 YAML 文件时按需加载
- ✅ Markdown:语法高亮 + 双通道预览——
#preview插槽接入任意渲染器,或开启内置零依赖轻量渲染器(见 Markdown 预览) - ✅ 支持 Monaco 的多种语言高亮;生产构建可能因静态依赖包含部分语言注册代码
- ✅ F12 / Cmd+点击 跳转定义、Cmd+S 保存、Cmd+Shift+S 全部保存
- ✅ 内置
web-code-editor-light/web-code-editor-dark主题(对 Python 语义 token 做了配色),也支持vs/vs-dark/hc-black
工作台(自带 UI,零 UI 框架依赖)
- ✅ 文件树:新建文件/文件夹(行内输入)、重命名、删除(确认弹窗)、复制路径、下载文件、右键菜单、文件名过滤搜索
- ✅ 彩色文件图标:内置 50+ 高频扩展名的 vscode-icons 风格 SVG 图标(零依赖内联),冷门类型回退文字徽章;目录折叠/展开双态
- ✅ 非文本文件体系:图片/PDF 在线预览(
remoteUrl驱动,可关)、二进制文件下载占位、文件类型自动判定(显式标记 > 扩展名 > 内容嗅探) - ✅ serverHooks 服务端钩子:新建/重命名/删除/移动操作先经宿主确认再落本地树(确认后提交本地变更),天然对接后端文件系统 API
- ✅ 拖拽整理:拖拽移动(目录内/跨目录)、同级排序(上沿 before / 下沿 after)、拖入目录(inside),目录不可拖入自身或其子目录,路径深度上限 10 层
- ✅ 多页签:脏状态标记、自动滚动到活动页签、纵向滚轮横向滚动、一键关闭
- ✅ 草稿状态栏:「已保存 HH:MM / 未保存」实时状态、发布按钮(文字可配)
- ✅ 受控文件列表(
filesprop),支持外部重置与异步到达的默认展开/打开路径
工程质量
- ✅ 增量内容同步(编辑器 -> store 用 range 增量,避免每次全量
getValue()) - ✅ JS/TS/Python model 预创建与生命周期管理,支撑跨文件解析
- ✅ Pyright worker 引用计数:多个编辑器实例共享一个 worker,全部卸载才 terminate
- ✅ Python LSP 按需初始化:项目无 Python 文件时完全不加载
- ✅ TS worker 预热:打开 TS/JS 文件后台强制初始化,缩短首屏 IntelliSense 等待
- ✅ 采用 Monaco 官方 ESM 构建;最终分包和体积取决于宿主构建配置
本地体验
cd vue-monaco-editor
pnpm install
pnpm dev # 打开 playground(默认 http://localhost:5180)
安装
pnpm add @emtt/vue-monaco-ide monaco-editor monaco-pyright-lsp
# 或
npm install @emtt/vue-monaco-ide monaco-editor monaco-pyright-lsp
要求:Vue ^3.5,monaco-editor ~0.52.2,monaco-pyright-lsp 0.1.x。
monaco-yaml 已作为包的运行依赖安装,无需调用方单独添加。
快速开始
1. 注册插件(推荐)
// main.ts
import { createApp } from 'vue'
import VueMonacoEditor from '@emtt/vue-monaco-ide'
// 全局样式(CSS 变量 + 反馈弹窗样式,必须引入)
import '@emtt/vue-monaco-ide/style.css'
import App from './App.vue'
createApp(App)
.use(VueMonacoEditor) // worker 默认走 CDN,开箱即用
.mount('#app')
<!-- App.vue -->
<script setup lang="ts">
import type { FileInput } from '@emtt/vue-monaco-ide'
const files: FileInput[] = [
{ path: '/src/utils.py', name: 'utils.py', language: 'python', content: 'def add(a: int, b: int) -> int:\n return a + b\n' },
{ path: '/src/main.py', name: 'main.py', language: 'python', content: 'from utils import add\nprint(add(1, 2))\n' },
{ path: '/src/index.ts', name: 'index.ts', language: 'typescript', content: 'const x: number = 1\n' },
]
</script>
<template>
<VueMonacoEditor
:files="files"
default-open-path="/src/main.py"
:default-expanded-paths="['/src']"
theme="web-code-editor-light"
style="height: 600px"
@save="({ path, content }) => console.log('save', path)"
@change="({ path, isDirty }) => console.log('change', path, isDirty)"
/>
</template>
2. 或直接使用组件
<script setup lang="ts">
import { VueMonacoEditor } from '@emtt/vue-monaco-ide'
import '@emtt/vue-monaco-ide/style.css'
</script>
功能章节
Markdown 预览
Markdown 文件支持「预览 / 编辑」双模式切换,两条通道按优先级生效:
通道一:#preview 插槽(优先,适合已有渲染器的项目)
<VueMonacoEditor :files="files">
<template #preview="{ content }">
<!-- 使用你自己的渲染器,例如 md-editor-v3 / markdown-it / marked -->
<MdPreview :model-value="content" />
</template>
</VueMonacoEditor>
插槽参数:file(当前文件节点)、content(当前内容字符串)。
通道二:内置轻量渲染器(零依赖,开箱即用)
<VueMonacoEditor :files="files" builtin-markdown-preview />
内置渲染器支持 ATX 标题、围栏代码块、段落、嵌套列表、引用块(可嵌套)、水平线、行内 code span/粗体/斜体/删除/链接/图片、GFM 表格。安全模型为白名单重建而非过滤:全部文本先转义再解析,标签/属性/URL 协议三方白名单(链接仅 http(s)/mailto/#,图片额外允许 data:image/*;base64),不满足的标记一律退化为纯文本。不支持原始 HTML 透传、缩进式代码块、任务列表与脚注。
非文本文件(图片 / PDF / 二进制)
文件类型由 resolveFileKind 判定(显式 fileKind 标记 > 扩展名 > 内容嗅探),编辑区按类别分流:
- image 且带
remoteUrl→<img>在线预览;pdf 且带remoteUrl→<iframe>内嵌预览(mediaPreview: false可整体关闭,退化为下载占位) - binary 或缺
remoteUrl→ 下载占位卡片,点击触发download事件,由页面层决定下载方式(后端接口 /remoteUrl/ base64 解码)
const files: FileInput[] = [
{ path: '/assets/logo.png', name: 'logo.png', remoteUrl: 'https://.../logo.png' },
{ path: '/docs/spec.pdf', name: 'spec.pdf', fileKind: 'pdf', remoteUrl: 'https://.../spec.pdf' },
{ path: '/pkg/lib.zip', name: 'lib.zip' }, // 二进制:无需 content
]
savedContent 用于「云端内容 ≠ 本地草稿」场景:脏状态以 savedContent ?? content 为基线。
serverHooks 服务端钩子
文件树的全部写操作(新建/重命名/删除/移动)支持先经服务端确认再落本地——钩子通过才应用本地变更,返回 false 或抛错则放弃(确认后提交本地变更):
<script setup lang="ts">
import type { WebCodeEditorServerHooks } from '@emtt/vue-monaco-ide'
// 对接任意后端文件系统 API;payload 定义见 WebCodeEditorServerHooks 类型
const serverHooks: WebCodeEditorServerHooks = {
async createFile({ parentPath, name }) {
return await api.createFile(parentPath, name)
},
async createDirectory({ parentPath, name }) {
return await api.createDir(parentPath, name)
},
async rename({ path, newName, isDirectory }) {
return await api.rename(path, newName)
},
async remove({ path, isDirectory }) {
return await api.remove(path)
},
async move({ sourcePath, targetFolderPath, isDirectory }) {
return await api.move(sourcePath, targetFolderPath)
},
}
</script>
<template>
<VueMonacoEditor :files="files" :server-hooks="serverHooks" />
</template>
配套的防抖保护:同一操作在途时重复触发(快速双击新建、连续拖拽)会被静默忽略;目录层级上限 10 层(含拖拽整体迁移时的子孙深度校验)。未提供 serverHooks 时所有操作纯本地生效,行为与 0.1.x 一致。
yaml 语言服务
YAML 完整语言服务默认注册,首次打开 .yml / .yaml 文件时加载。需要 schema 校验时配置内联 schema:
// main.ts —— 主入口已注册 YAML,只需按需配置 schema
import { configureWorkers } from '@emtt/vue-monaco-ide'
configureWorkers({
yamlSchemas: [
{
fileMatch: ['*.yml', '*.yaml'],
uri: 'https://json.schemastore.org/prettierrc.json',
// 内网环境可将 uri 指向本地 schema 文件;enableSchemaRequest 固定关闭,
// schema 内容需内联传入 schema 字段
schema: {},
},
],
})
worker 加载同样遵循下方三档策略:默认从 jsDelivr 的 /+esm 端点加载 YAML worker;可用 workerUrls.yaml 指定本地产物;workerUrls.yaml: false 禁用专属 worker(回退 editor worker,仅失去语言服务,高亮不受影响)。YAML 服务仅在打开 YAML 文件时初始化。
Worker 加载策略
Monaco 与 Pyright 的语言服务都运行在 Web Worker 中,worker 脚本的加载方式由 configureWorkers 控制,分三档:
1. 默认:CDN(零配置)
默认语言集合包括 JavaScript、TypeScript、JSON、Markdown、Python、CSS 和 HTML;其他语言会在打开对应文件时动态加载。
不做任何配置时,worker 从 jsDelivr CDN 加载,版本自动 pin 到你安装的 monaco-editor / monaco-pyright-lsp 版本(构建时从各自 package.json 读取并写入产物)。跨域 worker 已内部处理(blob URL 中转 + 模块 import)。
注意:默认方案需要用户能访问
cdn.jsdelivr.net(内网环境请用方案 2/3)。
2. 指定 worker URL
把从 npm 包复制的 worker 文件放到你的静态目录或私有 CDN:
import { configureWorkers } from '@emtt/vue-monaco-ide'
configureWorkers({
workerUrls: {
editor: '/workers/editor.worker.js',
typescript: '/workers/ts.worker.js',
json: '/workers/json.worker.js',
// css: ..., html: ...
},
pyrightWorkerUrl: '/workers/pyright/worker.js', // 来自 monaco-pyright-lsp/dist/worker.js
})
3. 完全接管(推荐生产环境使用)
在 Vite 项目中用官方 ?worker 导入获得最佳的本地打包体验:
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
app.use(VueMonacoEditor, {
workers: {
getWorker(label) {
if (label === 'typescript' || label === 'javascript') return new tsWorker()
if (label === 'json') return new jsonWorker()
return new editorWorker()
},
},
})
Pyright worker(monaco-pyright-lsp/dist/worker.js)可通过 CDN 或独立静态资源加载,不建议打进主 bundle;通过 pyrightWorkerUrl 指定。
组件 API
Props(<VueMonacoEditor>)
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
files |
FileInput[] |
[] |
受控文件列表(扁平 path 数组,自动构建文件树) |
defaultExpandedPaths |
string[] |
[] |
初始展开的目录路径(异步到达同样生效,只展开不收起) |
defaultOpenPath |
string | null |
null |
初始打开并激活的文件路径(异步到达同样生效) |
theme |
string |
'vs' |
web-code-editor-light / web-code-editor-dark / vs / vs-dark / hc-black |
loading |
boolean |
false |
强制显示加载遮罩(内部初始化完成后自动隐藏) |
saveTime |
string | null |
null |
初始「已保存」时间展示(如 '16:32') |
publishText |
string |
'发布' |
页签栏右侧按钮文字 |
serverHooks |
WebCodeEditorServerHooks |
- | 文件树写操作的服务端确认钩子(见 serverHooks) |
mediaPreview |
boolean |
true |
image/pdf 且带 remoteUrl 时在线预览;关闭后退化为下载占位 |
builtinMarkdownPreview |
boolean |
false |
无 #preview 插槽时启用内置 Markdown 渲染器(见 Markdown 预览) |
FileInput:
interface FileInput {
path: string
name: string
/** 文本内容;目录与二进制条目可省略 */
content?: string
/** 语言 id;省略时按扩展名推断 */
language?: string
isDirectory?: boolean
/** 显式指定文件类别(默认按扩展名/内容自动判定) */
fileKind?: 'text' | 'image' | 'pdf' | 'binary'
/** 图片/PDF 在线预览地址(data:/http URL) */
remoteUrl?: string
/** 脏状态基线:云端内容 ≠ 本地草稿时传入(脏标记以它为准) */
savedContent?: string
}
Emits
| 事件 | 参数 | 触发时机 |
|---|---|---|
save |
{ path, content, name } |
Cmd/Ctrl+S 或程序化保存当前文件 |
save-all |
{ path, content, name }[] |
Cmd/Ctrl+Shift+S,仅含未保存文件 |
change |
{ path, content, name, isDirty } |
活动文件内容变化 |
open-file |
path: string |
打开文件(含跳转定义跨文件切换) |
close-file |
path: string |
关闭页签 |
publish |
- | 点击发布按钮;组件只发出事件,实际提交/部署由调用方实现 |
refresh |
- | 点击文件树刷新按钮 |
ready |
{ elapsedMs?: number } |
编辑器初始化完成、可交互 |
worker-error |
{ type: 'python' | 'typescript', error } |
worker 预加载失败(非致命) |
operation-error |
{ operation, path?, targetPath?, message?, code?, cause? } |
服务端钩子拒绝或失败,本地状态不会改变 |
download |
{ path, name, remoteUrl? } |
非文本文件点击下载按钮(由页面层决定下载方式) |
Slots
| 插槽 | 作用域 | 说明 |
|---|---|---|
preview |
{ file: FileNode, content: string } |
Markdown 预览渲染器,优先于内置渲染器;两者任一可用时显示预览/编辑切换 |
暴露方法(ref 调用)
editorRef.value.save() // 保存当前文件
editorRef.value.saveAll() // 保存全部
editorRef.value.openFile(path) // 打开文件
editorRef.value.closeFile(path) // 关闭页签
editorRef.value.getActiveFile() // 当前活动 FileNode
editorRef.value.getFiles() // 全部文件(含未保存内容与 fileKind/remoteUrl/isDirectory)
editorRef.value.setFileContent(path, content)
// 外部写入内容并标记为已保存(如 revision 冲突时采用远端内容)
editorRef.value.markFileSaved(path, content)
// 调用方持久化成功后确认保存;save/save-all 事件不会提前清除未保存状态
插件 API
app.use(VueMonacoEditor, options?)
export interface VueMonacoEditorPluginOptions {
/** Worker 配置,见「Worker 加载策略」 */
workers?: ConfigureWorkersOptions
/** 注册的组件名,默认 'VueMonacoEditor' */
componentName?: string
}
独立导出
import {
VueMonacoEditor, // 主组件
configureWorkers, // worker 配置
setFeedbackProvider, // 接管 toast / confirm 弹窗
} from '@emtt/vue-monaco-ide'
工具函数通过独立子路径导入,例如 @emtt/vue-monaco-ide/utils/file-kind、@emtt/vue-monaco-ide/utils/path、@emtt/vue-monaco-ide/utils/language 和 @emtt/vue-monaco-ide/utils/markdown。
定制与扩展
国际化
默认简体中文;app.use(VueMonacoEditor, { locale: 'en-US' }) 可使用英文。
也可运行时调用 setLocale('en') / setLocale('zh'),并使用 setMessages({ publish: '部署' }) 自定义文案,setMessages({}) 清除覆盖。配置在同一模块的组件实例之间共享。
主题变量
所有 UI 颜色通过 CSS 变量暴露,覆盖即可匹配品牌:
:root {
--vme-primary: #7c3aed; /* 主色:激活页签、发布按钮 */
--vme-accent: #7c3aed; /* 文件树操作按钮 hover */
--vme-border: #e5e7eb;
--vme-success: #22c55e;
--vme-warning: #f97316;
/* 完整列表见 src/styles/index.css */
}
接管 toast / 确认弹窗
内置零依赖的 toast 与 confirm(删除确认)。宿主有 UI 框架时可接管:
import { setFeedbackProvider } from '@emtt/vue-monaco-ide'
import { ElMessage, ElMessageBox } from 'element-plus'
setFeedbackProvider({
toast: (msg, type) => ElMessage({ message: msg, type }),
confirm: async ({ title, message }) =>
await ElMessageBox.confirm(message, title).then(() => true).catch(() => false),
})
依赖说明
| 依赖 | 类型 | 用途 |
|---|---|---|
vue |
peer | 组件框架(^3.5) |
monaco-editor |
peer | 编辑器内核与 TS/JSON 语言服务(~0.52.2) |
monaco-pyright-lsp |
peer | Pyright Web Worker 打包与 worker 协议(0.1.x) |
monaco-yaml |
dependency | 默认 YAML schema 校验、补全与悬停 |
vscode-languageserver |
dependency | LSP JSON-RPC 协议实现(monaco-pyright-lsp 同款协议库,主线程与 worker 通信) |
vite / vue-tsc / eslint 等 |
dev | 仅构建/校验期使用,不进入运行时产物 |
本包不含也不需要:element-plus / @element-plus/icons-vue(图标为内置 SVG)、UnoCSS / Tailwind(样式为内置 scoped CSS + CSS 变量)、md-editor-v3 / markdown-it(预览走插槽或内置零依赖渲染器)。
局限性与注意事项
-
仅浏览器 ESM 使用;不承诺 SSR 或 CommonJS。Vue 声明使用 3.5 类型 API,因此不再声明支持 Vue 3.3/3.4。Monaco 支持范围收紧至实际开发使用的 0.52.2 系列。
-
GitHub Pages 在线演示与本地 playground 使用同一套完整示例。
-
Pyright worker 加载与初始化有开销:按需加载,仅项目含 Python 文件时才拉取;首次分析(加载 typeshed、解析 import)需要数秒,期间显示「Python LSP 分析中」轻提示
-
serverHooks 在途竞态:钩子请求进行期间外部重置
filesprop 不做防护,宿主应避免在钩子在途时整体替换文件列表 -
内置 Markdown 渲染器定位为轻量预览:不支持原始 HTML 透传、缩进式代码块、任务列表、脚注与公式;复杂渲染需求请走
#preview插槽 -
yaml 语言服务的 schema 请求固定关闭:
enableSchemaRequest: false,schema 需内联传入yamlSchemas[].schema(默认 worker 走 CDN/+esm端点,断网或workerUrls.yaml: false时仅保留基础高亮) -
浏览器内存:大型项目(数百文件全量 TS model 同步)会占用较多内存,model 创建已做分块让出主线程,但超大工程建议分批挂载
-
删除确认/提示默认为内置 DOM 实现,可接管
-
下载由调用方负责:组件只发出
download事件,文件夹及任意二进制文件的获取、打包和权限控制由业务层实现 -
多实例路径必须唯一:同一页面挂载多个编辑器时,各实例的
files[].path必须全局唯一。Monaco 使用文件路径作为 Model URI;如果两个实例都使用/src/main.ts,可能共享内容、诊断标记或生命周期状态。需要时请由调用方在路径前增加项目或实例前缀 -
单实例编辑器区域(一个组件一个 Monaco editor 实例;文件树/页签为组件内部状态)
开发
pnpm install
pnpm dev # playground(http://localhost:5180)
pnpm quality # 格式化并检查 ESLint
pnpm type-check # vue-tsc
pnpm test:dev # 行为测试
pnpm build # 库构建(dist/)+ 类型声明(dist/types/)
pnpm build:site # Pages 最小消费者示例
pnpm pack # 验证并生成实际 npm tarball
贡献指南见 CONTRIBUTING.md。
发布
仅手动发布;CI 执行质量检查、测试、构建与打包。发布前使用 pnpm publish:npm,并先确认 tarball、版本号和 npm 登录状态。当前 0.2.0 已发布。
License
MIT,作者 hao503106@163.com(GitHub)。
Related Packages
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.3.0 | ^3.5.0 | MIT | |
| 0.2.0 | ^3.5.0 | MIT |