简单粗暴而又不失优雅地在vue项目中使用monaco
monaco-editor 官方实际是有ESM的支持的,当时没注意到 -_-||,实际项目请优先采用,详情请点击Integrating the ESM version of the Monaco Editor (https://github.com/Microsoft/monaco-editor/blob/master/docs/integrate-esm.md)
monaco-editor是一款直接在vscode中使用的编辑器,其强大之处就不用多说了。
然而其本身并未提供直接在vue中打包使用的机制,虽然有vue-monaco-editor,但其本身是基于react的移植版,而且很久未更新,不知道有多少坑,不敢用。
然而好处是monaco-editor直接提供了在浏览器以script调用的形式,那么我基于此进行改造即可。
大概思路如下:
- 提供加载方法,在调用前以script的形式动态加载资源,完成后暴露,供后续复用
- 再次调用直接复用,无需再次加载
那么首先提供一个通用的加载方法:
load-monaco.js
const MONACO_PATH = './static/js/monaco-editor/min/vs';
// 处理 monaco-editor 资源的加载
export default function loadMonaco() {
if (window.__MONACO_PROMISE__) {
return window.__MONACO_PROMISE__;
}
const scriptStr = `
require.config({
paths: {
'vs': '${MONACO_PATH}'
}
});
require(['vs/editor/editor.main'], function () {
window.__monaco_editor__ = monaco;
});
`;
const editorPromise = new Promise((resolve, reject) => {
// loader 加载
const loaderScript = document.createElement('script');
loaderScript.id = 'monaco-editor-loader';
loaderScript.src = MONACO_PATH + '/loader.js';
loaderScript.onload = () => {
loaderScript.onload = null;
resolve('loader');
};
loaderScript.onerror = () => {
loaderScript.onerror = null;
reject('monaco-editor资源加载失败');
};
document.body.appendChild(loaderScript);
})
.then(() => {
// 依赖资源加载
return new Promise((resolve, reject) => {
const editorScript = document.createElement('script');
editorScript.text = scriptStr;
editorScript.onerror = () => {
editorScript.onerror = null;
reject('monaco-editor资源加载失败');
};
document.body.appendChild(editorScript);
resolve('editor');
});
})
.then(() => {
// 加载检测
return new Promise((resolve, reject) => {
let timer;
let count = 0;
function check() {
// 已经加载则直接成功
if (window.__monaco_editor__) {
clearTimeout(timer);
resolve();
} else {
// 否则继续检测 但总次数不超过1000
if (count++ < 1000) {
setTimeout(check, 30);
} else {
reject('monaco-editor资源加载失败');
}
}
}
check();
});
});
return (window.__MONACO_PROMISE__ = editorPromise);
}
大意是在全局 window
下以一个名为 __MONACO_PROMISE__
的promise来处理资源的加载。其内部实现了一个monaco-editor 自身资源的loader标签加载。在这个loader加载完成后,再创建一个script标签,使用monaco自身的loader去加载其必须资源(实现代码参manaco的demo)。 全部完成后,解决此promise。
之后的调用也可以一直使用此promise,那么使用的时候直接这样就可以了:
import loadMonaco from '../utils/load-monaco.js';
export default {
mounted() {
loadMonaco().then(() => {
this.editor = __monaco_editor__.editor.create(this.$refs.codeEditor, {
value: this.codeSource,
language: 'html',
theme: 'vs-dark',
automaticLayout: true,
autoIndent: true,
autoClosingBrackets: true,
acceptSuggestionOnEnter: 'on',
colorDecorators: true,
dragAndDrop: true,
formatOnPaste: true,
formatOnType: true,
mouseWheelZoom: true
});
})
}
}
还有一点可以优化,上面的 load-monaco.js 中指定的 MONACO_PATH
是 ./static/js/monaco-editor/min/vs
,而我们的 monaco-editor
肯定是npm安装的,源码在 node_modules,我们需要将其自动同步过来。
同样,写一个模块单独处理此资源的拷贝:
const path = require('path')
const fs = require('fs')
const monacoToFolder = path.resolve(__dirname, '../static/js/monaco-editor')
const monacoFromFolder = path.resolve(__dirname, '../node_modules/monaco-editor')
function mkDirExist(path) {
if (!fs.existsSync(path)) {
fs.mkdirSync(path);
}
}
function copy(src, dist) {
fs.createReadStream(src).pipe(fs.createWriteStream(dist));
}
function copyFolder(src, output) {
mkDirExist(path.resolve(output));
src = path.resolve(src);
let temp = '';
if (fs.existsSync(src)) {
fs.readdirSync(src).forEach((file) => {
temp = path.resolve(src, file);
if (fs.statSync(temp).isDirectory()) {
copyFolder(temp, path.resolve(output, file))
} else {
copy(temp, path.resolve(output, file))
}
});
}
}
module.exports = new Promise((resolve, reject) => {
// 已经存在则直接成功
if (fs.existsSync(monacoToFolder)) {
console.log('[dev output]: monaco-editor 目录已经存在,直接开始构建!');
resolve()
} else {
// 否则进行拷贝
try {
console.log('[dev output]: monaco-editor 资源尚不存在,开始拷贝!');
copyFolder(monacoFromFolder, monacoToFolder)
console.log('[dev output]: monaco-editor 资源拷贝完成,开始构建!');
resolve()
} catch (error) {
reject(error)
}
}
})
修改 webpack.dev.conf.js
,启动dev服务时,首先处理资源,完成后再开始。而如果资源已经存在,则这个promise会直接成功,以加快构建速度(比直接配置 CopyWebpackPlugin
快多了)。
module.exports = require('./copy-monaco').then(() => { // 新增
return new Promise((resolve, reject) => {
// ...
})
}) // 新增
然后修改 build/build.js
,使得其在构建时也自动从node_modules 拷贝最新的代码过来:
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
require('./copy-monaco').then(() => { // 新增
return webpack(webpackConfig, (err, stats) => {
// ...
})
}) // 新增
})