
Vue 项目里折腾 monaco-editor最烦的就是这个报错Could not resolve monaco-editor/esm/vs/editor/editor.api。坦白讲我最初看到这行英文也愣了一下明明monaco-editor已经写进 dependencies 了为什么打包器还是告诉我解析不到这个路径后来把 node_modules 翻了个底朝天才明白这不是缺包的问题而是包结构、导入方式、构建工具配置三件事搅在一起的结果。这篇文章我不打算讲什么“优雅封装”的空话而是把我在实际项目里怎么排查、怎么修、修完还要防哪些连环坑的完整过程写下来。不管你的项目用的是 vite 还是 webpack也无论你会不会 TypeScript照着下面的路子基本都能跑通。内容会涉及 monaco-editor 的目录结构、两种构建工具下的 worker 处理方式以及一版可以直接抄进 Vue 组件里的最小实现代码。1. 这个报错到底卡在哪1.1 报错信息拆解Could not resolve这条消息大概率不是你代码写错了而是打包器在做“模块解析”的时候找不到目标文件。换成人话你告诉打包器“我要导入monaco-editor/esm/vs/editor/editor.api”打包器把这段路径当成一个地址去 node_modules 里找结果发现这个地址对不上于是直接抛异常。我见过两种措辞不同的版本。Vite Rollup 环境下通常报[vite]: Rollup failed to resolve import monaco-editor/esm/vs/editor/editor.api from src/main.ts.Vue CLI / webpack 环境下则更像Module not found: Error: Cant resolve monaco-editor/esm/vs/editor/editor.api in /project/src两者本质上是一件事只是不同工具习惯用不同的说法。你只要记住这段导入路径打包器不认。那么问题来了为什么不认网上很多老教程都写import * as monaco from monaco-editor/esm/vs/editor/editor.api这也不是凭空捏造的路径它确实对应 monaco-editor 这个包里的一个 ES Module 入口。但“存在”不等于“一定可被解析”这中间还有包自身的 exports 限制、版本差异、构建工具配置等多重因素。我后面会逐一拆开讲。1.2 monaco-editor 的包结构到底长什么样先别急着改代码我建议你先去node_modules/monaco-editor目录底下翻一翻好多疑问当场就能解开。这个包安装好后根目录一般会同时存在几个子目录常见的有esm/ES Module 版本后续导入monaco-editor/esm/...指的就是这里。min/压缩后的 ES Module 版本体积更小生产构建常用。dev/未压缩、带更完整报错信息的开发版本。umd/老式全局版本直接 script 标签引入时用。esm/vs/editor/editor.api.js这个文件就是你想要 import 的目标。它内部把 monaco 的核心 APIeditor.create、languages.register、ColorProvider等统一暴露出来很多组件库和二次封装都是基于它写的。但你需要注意一点这个包在不同历史版本里目录结构有过调整。20.x 时代很多教程用这套路径是完全没有问题的到 0.31、0.34、0.40 这些版本esm目录依旧存在再往后某些版本开始调整exports字段对子路径的访问限制变得严格。也就是说你在老博客上看到的导入路径放到新版本下可能直接失效。另外还有一种很气人的情况npm 安装过程中断、镜像源同步延迟、lockfile 版本混乱导致本地node_modules/monaco-editor/esm目录根本就是残缺的。这种情况你不会马上发现因为入口文件monaco-editor本身可能存在但子路径文件没了。所以排查第一步永远是看一眼目录里到底有没有这个文件千万不要一上来就怀疑代码逻辑。1.3 为什么 webpack 和 vite 对这个路径的反应不一样我举个生活化的类比你把一个快递的收货地址写成了“小区名 楼栋号”但漏了门牌号。快递员是根据手里面单规则去匹配的不同快递公司对地址库的完整度要求不一样。webpack 的解析规则比较“宽容”它会启用各种 fallback 尝试拼接后缀、补全扩展名vite 底层用 esbuild 和 Rollup它们更看重 Node.js 语义尤其是在package.json里声明了exports字段后解析器会严格按照这个白名单来放行路径。所以你会看到几个典型场景同一个项目从 vue-cli 切换到 vite原本能跑的代码突然报Could not resolve。同一个 vite 项目把monaco-editor从 0.34 升到 0.45原本好的代码突然炸掉。同一个版本本地开发好好的CI 环境执行npm ci后再打包开始报错。这些都不是玄学背后全是“包结构 解析规则 安装完整性”在起作用。2. 动手之前的三分钟自查2.1 先确认依赖真的装好了很多人看到报错第一反应是百度其实更快的办法是直接看现场。打开终端进到项目目录执行npm ls monaco-editor如果输出显示missing说明依赖根本没有装上如果显示了你预期的版本那就继续看包内容。然后看目录是否存在ls -la node_modules/monaco-editor/esm/vs/editor/editor.api.js这个文件存在问题就还有得救如果提示No such file or directory就要检查是不是安装过程出了问题。我的习惯是直接删掉node_modules和package-lock.json或者yarn.lock/pnpm-lock.yaml重新装一遍。虽然听起来有点暴力但很多时候就是安装缓存产生了半成品目录。补充一点用 pnpm 的朋友要注意pnpm 的 node_modules 结构和 npm 完全不同包之间通过 symlink 互相引用。某些情况下monaco-editor 的 worker 文件是动态加载的pnpm 严格的依赖隔离策略会让它找不到兄弟模块。如果你恰好用的是 pnpm又恰好遇到这类解析问题优先试试换到 npm 或者 yarn 重装依赖很多时候问题忽然就消失了。2.2 检查 monaco-editor 的版本和 package.json 声明看清楚版本号是判断很多问题的第一步。方法很简单node -e console.log(require(./node_modules/monaco-editor/package.json).version)也可以直接打开node_modules/monaco-editor/package.json重点看两个字段第一个是main它告诉你当你import * as monaco from monaco-editor时打包器最终加载哪个文件。第二个是exports它相当于一份“允许外部访问的路径白名单”。如果这个字段存在且里面没有./esm/vs/editor/editor.api这条子路径那么无论你用 vite 还是新版本 webpack直接用子路径导入都会很被动。所以我后面给的两个修复方案里第一个建议往往是别跟子路径死磕直接改用主入口导入。虽然会牺牲一些体积但换来的是稳定。2.3 警惕多版本不兼容还有一个隐蔽问题值得单独拿出来说。如果项目里同时存在多个版本的 monaco-editor比如你的组件库 B 依赖0.34.0代码里又装了0.45.0打包器解析到不同位置时会非常混乱。你可以在npm ls monaco-editor的输出里看到完整的依赖树如果有多个版本建议先统一overrides: { monaco-editor: 0.45.0 }或者直接让组件库和业务代码都使用同一个版本。这种“明明装了包却总是报解析失败”的场景十有八九和版本分裂有关。3. Vite 项目里的修复方案3.1 方案 A先换成主入口导入如果你的项目是用 vite 构建的最简单、最稳妥的方式是把导入路径从monaco-editor/esm/vs/editor/editor.api改成monaco-editor。在代码里这样写import * as monaco from monaco-editor这个方案不依赖任何子路径是官方 package 的主入口几乎所有版本的打包器都能正常解析。代价是会引入更多代码因为主入口默认把一大堆语言和功能都打包进来了。但对绝大多数不是极度在意首屏体积的项目来说完全够用。我实际操作下来很多“为什么 monaco 一集成就报错”的案例换成这行导入之后立刻就不报错了。等整个功能跑通再考虑要不要切成按需加载。3.2 方案 B彻底解决 worker 资源定位直接用主入口导入能解决解析报错但 monaco 的另一个老大难问题会接踵而至编辑器一打开控制台出现一堆 worker 加载失败的 404或者代码提示功能完全失效。原因在于 monaco-editor 的架构里核心编辑区、语法高亮、代码格式化等工作是放在 Web Worker 里跑的。构建工具如果不知道 worker 文件的真实地址就会用默认的相对路径去找结果当然是找不到。Vite 下正确做法是手动声明 worker 环境。在入口文件比如 Vue 的main.ts或某个工具模块里加上这一段import editorWorker from monaco-editor/esm/vs/editor/editor.worker?worker import jsonWorker from monaco-editor/esm/vs/language/json/json.worker?worker import cssWorker from monaco-editor/esm/vs/language/css/css.worker?worker import htmlWorker from monaco-editor/esm/vs/language/html/html.worker?worker import tsWorker from monaco-editor/esm/vs/language/typescript/ts.worker?worker self.MonacoEnvironment { getWorker(_, label) { if (label json) { return new jsonWorker() } if (label css || label scss || label less) { return new cssWorker() } if (label html || label handlebars || label razor) { return new htmlWorker() } if (label typescript || label javascript) { return new tsWorker() } return new editorWorker() } }?worker是 vite 提供的静态资源处理语法构建时会把目标文件打包成独立的 worker 文件并返回一个可以直接new的构造器。设置好MonacoEnvironment.getWorker后monaco 在运行时会问“我应该给这类语言分配哪个 worker”然后拿到对应的 worker 实例。这里需要注意这段代码要放在真正使用 monaco 之前执行。如果你在 Vue 组件里写import * as monaco from monaco-editor最好把 worker 环境配置放到main.ts或者一个公共工具文件的最上方保证初始化顺序。3.3 方案 C手工引入入口文件 CSS有些场景下你想走按需加载的路线同时避开主入口带来的超大体积。比如只想要 JavaScript、TypeScript 的语法支持不想把全世界的语言都打进来。这时可以这样写import * as monaco from monaco-editor/esm/vs/editor/editor.api import monaco-editor/esm/vs/editor/editor.main但前面说过子路径在新版本下可能被exports限制。另一种绕法是用 vite 的resolve.alias硬性把某个子路径指到物理文件import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], resolve: { alias: { monaco-editor/esm/vs/editor/editor.api: monaco-editor/esm/vs/editor/editor.api.js, }, }, })这只是一种兜底手段核心作用是把不带后缀的路径强制补成实际存在的.js文件。如果你不想折腾 alias更推荐先试主入口稳定运行之后再考虑按需方案。另外还要记得引入 CSS。monaco 的图标、滚动条、布局样式都依赖样式文件不引入会出现界面错乱。Vite 下通常放在main.ts里import monaco-editor/min/vs/editor/editor.main.css如果这句也报解析失败就去node_modules/monaco-editor里找真实存在的 css 文件名因为不同版本路径有细微差异。3.4 vite.config.js 最终配置参考最后我把一个可以在 Vite 新项目里直接使用的配置放在一起方便你对照抄import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], optimizeDeps: { include: [monaco-editor], }, build: { chunkSizeWarningLimit: 3000, }, })optimizeDeps.include是让 vite 在开发阶段提前预构建 monaco-editor减少首次加载时的二次转换。chunkSizeWarningLimit调大纯粹是因为 monaco 打包后体积不小避免构建时老是警告你可以按项目实际情况改。Vite 下的关键点总结成一句话能用主入口就别用子路径能用?worker就别让 monaco 自己猜 worker 地址。4. Webpack 项目里的修复方案4.1 用官方插件一条龙处理Vue 项目如果走的是 vue-cli / webpack 路线官方其实提供了专门的插件monaco-editor-webpack-plugin。它的作用就是把你在代码里写的import * as monaco from monaco-editor自动拆解、重定向到准确的 ESM 入口同时把 CSS、字体、worker 全部处理妥当。先安装npm install monaco-editor-webpack-plugin -D然后在vue.config.js里配置。vue-cli 项目通常有两种写法我一般用chainWebpackconst MonacoWebpackPlugin require(monaco-editor-webpack-plugin) module.exports { chainWebpack(config) { config.plugin(monaco).use(MonacoWebpackPlugin, [ { languages: [json, javascript, typescript, html, css], features: [coreFeatures], }, ]) }, }如果你更喜欢configureWebpack的直白风格也可以这样写const MonacoWebpackPlugin require(monaco-editor-webpack-plugin) module.exports { configureWebpack: { plugins: [ new MonacoWebpackPlugin({ languages: [json, javascript, typescript, html, css], }), ], }, }languages数组可以按需声明每多声明一种语言打包产物就会多一部分对应语法支持。这样做的好处是monaco-editor的主入口照样导入但最终打进 bundle 的只有你需要的语言和功能。我刚才说“能跑通再优化”webpack 项目里这个插件就是优化阶段的利器。它帮你省掉的死磕点包括入口文件重定向、样式注入、字体文件处理、worker 文件复制以及运行时环境注入。没有它手动配置 webpack 的 rule 会相当麻烦。4.2 不依赖插件的手动 worker 配置如果你因为某些原因不想引入额外插件也可以手动配置 worker。思路和 vite 那边差不多只是语法不同。在入口文件设置import * as monaco from monaco-editor self.MonacoEnvironment { getWorker(workerId, label) { const getWorkerModule (moduleUrl, label) { return new Worker(new URL(moduleUrl, import.meta.url), { type: module }) } switch (label) { case json: return getWorkerModule(/vs/language/json/json.worker.js, label) case css: case scss: case less: return getWorkerModule(/vs/language/css/css.worker.js, label) case html: case handlebars: case razor: return getWorkerModule(/vs/language/html/html.worker.js, label) case typescript: case javascript: return getWorkerModule(/vs/language/typescript/ts.worker.js, label) default: return getWorkerModule(/vs/editor/editor.worker.js, label) } } }这种写法在 webpack 5 环境下是可以工作的前提是你已经通过某种方式把 monaco-editor 的资源暴露成了静态文件。像 vue-cli 项目里webpack 5 的new URL(..., import.meta.url)会在构建时把资源地址计算出来不需要手工复制文件。但我得说句实在话既然官方提供了插件手动配置只适合你项目确实有特殊约束的情况。否则就是在给自己找活干。4.3 vue-cli 项目里的版本匹配提醒使用monaco-editor-webpack-plugin时最容易踩的坑是版本不匹配。早期版本的插件面向 monaco-editor 0.20 左右的目录结构设计后来 monaco 更新了好几轮插件也一直在跟进。如果你用 monaco-editor 0.34 却配了一个很老版本的monaco-editor-webpack-plugin可能会报奇怪的路径错误。我建议尽量用新版本插件并在package.json里锁定 monaco-editor 的精确版本{ devDependencies: { monaco-editor-webpack-plugin: ^0.3.2 }, dependencies: { monaco-editor: 0.45.0 } }不是说越新越好而是建议你固定一个已验证的组合不要每次npm install都让 monaco 悄悄漂移版本。这一点在 CI 构建里尤其重要今天能跑明天炸的情况往往就是依赖没锁死。5. 代码层面最容易踩的连环坑5.1 在 Vue 组件里如何正确初始化编辑器不管前面的报错最终是怎么解决的最后总要把 monaco 集成到 Vue 组件里。我给出一个在 Vue 3script setup环境中能直接跑起来的最小示例template div refeditorContainer classeditor-container/div /template script setup import { ref, onMounted, onBeforeUnmount } from vue import * as monaco from monaco-editor const editorContainer ref(null) let editor null onMounted(() { editor monaco.editor.create(editorContainer.value, { value: console.log(hello monaco), language: javascript, theme: vs-dark, automaticLayout: true, minimap: { enabled: false, }, }) // 如果需要监听内容变化 editor.onDidChangeModelContent(() { const value editor.getValue() console.log(content changed, value) }) }) onBeforeUnmount(() { if (editor) { editor.dispose() } }) /script style scoped .editor-container { width: 100%; height: 480px; border: 1px solid #ddd; } /style这里有几个细节要注意。容器必须有明确高度。很多小白第一次用 monaco发现页面一片空白查来查去最后发现div高度是 0。即便你设置了automaticLayout: true容器自身没有高度编辑器自然是不可见的。automaticLayout: true适合大多数场景它会让 monaco 监听容器尺寸变化并自动跟随布局。但如果你对整个页面做了比较频繁的 resize 操作这个模式会触发大量计算。更精细的做法是去掉这个选项自己监听需要调整的时机再手动调用editor.layout()组件卸载时一定要dispose。不然多路由切换几次内存里残留多个编辑器实例页面会越来越卡。5.2 运行阶段常见报错速查表以下是我在实际项目里遇到过的报错和对应处理方式整理成一张表方便你按图索骥报错信息原因分析处理办法Could not resolve monaco-editor/esm/vs/editor/editor.api子路径被 exports 限制或目录结构不完整改用主入口导入或按 3.3 的 alias 方式绕到物理文件self is not defined在 SSR、SSG 或 node 环境里加载了 monaco把 monaco 相关代码限制在浏览器端onMounted 动态 importprocess is not defined某些依赖在浏览器环境引用了 Node 的 process在 vite 的 define 里配置process.env: {}并定位真实依赖404 ... editor.worker.jsworker 资源路径不准按 3.2 或 4.2 配置MonacoEnvironment.getWorkercodicon.ttf 404monaco 图标字体没被正确复制检查 CSS 是否引入webpack 下确认插件正确处理了字体编辑器空白不渲染容器没有高度或初始化时机不对给容器固定高度确认在 DOM 挂载后执行 create重复进入页面内存飙升组件卸载时没有销毁编辑器实例在 onBeforeUnmount 调用editor.dispose()“process is not defined”这条值得多写两句。它经常出现在 vite 项目里因为某些工具库内部引用了process全局变量。你可以在vite.config.js里加export default defineConfig({ define: { process.env: {}, }, })这能让项目先跑起来但不要只依赖这个补丁还是要找出哪个库在使用 process能换就换能升级就升级否则问题会潜伏到生产环境。5.3 我的经验按需加载语言才是长期方案前面强调过“先跑通再优化”。等你的编辑器能正常打开下一步就是控制体积。monaco-editor 全量包不小如果你只做代码展示或轻量编辑完全没必要把上百种语言全带上。按需加载语言的核心思路是从主入口切到按模块引入。以 TypeScript 和 JavaScript 为例import monaco-editor/esm/vs/editor/editor.main import monaco-editor/esm/vs/language/typescript/monaco.contribution import monaco-editor/esm/vs/basic-languages/typescript/typescript.contribution每个语言模块通过monaco.contribution注册对应的语言支持。这个过程比较琐碎不同模块之间的依赖关系也要慢慢试。我的建议是分两步走第一步用主入口跑通业务逻辑第二步再做语言和功能的裁剪每裁剪一部分就重新构建一次看体积和功能是否符合预期。我自己实践下来的体感是把features收紧、只保留需要的语言之后产物体积通常能比全量版本少三到五成。对于产品体验来说这不是一个小数字。再补充一个经验不要直接修改node_modules/monaco-editor里的文件来“修复问题”。好几个朋友跟我抱怨这招不灵因为一旦重新npm install改动全部丢失而且根因依然存在。正确的顺序永远是先定位根因再在项目代码层面或构建配置层面修复最后把相关命令和配置写进项目文档免得团队其他成员重复踩坑。我每次新接一个前端项目只要涉及代码编辑器都会优先确认三件事项目用什么构建工具、monaco-editor 是什么版本、有没有现成的 worker 配置。这三件事里任何一件没确认后面大概率会绕路。如今再看到这篇文章开头的那个报错我已经不会觉得慌了因为流程已经固化成一套肌肉记忆先看包目录结构再确认导入方式然后处理 worker最后才是业务代码。最后再分享一个小技巧集成 monaco-editor 这类体积较大的库时建议把初始化逻辑抽成一个独立模块比如src/utils/monaco.ts统一处理导入、worker 环境、主题注册和语言注册。这样不管你的 Vue 组件里哪里要用编辑器都只调用同一个初始化函数遇到问题也只需要在一个文件里排查。如果你正在被这个报错折腾不妨按这个思路重构一下你会发现后续维护省心很多。