闲谈MonacoEditor-基本使⽤
什么是Monaco Editor?
微软之前有个项⽬叫做Monaco Workbench,后来这个项⽬变成了VSCode,⽽Monaco Editor(下⽂简称monaco)就是从这个项⽬中成长出来的⼀个web编辑器,他们很⼤⼀部分的代码(monaco-editor-core)都是共⽤的,所以monaco和VSCode在编辑代码,交互以及UI上⼏乎是⼀摸⼀样的,有点不同的是,两者的平台不⼀样,monaco基于浏览器,⽽VSCode基于,所以功能上VSCode更加健全,并且性能⽐较强⼤。
开始使⽤
本⽂采⽤的是webpack编译,所以以下都是基于webpack来说明。
基本功能
⾸先,我们需要安装monaco
npm install monaco-editor -S
然后在⾃⼰的⽂件中引⼊monaco,这⾥不需要全部引⼊,只需要引⼊⾃⼰需要使⽤的功能模块即可。
HTML
<div id="monaco">
</div>
JS
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js';
const monacoInstance=ElementById("monaco"),{
value:`console.log("hello,world")`,
language:"javascript"
})
monacoInstance.dispose();//使⽤完成销毁实例
我们设置了语⾔为javascript,界⾯是出来了,但是却发现没有语法⾼亮,输⼊命令发现其实根本没有javascript语⾔,只有⼀个最基础的plaintext。
所以我们还需要再定义⼀个javascript语⾔,但是定义⼀门语⾔并不是⼀件很容易的事情,幸好,monaco⾃⾝提供了许多种内置语⾔,我们只需要引⼊即可。
import 'monaco-editor/esm/vs/basic-languages/ibution';
引⼊完成,再次查看界⾯,发现已经有了语法⾼亮。
这时,我们可以尝试像使⽤VSCode⼀样使⽤monaco,按下ctrl+f来执⾏⽂本查,我们会发现出来的不是monaco的查控件,⽽是浏览器的,因此我们需要引⼊查控件。
import 'monaco-editor/esm/vs/editor/contrib/find/findController.js';
再次尝试查,出来的已经是monaco的查控件啦。
monaco还有许多这类控件,我们可以按需引⼊⾃⼰⽤到的。
不过有⼀个更加简便的⽅法,那就是直接引⼊main⽂件来代替api⽂件。
import * as monaco from 'monaco-editor/esm/vs/editor/editor.main.js';
点开⽂件,我们可以看到
editor.main.js
import '../language/ibution';
import '../language/ibution';
import '../language/ibution';
import '../language/ibution';
import '../ibution';
export * from './edcore.main';
采⽤这种⽅式引⼊的话,会⾃动带上所有的内置语⾔和控件,唯⼀的缺点就是包的体积过⼤。
⽬前为⽌,我们已经实现了⼀个可以输⼊,⾼亮,查的web编辑器,但是,和VSCode⽐较起来,还少了许多重要的功能,例如代码补全,错误提⽰以及快捷命令功能等。
进阶使⽤
⾸先,我们⾃⼰可以设想⼀下,假如要⾃⼰来实现代码补全以及错误提⽰,我们会怎么做?
第⼀,我们要解析输⼊的⽂本,这时,我们就需要写⼀个Parser。
第⼆,根据Parser解析的结果来调⽤monaco的标注接⼝来标注错误的代码从⽽实现错误提⽰功能
第三,根据Parser解析的结果信息,提供上下⽂相关的代码候选项来实现代码补全功能。
可以看出来,实现起来难度会很⼤,涉及到的点很多,不过,和语法⾼亮⼀样,monaco也帮助我们实现了这些功能,⽬前⽀持
html,css,ts/js,json四种语⾔,我们只需要引⼊即可。但是这边的引⼊可没有语法⾼亮那么简单。
Monaco的实现采⽤worker的⽅式,因为语法解析需要耗费⼤量时间,所以⽤worker来异步处理返回结果⽐较⾼效。我们使⽤的话需要两步。
1. 提供⼀个定义worker路径的全局变量
window.MonacoEnvironment = {
getWorkerUrl: function (moduleId, label) {
多文本编辑器editor什么意思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
entry: {
"main": solve(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',
},
好了,这边开始就是monaco的地狱模式,我们会遇到⾮常多的问题。
问题⼀. 我们的输出⼀般是加hash的,所以,输出的worker⽂件也会有对应的hash值后缀,例如typescript.worker.a23sf4asfqw.js,那么,第⼀步中的getWorkerUrl中的配置(typescript.worker.js)就和他对不上了,导致查worker路径失败。
问题⼆. worker是运⾏在单独的线程中的,所以没有window变量,我们需要修改webpack的全局变量为self才可以。
问题3. 假如使⽤html-webpack-plugin插件,我们就要防⽌worker被直接引⼊html⽂件(因为worker也是单独的entry),因此还需要设置html-webpack-plugin的chunks。
....
不得不说monaco是⼀个很贴⼼的编辑器,他也帮我们解决了这⼀系列问题。解决我们问题的就是。
使⽤起来也⾮常简单
npm install monaco-editor-webpack-plugin -S
webpack配置
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
return {
...
plugins:[
new MonacoWebpackPlugin()
]
.
..
}
}
插件会帮我们做这么⼏件事
1. ⾃动注⼊getWorkerUrl全局变量
2. 处理worker的编译配置
3. ⾃动引⼊控件和语⾔包。
具体要引⼊哪些控件和语⾔包,我们可以通过配置languages和features来控制
new MonacoWebpackPlugin({
languages:["javascript","css","html","json"],
features:["coreCommands","find"]
})
缺省情况下,插件的会引⼊默认的语⾔包和控件(瞄了⼀下,应该是所有的控件和语⾔包),具体可以查看这个地址。
很好,现在我们以及完成了⼀个拥有⾃动补全,错误提⽰,以及语法⾼亮的编辑器。
事件绑定
在完成了编辑器本⾝的配置之后,我们可以开始进⾏下⼀步,绑定编辑事件。
const Value();
console.log(newValue)
})
monacoInstance是⼀个create⽅法返回的实例,他包含很多操作实例的⽅法。event是⼀个IModelContentChangedEvent对象,他包含了⾮常⾮常详细的变更信息,包括操作的类型(撤销、恢复,
还是⼿动输⼊引发的⽂本变更),变更的⽂本位置,变更的⽂本内容等。⽽我们要获取最新的值,则需要调⽤
细⼼的朋友应该还会发现⼀个很奇怪的地⽅,那就是我们绑定的⽅法⽤的是onDidChangeModelContent,⾥⾯有⼀个Model,这命名可是很讲究的,字⾯意思就是变更Model内容触发事件,从头到尾,我们都没看到有Model的存在,那么为什么这边是变更Model内容触发事件呢,难道我们操作的是Model?
是的,其实我们在编辑的时候,就是在Model上编辑,默认情况下,monaco会帮我⽣成⼀个Model,我们可以调⽤getModel打印⼀下
看⼀看api,我们可以发现,Model其实是⼀个保存编辑状态的对象,他⾥⾯含有语⾔信息,当前的编辑⽂本信息,标注信息等。所以我们可以缓存⼀下Model对象,在需要的时候直接调⽤setModel即可随时切换到之前的状态。或者也可以在初始化实例的时候设置⼀个Model。
const model=ateModel("hahahaha","javascript");
monacoInstance = acoDom.current, {
model:model
})
⽽且我们可以直接在model上来绑定我们的事件
...
})
Model最后也需要我们销毁,这⾥分两种情况,假如是通过createModel创建的Model,那么我们需要⼿动销毁,但是如果是monaco默认创建的,则不需要,在调⽤实例的销毁⽅法时,会顺带销毁默认创建的Model。
model.dispose();
除了编辑事件之外,Model还有许多其他事件
例如:
onDidChangeOptions 配置改变事件
onDidChangeLanguage 语⾔改变事件
...
在简单的场景下,Model的存在可能使得我们使⽤起来⽐较繁琐,但是,在复杂场景下,model可以极⼤的简化代码复杂性。
设想⼀下我们有5个tab,每个tab都是⼀个编辑器,每个编辑器都有各⾃的语⾔,内容和标注信息,如果没有Model,我们需要保存每个tab 的语⾔,内容等信息,在切换到对应tab时再将这些信息初始化到编辑器上,但是利⽤Model,我们不需要额外去保存每个tab的编辑信息,只需要保存每个tab的Model,然后将Model传给编辑器进⾏初始化即可。
结尾
本⽂重点在于介绍monaco的基本设计和使⽤,代码细节部分不是很详细,如果有疑问,可以翻阅。谢谢~
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论