驱动VS Code的monaco-editor——最佳使用指南

9,247 阅读7分钟

什么是 monaco-editor

Monaco编辑器是驱动VS Code的代码编辑器。它采用MIT许可证,并支持Edge、Chrome、Firefox、Safari和Opera浏览器。Monaco编辑器不支持移动浏览器或移动Web框架。

---译自《monaco-editor官方网站

monaco 是一个开源的编辑器插件,这个编辑器功能十分强大,可以让你在浏览器中轻松的进行代码编辑。许多知名网站都用到了这个编辑器插件,如果你的网站也有使用代码编辑器的需求,强烈推荐你使用一下monaco。当然,也有其他很多类似的编辑器插件,但 monaco 以其强大而完备的功能独占鳌头。

虽然 monaco 十分强大,与之伴随而来的是它的包也比较大,当我们去使用它的时候,会给我们的网站带来一些性能方面的影响,目前现有的关于 monaco 的使用的文章也比较少,本篇文章介绍一下笔者在对 monaco 使用方面的一些优化尝试与建议。

常规使用方式

打开 monaco-editor 文件,我们可以看到,有 esmmin 两个文件包,里面的目录结构也比较相似,意味着有两种引入方式,以下将会介绍。

下载包

npm install monaco-editor

AMD 引入方法

参考: github.com/microsoft/m…

<!DOCTYPE html>
<html>
	<head>
		<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
	</head>
	<body>
		<div id="container" style="width:800px;height:600px;border:1px solid grey"></div>

		<script src="monaco-editor/min/vs/loader.js"></script>
		<script>
			require.config({ paths: { vs: 'monaco-editor/min/vs' } });
			require(['vs/editor/editor.main'], function () {
				var editor = monaco.editor.create(document.getElementById('container'), {
					value: ['function x() {', '\tconsole.log("Hello world!");', '}'].join('\n'),
					language: 'javascript'
				});
			});
		</script>
	</body>
</html>

ESM 引入方法

参考:github.com/microsoft/m…

import * as monaco from 'monaco-editor';

self.MonacoEnvironment = {
	getWorker: function (workerId, label) {
		const getWorkerModule = (moduleUrl, label) => {
			return new Worker(self.MonacoEnvironment.getWorkerUrl(moduleUrl), {
				name: label,
				type: 'module'
			});
		};

		switch (label) {
			case 'json':
				return getWorkerModule('/monaco-editor/esm/vs/language/json/json.worker?worker', label);
			case 'css':
			case 'scss':
			case 'less':
				return getWorkerModule('/monaco-editor/esm/vs/language/css/css.worker?worker', label);
			case 'html':
			case 'handlebars':
			case 'razor':
				return getWorkerModule('/monaco-editor/esm/vs/language/html/html.worker?worker', label);
			case 'typescript':
			case 'javascript':
				return getWorkerModule('/monaco-editor/esm/vs/language/typescript/ts.worker?worker', label);
			default:
				return getWorkerModule('/monaco-editor/esm/vs/editor/editor.worker?worker', label);
		}
	}
};

monaco.editor.create(document.getElementById('container'), {
	value: "function hello() {\n\talert('Hello world!');\n}",
	language: 'javascript'
});

如果你仅需要使用其核心功能,不用通过 import * as monaco from 'monaco-editor'引入整个代码包,你可以可以这样引入monaco ,会使得 monaco的体积减小一点:

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

相关的功能需要选择性的引入,比如你需要使用 monaco 展示 js 语言,并且使用注释功能,你将需要这样引入

import 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution'
import 'monaco-editor/esm/vs/editor/contrib/comment/browser/comment.js'

个人不推荐这种引入方式,因为相关文档几乎没有,会需要一些理解成本去尝试

缺点

  1. ESM 引入方式导致包过大:打包后的文件高至 5 到 6M,即使仅使用其核心功能,文件依旧有超过2M的体积,给网页加载带来了一定的负担
  2. AMD 引入的方式只能同步加载脚本文件,意味着需要等待脚本加载完毕才能使用,依然对性能造成一定的影响

