常见web代码编辑器框架

在接到在线代码编辑器的需求之后,就开始了相关的调研,以下目前社区最受欢迎的三个开源方案:

这里就不展开比较三者的优劣,具体这篇文章:「译」Ace,CodeMirror 和 Monaco:Web 代码编辑器的对比,已经很详细的对这三大框架做了比较,大家可以结合实际业务场景,做出自己的技术选型。

最终,我们团队决定采用 Codemirror

Codemirror踩坑

在决定采用Codemirror之后,立即就开始尝试去写一些demo,但是由于前期不了解Codemirror版本较多,且基于react,vue等框架,社区有很多不同的实现库,那到底用哪个版本,用哪个库呢?这中间也踩了不少坑,在这里给大家梳理一下,让有需求的小伙伴儿们能够少踩一些坑。

首先,说明一下Codemirror的版本,目前主要是用的是两个版本:

Codemirror6和Codemirror5的主要区别体现在以下几个方面:

  • 架构:CM6是对CM5进行了全面重写和重构的版本,采用了新的架构。CM6的设计目标是提供更好的可扩展性、模块化和定制性,以适应不同的编辑器需求。
  • 模块化:CM6引入了模块化的概念,使得编辑器的功能可以以更灵活的方式组织和扩展。它提供了一套核心模块,以及可选的插件和扩展模块,使开发人员能够根据自己的需求选择和组合功能。
  • 插件系统:CM6的插件系统更加强大和灵活。它采用了新的基于状态和触发器的机制,使得插件能够更好地与编辑器交互并响应变化。这使得开发人员能够创建更复杂和定制化的编辑器功能。
  • 渲染方式:CM6使用新的渲染引擎,采用了虚拟DOM的概念,以提高性能和响应能力。它还支持可选的受限制的线性渲染模式,可以在大型文档上提供更好的性能

因此,2023年的我们,推荐大家使用Codemirror6

注意❗

注意,大家在网上搜到的大多数关于codemirror的文章依然是v5的版本,想要直接copy代码调试的小伙伴儿们要注意了哦,如何判断呢?典型的一个判断标准的就是V6的版本采用的ESM模块化的方式去引入,而V5依然是传统的非模块的方式,大家可以根据这去判断搜到的代码是采用的哪个版本。

确定使用V6的版本之后,接下来就是要考虑,社区是否有现成的基于React,Vue封装好的Codemirror组件呢?当然是有的:

  • uiwjs/react-codemirror
    • github:github.com/uiwjs/react…
    • 实现:基于codemirror6以及react16.8+实现
    • 优缺点:优点就是简单易用,直接引入react组件即可,常见的语言设置,主题设置等功能都支持,但是相对于原生的codemirror来说,功能还是有限,如果需要对codemirror进行更复杂的自定义,建议使用原生的codemirror,基于原生的codemirror,自己封装相应的react组件。
  • react-codemirror2
    • github:github.com/scniro/reac…
    • 实现:基于codemirror5以及react16实现
    • 优缺点:相对比较老了,最后一次维护也是在3年前了,在如今较新版本的react中可能会有问题,例如:react18+ 中初始化会报错。

因此,如果只是简单场景的在线编辑器uiwjs/react-codemirror 这个库基本可以满足条件,但是如果想自定义相对比较复杂的场景,建议还是基于原生的codemirror6去自己封装相应的组件。

确定使用原生的codemirror6之后,接下来我们就看看其基本使用啦。

常见场景

基本使用

Codemirror6采用现代的JavaScript模块化和构建工具,并通过一组模块化的包来实现不同的功能。其中,@codemirror/view@codemirror/state是CodeMirror 6(以下简称CM6)库的核心包之一,用于构建可扩展的代码编辑器。

  • @codemirror/view:这个包提供了构建编辑器用户界面(UI) 的核心功能。它定义了编辑器的视图组件、输入处理、渲染逻辑等。@codemirror/view中的主要类是EditorView,它代表了一个CM6编辑器实例的视图。你可以使用@codemirror/view来创建、控制和自定义编辑器的外观和交互行为。
  • @codemirror/state:这个包定义了编辑器的状态管理和文档模型。它提供了EditorState类,用于表示编辑器的完整状态,包括文本内容、光标位置、选中范围、编辑器的配置选项等。通过@codemirror/state,你可以创建和管理编辑器的状态,执行编辑操作、查询编辑器状态,以及响应状态的变化。

以下就是一个基于react最小版本的codemirror在线编辑器的实现:

// myCodemirror.tsx
import React, { useRef, useEffect } from 'react';
import { basicSetup } from 'codemirror';
import { EditorView, keymap } from '@codemirror/view';
import { EditorState } from '@codemirror/state';
 
const CodeMirror: React.FC = () => {
  const editorRef = useRef(null);
 
  useEffect(() => {
    // 初始化CodeMirror编辑器
    const state = EditorState.create({
      doc: 'hello world!',
      extensions: [
        basicSetup,
      ],
    });
    const editor = new EditorView({
      state,
      parent: editorRef.current,
    });
    return () => {
      editor.destroy(); // 注意:此后此处要随组件销毁
    };
  }, []);
  return <div ref={editorRef}></div>;
};
 
export default CodeMirror;

语言设置

最小版本的codemirror代码编辑器实现以后,接下来,我们一般都需要进一步指定对应的语言,通过 codemirror6 官方github可以看到,其不同语言都有一个对应的包来维护,我们在项目中,只需要引入相应的包,并且在extensions配置即可。

