简单粗暴而又不失优雅地在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调用的形式,那么我基于此进行改造即可。

大概思路如下:

  1. 提供加载方法,在调用前以script的形式动态加载资源,完成后暴露,供后续复用
  2. 再次调用直接复用,无需再次加载

那么首先提供一个通用的加载方法:

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) => {
      // ... 
    })
  }) // 新增
})

Last modified on 2018-08-19