简单粗暴而又不失优雅地在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) => {
      // ... 
    })
  }) // 新增
})