页面代码编辑器

448 阅读5分钟

可用依赖:ace,codemirror,monaco-editor

ace:ajax团队维护,demo中支持lua语言提示和校验

codemirror:lua只有基本的语法高亮和简易的语法提示

monaco-editor :微软团队维护,默认实现html,css,ts/js,json的语法补全和校验,其他语言只有基本的语法高亮。

en.wikipedia.org/wiki/Compar…

基本功能

安装monaco

npm install monaco-editor -S

HTML

<div id="monaco">
</div>

JS

import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js';
const monacoInstance=monaco.editor.create(document.getElementById("monaco"),{
  value:`console.log("hello,world")`,
  language:"javascript"
})
monacoInstance.dispose();//使用完成销毁实例

语法高亮

根据设置的语言,引入monaco中对应的内置语言文件

例如:

JavaScript

import 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution';

lua

import 'monaco-editor/esm/vs/basic-languages/lua/lua.contribution.js'

monaco的查找控件

import 'monaco-editor/esm/vs/editor/contrib/find/browser/findController.js'

monaco还有许多这类控件,我们可以按需引入自己用到的。

不过有一个更加简便的方法,那就是直接引入main文件来代替api文件。

import * as monaco from 'monaco-editor/esm/vs/editor/editor.main.js';

采用这种方式引入的话,会自动带上所有的内置语言和控件,唯一的缺点就是包的体积过大

代码补全和错误提示

首先,我们自己可以设想一下,假如要自己来实现代码补全以及错误提示,我们会怎么做?

第一,我们要解析输入的文本,这时,我们就需要写一个Parser。

第二,根据Parser解析的结果来调用monaco的标注接口来标注错误的代码从而实现错误提示功能

第三,根据Parser解析的结果信息,提供上下文相关的代码候选项来实现代码补全功能。

可以看出来,实现起来难度会很大,涉及到的点很多,不过,和语法高亮一样,monaco也帮助我们实现了这些功能,目前支持html,css,ts/js,json四种语言,我们只需要引入即可。但是这边的引入可没有语法高亮那么简单。

Monaco的实现采用worker的方式,因为语法解析需要耗费大量时间,所以用worker来异步处理返回结果比较高效。我们使用的话需要两步。

提供一个定义worker路径的全局变量

monacoInstance.MonacoEnvironment = {
 getWorkerUrl: function (moduleId, label) {
  if (label === 'json') {
    return './json.worker.js';
 }
  if (label === 'css') {
   return './css.worker.js';
  }
  if (label === 'html') {
   return './html.worker.js';
  }
  if (label === 'typescript' || label === 'javascript') {
   return './typescript.worker.js';
  }
  if(label==="sql"){
   return "./sql.worker.js";
  }
  return './editor.worker.js';
  }
}

选择对应的language,monaco会去调用getWorkerUrl去查worker的路径,然后去加载。这边默认会加载一个editor.worker.js,这是一个基础功能文件,提供了所有语言通用的功能(例如已定义常量的代码补全提示),无论使用什么语言,monaco都会去加载他。

2. 打包worker

在webpack中引入需要的worker

npm install monaco-editor-webpack-plugin -S
entry: {
      "main": path.resolve(process.cwd(), "src/main.js"),
      "editor.worker": 'monaco-editor/esm/vs/editor/editor.worker.js',
      "ts.worker": 'monaco-editor/esm/vs/language/typescript/ts.worker',
    },
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');

module.exports=function(){
  return {
    ...
    plugins:[
      new MonacoWebpackPlugin()
    ]
    ...
  }
}

插件会帮我们做这么几件事

  1. 自动注入getWorkerUrl全局变量
  2. 处理worker的编译配置
  3. 自动引入控件和语言包。
  4. 具体要引入哪些控件和语言包,我们可以通过配置languagesfeatures来控制
new MonacoWebpackPlugin({
  languages:["javascript","css","html","json"],
  features:["coreCommands","find"]
})

事件绑定

monacoInstance.onDidChangeModelContent((event) => {
 const newValue=monacoInstance.getValue();
 console.log(newValue)
})

monacoInstance是一个create方法返回的实例,他包含很多操作实例的方法。event是一个IModelContentChangedEvent对象,他包含了非常非常详细的变更信息,包括操作的类型(撤销、恢复,还是手动输入引发的文本变更),变更的文本位置,变更的文本内容等。而我们要获取最新的值,则需要调用

monacoInstance.getValue();

细心的朋友应该还会发现一个很奇怪的地方,那就是我们绑定的方法用的是onDidChangeModelContent, 里面有一个Model,这命名可是很讲究的,字面意思就是变更Model内容触发事件, 从头到尾,我们都没看到有Model的存在,那么为什么这边是变更Model内容触发事件呢,难道我们操作的是Model?

是的,其实我们在编辑的时候,就是在Model上编辑,默认情况下,monaco会帮我生成一个Model,我们可以调用getModel打印一下

monacoInstance.getModel()

看一看api,我们可以发现,Model其实是一个保存编辑状态的对象,他里面含有语言信息,当前的编辑文本信息,标注信息等。所以我们可以缓存一下Model对象,在需要的时候直接调用setModel即可随时切换到之前的状态。或者也可以在初始化实例的时候设置一个Model。

const model=monaco.editor.createModel("hahahaha","javascript");
monacoInstance = monaco.editor.create(this.monacoDom.current, {
 model:model
})

而且我们可以直接在model上来绑定我们的事件

model.onDidChangeContent((event)=>{
...
})

Model最后也需要我们销毁,这里分两种情况,假如是通过createModel创建的Model,那么我们需要手动销毁,但是如果是monaco默认创建的,则不需要,在调用实例的销毁方法时,会顺带销毁默认创建的Model。

model.dispose();

除了编辑事件之外,Model还有许多其他事件

例如:

onDidChangeOptions ****配置改变事件

onDidChangeLanguage 语言改变事件

在简单的场景下,Model的存在可能使得我们使用起来比较繁琐,但是,在复杂场景下,model可以极大的简化代码复杂性。

设想一下我们有5个tab,每个tab都是一个编辑器,每个编辑器都有各自的语言,内容和标注信息,如果没有Model,我们需要保存每个tab的语言,内容等信息,在切换到对应tab时再将这些信息初始化到编辑器上,但是利用Model,我们不需要额外去保存每个tab的编辑信息,只需要保存每个tab的Model,然后将Model传给编辑器进行初始化即可。