例如:我们要引入sql语言,首先安装依赖包:

pnpm install @codemirror/lang-sql --save

然后,在基于我们刚刚的最小编辑器代码的基础上,引入该依赖包,就以下两步:

import { sql } from '@codemirror/lang-sql'; // 引入语言包
const state = EditorState.create({
  doc: 'hello world!',
  extensions: [
    basicSetup,
    sql(), // 在extensions中配置语言
  ],
});

完整代码如下:

// myCodemirror.tsx
import React, { useRef, useEffect } from 'react';
import { basicSetup } from 'codemirror';
import { EditorView, keymap } from '@codemirror/view';
import { EditorState } from '@codemirror/state';
import { sql } from '@codemirror/lang-sql'; // 引入语言包
 
const CodeMirror: React.FC = () => {
  const editorRef = useRef(null);
 
  useEffect(() => {
    const state = EditorState.create({
      doc: 'hello world!',
      extensions: [
        basicSetup,
        sql(), // 在extensions中配置语言
      ],
    });
    const editor = new EditorView({
      state,
      parent: editorRef.current,
    });
    return () => {
      editor.destroy();
    };
  }, []);
  return <div ref={editorRef}></div>;
};
 
export default CodeMirror;

主题设置

语言设置完以后,接下来就是设置一个自己喜欢的主题啦,默认情况下,codemirror6 只包含一个主题:@codemirror/theme-one-dark, 如需要使用,直接在项目中安装引入即可

// 安装主题
pnpm install @codemirror/theme-one-dark
// import引入,然后在extensions属性中配置即可。
import { oneDark } from '@codemirror/theme-one-dark'
const state = EditorState.create({
  doc: 'hello world!',
  extensions: [
    //...
    oneDark
  ],
});

当然,如果还想引用其他主题,也可以参考如下两个网站:

里面有很多好看的主题,效果如下: 

使用配置方法都类似,大家选择自己喜欢的主题即可。

监听doc变化

如何监听代码变化呢?在extensions中配置 EditorView.updateListener即可。代码如下:

const state = EditorState.create({
  doc: 'select * from table',
  extensions: [
    // ...
    EditorView.updateListener.of((v) => {
	// 文档更新后再触发
	if(v.docChanged) {
      console.log(v.state.doc.toString()) //监测得到的最新代码 
	}
   }),
  ],
});

自定义autocomplete

在实际使用中,经常需要自定义一些关键词,例如:公司内部常用的表,字段等信息,我们需要配置到autocompletion中,这样我们输入的时候,就可以快速提示,快捷输入。

在codemirror中也提供了相应的功能,核心是依赖@codemirror/autocomplete这个库。

  1. 首先是安装依赖
pnpm i @codemirror/autocomplete --save
  1. 引入代码,及其配置
import { autocompletion } from '@codemirror/autocomplete';
 
// 自定义关键词函数
function myCompletions(context: CompletionContext) {
  let word = context.matchBefore(/\w*/)
  if (word.from === word.to && !context.explicit)
    return null
  return {
    from: word.from,
    options: [
      {label: "match", type: "keyword"},
      {label: "hello", type: "variable", info: "(World)"},
      {label: "magic", type: "text", apply: "⠁⭒*.✩.*⭒⠁", detail: "macro"}
    ]
  }
}
/// 在extensions中,引入myCompletions
const state = EditorState.create({
  doc,
  extensions: [
    // ...
    autocompletion({ override: [myCompletions]})
  ],
});

实现如下如下:

注意❗

注意:以上这种方式,自定义的自动提示会直接覆盖sql语言自带的自动提示,显然我们只是想拓展,并不是直接覆盖,这部分如何实现还再进一步调研中,了解的小伙伴儿欢迎分享。

完整代码

以下是用react封装的一个codemirror组件,包含了基础功能语言设置主题设置doc变化监听等功能。有需要的小伙伴儿可以直接复制到一个.tsx文件中,在本地查看效果。

import React, { useRef, useEffect, useState } from 'react';
import { basicSetup } from 'codemirror';
import { EditorView, keymap } from '@codemirror/view';
import { EditorState } from '@codemirror/state';
import { sql } from '@codemirror/lang-sql';
import { ayuLight } from 'thememirror';
 
const sqlLang = sql();
 
const CodeMirror: React.FC = () => {
  const editorRef = useRef(null);
  const [doc, setDock] = useState();
 
  useEffect(() => {
    // 初始化CodeMirror编辑器
    const state = EditorState.create({
      doc,
      extensions: [
        basicSetup,
        sqlLang,
        ayuLight,
        EditorView.updateListener.of((v) => {
          if (v.docChanged) {
            setDock(v.state.doc.toString());
          }
       }),
      ],
    });
    const editor = new EditorView({
      state,
      parent: editorRef.current,
    });
 
    return () => {
      editor.destroy(); // 注意:此后此处要随组件销毁
    };
  }, []);
  return <div ref={editorRef}></div>;
};
 
export default CodeMirror;

效果如下:

后续迭代过程中,有新的一些功能实现点,或者坑都会在这里陆续更新和完善。

总结

在整个过程中,主要就是前期对codemirror的版本,以及社区基于react,vue等有很多不同的封装库,有点混乱,不知道该选择哪一个,这篇文章为大家整体梳理了一下,同时,把codemirror6的常用功能进行了整理,希望可以让需要的小伙伴儿可以少走一些坑,快速接入项目中。