CDN引入使用

伴随着 monaco 编辑器强大功能,打包出来的包体积偏大是可以理解的。由此我们可以采用 cdn 引入的方式,使得 monaco 以更快更轻便的方式加载。

免费 cdn 服务比较多,比如 cloudflarejsdelivrbootcdn 这些 CDN加速服务,以下我将以 cloudflare 作为示例。

步骤

以下为 monacovue3 中的使用

  1. 在 html 文件当中引入cdn资源
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/logo.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>lemCloud</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/loader.js"></script>
    <script defer>
      require.config({
        paths: {
          vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/',
        },
      })
      window.MonacoEnvironment = {
        getWorkerUrl: function (workerId, label) {
          return `data:text/javascript;charset=utf-8,${encodeURIComponent(`
            self.MonacoEnvironment = {
              baseUrl: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/'
            };
            importScripts('https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/base/worker/workerMain.js');`)}`
        },
      }
      require(['vs/editor/editor.main'], function () {
      })
    </script>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>
  1. 使用
monaco.editor.create(document.getElementById('container'), {
	value: "function hello() {\n\talert('Hello world!');\n}",
	language: 'javascript'
});

缺点

对性能造成较大影响:在 vue3 中使用 monaco 时是在 index.html 文件中引入两个脚本文件,并且第二个脚本必须等待第一个脚本调用成功后再进行调用,无论你是否在首页使用了它,都会加载上面这些 monaco 相关的文件,意味着将会加长首屏加载时间,并非最佳实践方案。

通过 @monaco-editor/loader 使用

www.npmjs.com/package/@mo…

这是一个可以在浏览器中轻松设置 monaco-editor 的实用程序,通过其加载器脚本配置和下载 monaco 源代码,无需使用 webpack(或任何其他模块捆绑器)的配置文件。

详细的使用步骤可以参考其文档链接,本文仅介绍其在 vue3 中的一次实践尝试。

下载包

npm install @monaco-editor/loader

创建 hooks

由于这是一部分可以公用的代码,所以使用了 vue3hooks,方便 monaco 的功能可重复地使用。

创建了 initMonaco 方法异步引入 monaco,并避免重复初始化,并且不阻塞页面的正常加载,提供了一个monacoRef(即为前文中的 monaco),在想使用的页面中使用。

// src\hooks\useMonacoEditor.ts
import loader from '@monaco-editor/loader'
import { ref } from 'vue'

const monacoRef = ref<any>(null)
const monacoLoader = loader.init()

const initMonaco = () => {
  return new Promise<void>((resolve, reject) => {
    if (monacoRef.value) {
      resolve()
      return
    }
    monacoLoader
      .then((monacoInstance) => {
        monacoRef.value = monacoInstance
      })
      .catch((error) => {
        if (error?.type !== 'cancelation') {
          console.error('Monaco initialization error:', error)
          reject()
        }
      })
  })
}

export function useMonacoEditor() {
  return {
    initMonaco,
    monacoRef
  }
}

使用

import { useMonacoEditor } from '@/hooks/useMonacoEditor'
const { monacoRef, initMonaco } = useMonacoEditor()

onMounted(async()=>{
	await initMonaco()
  monacoRef.value.editor.create(document.getElementById('container'), {
  	value: "function hello() {\n\talert('Hello world!');\n}",
  	language: 'javascript'
  });
})

可以看到使用 @monaco-editor/loader 之后,下载的文件在 1M以下,并使用了 jsdelivr 的CDN加速服务,可以使用到 monaco-editor 的几乎所有功能。

总结

总的来说本篇文章是笔者对于 monaco-editor 的最佳使用方式探索尝试的总结,目前比较推荐通过 @monaco-editor/loader 引入的方式,可以异步使用,并使用 CDN服务加速,可以更方便快捷的在浏览器中使用。感谢阅读,如有疑问,欢迎留言讨论。

彩蛋

monaco 详细配置分享

