厂里的项目需要在线代码编辑器,一开始我想试试 Monaco editor,VS Code 就用的这个,基本可以认为其功能、设计、性能都是上上之选。可惜它太重了,初始包就要 3M+,用在单机软件里问题不大,放在网页里就很不理想。于是用回 CodeMirror。不过毕竟是新项目,我不想继续使用 v5,便开始学习使用 CodeMirror@6。
0. CodeMirror@6
CodeMirror@6 其实发布很长时间了,我以前做 Showman 的时候就看过,不过当时我们已经在 CodeMirror@5 里投入很多,Showman 本身时间紧任务重,就没想迁移。
CodeMirror@6 是作者完全重写的,整体架构发生了非常大的变化,使用方式与上一个版本完全不同。所以我要写篇博客记录分享一下。
新版本着重提升了可用性与触屏支持,提供更好的内容解析功能,并且提供了现代化编程接口,比如模块管理、TypeScript 等。虽然整体还处于 beta 阶段,不敢保证日后没有破坏性变更,但还是新开项目的上乘之选。
1. 旧版本迁移
如果有上个版本的使用经验,那么最好先看这篇文档:Migration Guide。大部分使用问题都能迎刃而解。
以前创建编辑器只需要 codemirror.fromTextarea(element)
即可,现在麻烦了很多,因为整个包被拆成数个组件:
- view 即网页中的视图
- state 即代码解析结果
- 插件,包括快捷键、语言 mode、丰富功能,用来处理所有非核心逻辑
这样做的好处很明显,每个组件可以独立维护、独立使用,如果只需要其中一两项功能,就不用把所有代码都加载进来,性能会好很多。
在新版本中初始化编辑器一般这样做:
import { basicSetup, EditorState, EditorView } from '@codemirror/basic-setup';
import { css } from '@codemirror/lang-css';
import { ViewUpdate } from '@codemirror/view';
const editor = new EditorView({
state: EditorState.create({
doc: code,
extensions: [
// basicSetup 是一套插件集合,包含了很多常用插件
basicSetup,
// 这里只使用 css 解析器
css(),
// 新版本一切皆插件,所以实时侦听数据变化也要通过写插件实现
EditorView.updateListener.of((v: ViewUpdate) => {
this.localValue = v.state.doc.toString();
this.$emit('input', this.localValue);
}),
],
}),
parent: this.$refs.editor as HTMLDivElement,
});
2. 更新代码
新版本为了支持编辑代码的复杂需求,把所有变更都封装成了 transactions,通过 dispatch 告知 view 更新视图。所以修改代码就从 .setValue(code)
变成下面这种样子:
this.editor.dispatch({
changes: { from: 0, to: this.editor.state.doc.length, insert: code },
});
3. 高亮错误代码
新版本高亮错误代码会比较麻烦。以前直接 .addLineClass()
就可以了,现在则要先生成标记,然后再把标记添加到视图中。不过也带来一个好处:以前标记了错误行,用户编辑后,错误还在那里,很难区分,一般都是直接清除。现在因为状态更新更全面了,所以标记也可以随之更新。
我目前的实现方式如下,不是很理想,先分享出来吧:
const errorMarkTheme = EditorView.baseTheme({
'.cm-error-mark': {
boxShadow: '-2px 0 red',
},
});
const errorMark = Decoration.mark({
class: 'cm-error-mark',
});
const addErrorMarks: StateEffectType<any> = StateEffect.define<{ from: number; to: number }>();
const markField = StateField.define<DecorationSet>({
create() {
return Decoration.none;
},
update(marks, tr) {
marks = marks.map(tr.changes);
for (const effect of tr.effects) {
if (effect.is(addErrorMarks)) {
marks = marks.update({
add: [errorMark.range(effect.value.from, effect.value.to)],
});
}
}
return marks;
},
provide: (field) => EditorView.decorations.from(field),
});
export default {
highlightError(line: number, pos: number) {
const code = this.localValue;
// 必须用起始位置标记
const lines = code.split('\n');
const to = lines.slice(0, line - 1).reduce((total, line) => total + line.length + 1, 0) + pos;
const effects = [addErrorMarks.of({ from: to - 1, to })];
if (!this.editor.state.field(markField, false)) {
effects.push(StateEffect.appendConfig.of([markField, errorMarkTheme]));
}
this.editor.dispatch({ effects });
}
}
后记
后来我得知,产品里其实已经集成了 Ace Editor,我一阵紧张,毕竟引入多个同类型的仓库基本上可以认为是错误操作。赶快跑去看了眼 Ace Editor,以及一些对比文章,应该说还好,无论是功能、架构,还是受欢迎程度,应该都比不上 codemirror,尤其是生态很单薄,所以以后会继续深耕 codemirror。
欢迎吐槽,共同进步