标签: codemirror

  • 浅尝 CodeMirror@6

    浅尝 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。

  • 使用 Chrome extension 操作 CodeMirror

    使用 Chrome extension 操作 CodeMirror

    我们知道,浏览器扩展(Chrome extension)可以通过 content script 在目标网页里执行代码。它和目标页面的 JS 处于不同的沙箱,但是共用一套 DOM。所以,content script 无法访问目标页面的 JS 变量,但是可以修改目标页面的 DOM、侦听 DOM 上的事件、或者触发 DOM 事件。

    在这种情况下,要操作 CodeMirror 的时候会遇到一些问题。为了性能考虑,CodeMirror 不会把所有代码都渲染出来,而是会按需渲染。比如代码总共 1k 行,但是当前窗口里只能看到 40 行,那么 CodeMirror 就会渲染当前的 40 行,和前后大概各 20 行(方便快速滚屏),总计约 80 行。

    换言之,如果无法接触到目标页面的 JS 环境,单纯从 DOM 里无法获取到全部代码,也很难修改 CM 里的代码。于是我们就需要一些特别的技巧——这些技巧在操作其它类似的类库时也会用到,所以写篇博客分享下。

    解决方案:向目标网页注入 <script>

    此时,我们可以通过创建 <script> 的方式向目标网页注入一段 JS。这个 JS 的执行环境也是目标网页,所以可以访问到目标网页 环境下的 JS 变量。

    CodeMirror 会把实例添加到容器节点上,所以我们可以先找到容器节点,即 div.CodeMirror;然后访问它的 CodeMirror 属性找到 CM 实例;最后进行操作。

    为了方便 content script 和 CodeMirror 交互,我们必须搭建一个桥梁,也就是下面这段代码:

    const cms = document.getElementsByClassName('CodeMirror')
    for (const dom of cms) {
      // 这个 textarea 被 CodeMirror 视图所取代,是隐藏的。我们利用它传递代码
      const textarea = dom.previousElementSibling;
      const cm = dom.CodeMirror;
      // 给 CodeMirror 添加清空功能,可以通过在 dom 上广播事件触发
      dom.addEventListener('clear', () => {
        cm.setValue('');
      });
      // `changes` 是 CodeMirror 的事件,在代码改变时触发,这里我们通过侦听它,并且将代码同步到 `textarea` 的方式,方便 content script 访问
      cm.on('changes', cm => {
        if (texture.value === cm.getValue()) {
          return;
        }
        textarea.value = cm.getValue();
      });
    
      // 为初始化
      textarea.value = cm.getValue();
    }

    因为我执行 content script 的时机总在页面完成初始化之后,此时所有 CodeMirror 都已就位。如果你要在别的时间执行,可以做一些调整。

  • CodeMirror 笔记

    CodeMirror 笔记

    这篇文章用来记录使用 CodeMirror 时的一些心得。

    (更多…)