monacoRef.value.editor.create(document.getElementById('container'), {
    acceptSuggestionOnCommitCharacter: true, // 接受关于提交字符的建议
    acceptSuggestionOnEnter: 'off', // 接受输入建议 "on" | "off" | "smart"
    accessibilityPageSize: 10, // 辅助功能页面大小 Number 说明:控制编辑器中可由屏幕阅读器读出的行数。警告:这对大于默认值的数字具有性能含义。
    accessibilitySupport: 'on', // 辅助功能支持 控制编辑器是否应在为屏幕阅读器优化的模式下运行。
    autoClosingBrackets: 'always', // 是否自动添加结束括号(包括中括号) "always" | "languageDefined" | "beforeWhitespace" | "never"
    autoClosingDelete: 'always', // 是否自动删除结束括号(包括中括号) "always" | "never" | "auto"
    autoClosingOvertype: 'always', // 是否关闭改写 即使用insert模式时是覆盖后面的文字还是不覆盖后面的文字 "always" | "never" | "auto"
    autoClosingQuotes: 'always', // 是否自动添加结束的单引号 双引号 "always" | "languageDefined" | "beforeWhitespace" | "never"
    automaticLayout: true, // 自动布局
    codeLens: false, // 是否显示codeLens 通过 CodeLens,你可以在专注于工作的同时了解代码所发生的情况 – 而无需离开编辑器。 可以查找代码引用、代码更改、关联的 Bug、工作项、代码评审和单元测试。
    codeLensFontFamily: '', // codeLens的字体样式
    codeLensFontSize: 13, // codeLens的字体大小
    colorDecorators: false, // 呈现内联色彩装饰器和颜色选择器
    comments: {
      ignoreEmptyLines: true, // 插入行注释时忽略空行。默认为真。
      insertSpace: true, // 在行注释标记之后和块注释标记内插入一个空格。默认为真。
    }, // 注释配置
    contextmenu: false, // 启用上下文菜单
    columnSelection: false, // 启用列编辑 按下shift键位然后按↑↓键位可以实现列选择 然后实现列编辑
    autoSurround: 'never', // 是否应自动环绕选择
    copyWithSyntaxHighlighting: true, // 是否应将语法突出显示复制到剪贴板中 即 当你复制到word中是否保持文字高亮颜色
    cursorBlinking: 'smooth', // 光标动画样式
    cursorSmoothCaretAnimation: 'on', // 是否启用光标平滑插入动画  当你在快速输入文字的时候 光标是直接平滑的移动还是直接"闪现"到当前文字所处位置
    cursorStyle: 'line', // "Block"|"BlockOutline"|"Line"|"LineThin"|"Underline"|"UnderlineThin" 光标样式
    cursorSurroundingLines: 0, // 光标环绕行数 当文字输入超过屏幕时 可以看见右侧滚动条中光标所处位置是在滚动条中间还是顶部还是底部 即光标环绕行数 环绕行数越大 光标在滚动条中位置越居中
    cursorSurroundingLinesStyle: 'all', // "default" | "all" 光标环绕样式
    cursorWidth: 2, // <=25 光标宽度
    minimap: {
      enabled: false, // 是否启用预览图
    }, // 预览图设置
    scrollbar: {
      verticalScrollbarSize: 5,
      horizontalScrollbarSize: 5,
      arrowSize: 10,
      alwaysConsumeMouseWheel: false,
    },
    folding: false, // 是否启用代码折叠
    links: true, // 是否点击链接
    overviewRulerBorder: false, // 是否应围绕概览标尺绘制边框
    renderLineHighlight: 'gutter', // 当前行突出显示方式
    scrollBeyondLastLine: false, // 设置编辑器是否可以滚动到最后一行之后
    readOnly: false, // 是否为只读模式
    lineNumbers: 'on',
    lineNumbersMinChars: 0,
    theme: 'vs', //官方自带三种主题vs, hc-black, or vs-dark
    value: props.code || '{}', //编辑器初始显示文字
    language: 'json',
    fontSize: 13,
    roundedSelection: true, // 右侧不显示编辑器预览框
    autoIndent: 'full',
    formatOnType: true,
    formatOnPaste: true
  })