【CodeMirror】如何构建属于自己的线上编辑器 - 基础使用和使用详解

1,255 阅读3分钟

背景

随着技术的快速迭代和发展,我们可以发现,代码的编辑和运行已经可以脱离我们的本地环境转而在浏览器上线上完成编辑。如codesandbox、codepen产品。也包含了最初在学习JavaScript中使用到菜鸟教程中的在线运行。而这些产品对于code都有良好的语法高亮、代码提示hint、代码lint等功能。

目的

  • 基础使用。通过codemirror构建一个运行在浏览器的编辑器,支持mysql语法高亮。
  • 进阶使用。支持自定义语法高亮、支持自定义代码提示hint、代码lint等功能。

构建方式

目前codemirror已经更新到6.x的版本,但是我们这里使用5.x的版本。

image.png CodeMirror在市面上存在诸多以Vue、React二次封装的前端组建。所以,我们可以按照应用的前端开发框架来选择不同的CodeMirror组件。在这里,我们使用react-codemirror2

初始化

import { UnControlled as CodeMirrorReact } from "react-codemirror2";
import "codemirror/lib/codemirror.css";
import "codemirror/mode/sql/sql";
import "codemirror/theme/idea.css";

export default function HomePage() {
  return (
    <div>
      <CodeMirrorReact
        value="select col1, col2 from table1;"
        options={{
          mode: "text/x-sql",
          lineNumbers: true,
          theme: "idea",
          tabSize: 2,
        }}
        onChange={(editor, data, value) => {}}
      />
    </div>
  );
}

我们可以打开浏览器看到,sql语句可以具备关键字的高亮功能。

image.png

API解析

CodeMirror React组件引入

现在我们开始逐个代码解析。

import { UnControlled as CodeMirrorReact } from "react-codemirror2";

首先,我们引入了react-codemirror2的组件包,其中这个包里包含了两种组件。UnControlled / Controlled。

react-codemirror2 ships with the notion of an uncontrolled and controlled component. UnControlled consists of a simple wrapper largely powered by the inner workings of codemirror itself, while Controlled will demand state management from the user, preventing codemirror changes unless properly handled via value. The latter will offer more control and likely be more appropriate with redux heavy apps.

大致意思就是UnControlled仅仅只是对CodeMirror进行简单的封装,所以自由度比较高一点。而Controlled相对于UnControlled来说,如果需要进行一定的更改,需要值的校验通过。 两者的类型定义如下:

export interface IControlledCodeMirror extends ICodeMirror {
    onBeforeChange: (editor: codemirror.Editor, data: codemirror.EditorChange, value: string) => void;
    value: string;
}
export interface IUnControlledCodeMirror extends ICodeMirror {
    detach?: boolean;
    editorDidAttach?: (editor: codemirror.Editor) => void;
    editorDidDetach?: (editor: codemirror.Editor) => void;
    onBeforeChange?: (editor: codemirror.Editor, data: codemirror.EditorChange, value: string, next: () => void) => void;
    value?: string;
}
export declare class Controlled extends React.Component<IControlledCodeMirror, any> {
}
export declare class UnControlled extends React.Component<IUnControlledCodeMirror, any> {
}

CodeMirror默认样式和主题样式的引入

import "codemirror/lib/codemirror.css";
import "codemirror/mode/sql/sql";
import "codemirror/theme/idea.css";

首先项目会引入codemirror的默认样式文件codemirror.css。这个样式文件主要支撑这codemirror基本的外观样式和编辑器内部默认的高亮样式。

其次我们引入了mode文件。mode的中文含义有风格、样式的意思。所以我们知道,我们引入了sql风格的文件。但是这个风格不是针对于样式来说的,而是对于你所编辑的内容来说的。

image.png 因为mode会告诉你,为什么select和from会高亮显示。 我们可以打开mode文件,暂时先忽略具体的实现,可以看到如下两行代码。

image.png

CodeMirror.defineMode("sql", function(config, parserConfig){})
CodeMirror.defineMIME("text/x-sql",{})
  • CodeMirror.defineMode的作用是向CodeMirror注册Mode。
  • CodeMirror.defineMIME的作用是Mode与MIME配置相关联。而这么做的目的是为了后续扩展或覆盖Mode时更加的方便。

当我们使用时,我们只需要指定实例化的mode为‘text/x-sql’即可

最后,我们引入了idea.css文件。这个文件的父级目录为theme,我们就可以知道目录下所有的文件是为了配置好编辑器的扩展主题使用。当我们在实例化编辑器时,我们配置了theme: 'idea'时,CodeMirror会获取这个配置项,将起样式赋值给外层的className。

// /src/edit/utils.js
export function themeChanged(cm) {
  cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") +
    cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-")
  clearCaches(cm)
}

项目实例化

react-codemirror2本身在使用组件时,会在componentDidMount中调用cm(this.ref, this.props.options)去实例化codemirror。同时会把props的options字段传递给cm实例化的第二个参数。

// /node_modules/react-codemirror2/index.js
UnControlled.prototype.componentDidMount = function() {
    var _this = this;

    if (SERVER_RENDERED) return;
    this.detached = this.props.detach === true;

    if (this.props.defineMode) {
      if (this.props.defineMode.name && this.props.defineMode.fn) {
        cm.defineMode(this.props.defineMode.name, this.props.defineMode.fn);
      }
    }

    this.editor = cm(this.ref, this.props.options);
    this.shared = new Shared(this.editor, this.props);
    this.editor.on('beforeChange', function(cm, data) {
      if (_this.props.onBeforeChange) {
        _this.props.onBeforeChange(_this.editor, data, _this.editor.getValue(), _this.onBeforeChangeCb);
      }
    });
    this.editor.on('change', function(cm, data) {
      if (!_this.mounted || !_this.props.onChange) {
        return;
      }

      if (_this.props.onBeforeChange) {
        if (_this.continueChange) {
          _this.props.onChange(_this.editor, data, _this.editor.getValue());
        }
      } else {
        _this.props.onChange(_this.editor, data, _this.editor.getValue());
      }
    });
    this.hydrate(this.props);
    this.shared.apply(this.props);
    this.applied = true;
    this.mounted = true;
    this.shared.wire(this.props);
    this.editor.getDoc().clearHistory();

    if (this.props.editorDidMount) {
      this.props.editorDidMount(this.editor, this.editor.getValue(), this.initCb);
    }
  };

总结

CodeMirror是一个强大的线上编辑器实现插件,太具备强大的自定义功能。

木更 2022/07/30