浅尝 CodeMirror@6

厂里的项目需要在线代码编辑器,一开始我想试试 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) 即可,现在麻烦了很多,因为整个包被拆成数个组件:

  1. view 即网页中的视图
  2. state 即代码解析结果
  3. 插件,包括快捷键、语言 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。

如果您觉得文章内容对您有用,不妨支持我创作更多有价值的分享:


已发布

分类

来自

标签:

评论

《“浅尝 CodeMirror@6”》 有 1 条评论

  1. kortin 的头像
    kortin

    我们项目里也是用的ace editor,功能到挺强大,但文档极其难看

欢迎吐槽,共同进步

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据