开始
技术栈:react:^16.14.0, typescript scss codemirror 6.x
目标
实现一个编辑器组件,支持JavaScript,json,html,Sql,以及自定义的公式变量;
基础知识
文档地址:codemirror.net/
优势
Codemirror6和Codemirror5的主要区别体现在以下几个方面:
- 架构:CM6是对CM5进行了全面重写和重构的版本,采用了新的架构。CM6的设计目标是提供更好的可扩展性、模块化和定制性,以适应不同的编辑器需求。
- 模块化:CM6引入了模块化的概念,使得编辑器的功能可以以更灵活的方式组织和扩展。它提供了一套核心模块,以及可选的插件和扩展模块,使开发人员能够根据自己的需求选择和组合功能。
- 插件系统:CM6的插件系统更加强大和灵活。它采用了新的基于状态和触发器的机制,使得插件能够更好地与编辑器交互并响应变化。这使得开发人员能够创建更复杂和定制化的编辑器功能。
- 渲染方式:CM6使用新的渲染引擎,采用了虚拟DOM的概念,以提高性能和响应能力。它还支持可选的受限制的线性渲染模式,可以在大型文档上提供更好的性能。
周边
react-codemirror:github 1.4k的star;也是基于codemirror 6.x实现的;只要进行一些配置就可实现;
import React from 'react';
import CodeMirror from '@uiw/react-codemirror';
import { javascript } from '@codemirror/lang-javascript';
function App() {
const [value, setValue] = React.useState("console.log('hello world!');");
const onChange = React.useCallback((val, viewUpdate) => {
console.log('val:', val);
setValue(val);
}, []);
return <CodeMirror value={value} height="200px" extensions={[javascript({ jsx: true })]} onChange={onChange} />;
}
export default App;
实现一个基础编辑器
import { basicSetup, EditorView } from 'codemirror';
import { EditorState } from "@codemirror/state";
// 状态管理
let startState = EditorState.create({ doc: "Hello World", extensions: [ basicSetup ], });
// 视图层
let view = new EditorView({ state: startState, parent: document.body })
basicSetup内包含了一系列常用的插件和设置,基本上使用它就可以设置好一个基础的编辑器了。
知识点之状态管理
@codemirror/state:这个包定义了编辑器的状态管理和文档模型。它提供了EditorState类,用于表示编辑器的完整状态,包括文本内容、光标位置、选中范围、编辑器的配置选项等。通过@codemirror/state,你可以创建和管理编辑器的状态,执行编辑操作、查询编辑器状态,以及响应状态的变化。
const stateStart = EditorState.create({
doc: defaultValue, // 文档值
extensions: [ // 扩展值
...extensions(props, callback, handleError),
...(extensionsProps || []),
],
});
这里的extensions着重说一下,它好比电脑的各个扩展插口,用户可以根据自己的需求,加不同的扩展,创造不同的编辑器,这也是codemirror升级之后的一大特色;这里列举几个可扩展的功能,让你更好的理解它:
- 快捷键,比如Tab键的效果;
- 主题色的个性化配置;
- mode,你要使用的语言,比如我要开发一个JavaScript编辑器,可以安装对应的包
@codemirror/lang-javascript,进行扩展; - 代码高亮的配置;
- 增加装饰器;
- 当然也可以去监听视图层的变化去执行自己的逻辑;
EditorView.updateListener.of(...) - placeholder的设置;
- 对语言包的汉化;
- 语法检查;
- ...
import { json as jsonLang } from "@codemirror/lang-json";
import { baseTheme } from "./plugin/base-theme";
import { LightHighlight } from "./plugin/high-lighting";
// 此包导出通用编辑命令的集合,以及其中许多命令的键绑定
import { indentWithTab, defaultKeymap } from "@codemirror/commands";
const stateStart = EditorState.create({
doc: defaultValue, // 文档值
extensions: [
baseTheme,
LightHighlight,
keymap.of([...defaultKeymap, indentWithTab]),
jsonLang(),
EditorView.editable.of(editable),
// 最小高度
EditorView.theme({
".cm-content": minHeight ? {
minHeight: `${minHeight}px`
} : {},
"&": height ? {
height: `${height}px`
} : {}
}),
placeholder("请输入json");
// 监听doc变化
EditorView.updateListener.of((v: ViewUpdate) => {
let error = {};
// 遍历编辑器中的错误提示;
forEachDiagnostic(v.state, (d: Diagnostic) => {
error = d
});
handleError(error);
if (v.docChanged && trigger !== "blur") {
callback(v);
}
})
],
});
知识点之视图层
核心包:
@codemirror/view,此提供了构建编辑器用户界面(UI)的核心功能。它定义了编辑器的视图组件、输入处理、渲染逻辑等。@codemirror/view中的主要类是EditorView,它代表了一个codemirror6编辑器实例的视图。你可以使用@codemirror/view来创建、控制和自定义编辑器的外观和交互行为。
import { EditorView } from "@codemirror/view";
// 创建实例
editorView.current = new EditorView({
state: state, // 数据状态管理;
parent: editorRef.current || document.body, // 要挂载的容器;
});
这里可以通过实例的一些方法,实现一些逻辑功能;例如 dispatch update setState destroy
-
view.dispatch 的一个用法介绍; 用来插入一段值到编辑器中;
cosnt text = '插入的值' view.dispatch({ changes: { from: range.from, to: range.to, insert: text, }, selection: { anchor: range.from + text.length, } }) -
view.destroy 在组件生命周期结束之后一定要记得销毁;
关于lint
lint的作用就是检测诊断代码的正确性, 比较关注的一个字段就是Diagnostic,它携带的信息就有哪行哪列什么错误的信息;以及对于错误纠正的action等;
当你有一个语法检测库的时候lang-lint,可以直接使用linter方法,在扩展extensions中使用;
import { json as jsonLang, jsonParseLinter } from "@codemirror/lang-json";
...
extensions: [
jsonLang(),
// 错误语法检测提示
linter(jsonParseLinter()),
]
亦或者自定义语法检测,定一个文件/plugin/custom-linter,这里举例自定义函数的语法检测;
export const customLinter = (functions) => {
return linter((view) => {
let diagnostics: Diagnostic[] = [];
const lines: number = view.state.doc.lines;
for (let i = 0; i < lines; i++) {
const lineText = line.text.replace(/\s+/g, "");
// 遍历所有的补全项
if (lineText) {
const isSome = functions.some((it) => lineText.includes(it.label))
if (!isSome) {
diagnostics.push({
from: line.from,
to: line.to,
severity: "error",
message: `语法错误或未定义`,
actions: [];
});
}
}
}
return diagnostics;
})
}
在extensions中使用;
...
extensions:[
customLinter(functions),
]
关于插件ViewPlugin
ViewPlugin将有状态值与视图相关联。他们可以影响内容的绘制方式,并且当视图层改变也能收到change通知。
这里使用formClass为一个类创建一个插件,该类的构造函数将单个编辑器视图作为参数;
- 参数1:需要一个视图类
- 参数2:插件的参数
ViewPlugin.fromClass(
class {
keywords: DecorationSet;
constructor(view: EditorView) {
// 通过装饰器创建 deco (装饰范围的集合)
this.keywords = keywrodsMatcher.createDeco(view);
}
update(update: ViewUpdate) {
this.function = keywrodsMatcher.updateDeco(update, this.keywords);
}
},
{
decorations: (instance: any) => {
return instance.keywords;
},
// 指定插件在添加到编辑器配置时提供其他扩展。
provide: (plugin: ViewPlugin<any>) =>
EditorView.atomicRanges.of((view) => {
return view.plugin(plugin)?.keywords || Decoration.none;
}),
}
);
decorations 是一个装饰器,返回一个装饰器的实例,装饰器的使用下文介绍;provide 指定插件在添加到编辑器配置时提供其他扩展, EditorView.atomicRanges.of的作用是进行光标移动和删除;
关于装饰器
MatchDecorator,它的理解就是,通过正则表达式对匹配的text进行修饰;主要看它的一些配置了
下面创建一个对关键字的修饰;
class keyWordsWidget extends WidgetType {
text: string;
constructor(text: string) {
super();
this.text = text;
}
// 将此实例与相同类型的另一个实例进行比较, 用于是否dom重绘;
eq(other: FunctionWidget) {
return this.text == other.text;
}
// 为此小组件实例构建 DOM 结构,这里可以设置关键字的样式;return 为一个dom
toDOM() {
const elt = document.createElement("span");
const textCode = document.createElement("span");
textCode.style.cssText = `
color: red;
font-size: 14px;
background: #f8d7da;
user-select: none;
`;
textCode.textContent = this.text;
elt.appendChild(textCode);
return elt;
}
// 配置编辑器应忽略小组件内哪些类型的事件。默认设置是忽略所有事件
ignoreEvent() {
return true;
}
}
// 实现对关键字样式的修饰
const keywrodsMatcher = new MatchDecorator({
regexp: new RegExp(keywords.join("|"), "g"),
decoration: (match) => {
const keyword = match[1];
return Decoration.replace({
// 创建一个小部件装饰,在给定位置显示 DOM 元素。
widget: new keyWordsWidget(`${keyword}`),
});
}
});
这里多余使用了WidgetType,主要用于描述widget。使用这样的描述对象可以延迟创建小部件的 DOM 结构。
主题
import { EditorView } from "@codemirror/view";
// 主题配置
export const baseTheme: any = EditorView.baseTheme({
'&': {
color: '#586e75',
backgroundColor: '#fff',
caretColor: '#657b83',
fontSize: "16px"
},
".cm-content": {
"white-space": "pre-wrap"
},
'.cm-tooltip': {
border: '1px solid #ccc',
backgroundColor: '#fff',
padding: '5px',
},
'&.cm-focused .cm-cursor': { borderLeftColor: '#657b83' },
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, ::selection": { backgroundColor: "#bfe3ff", color: "#4284d3"
},
...
})
代码高亮
import { syntaxHighlighting, HighlightStyle, defaultHighlightStyle } from '@codemirror/language';
import { tags as t } from '@codemirror/highlight';
export const LightHighlight = syntaxHighlighting(HighlightStyle.define([
// const, let, function, if
{ tag: t.keyword, color: '#859900', fontWeight: "bold" },
// document
{ tag: [t.name, t.deleted, t.character, t.macroName], color: '#657b83' },
// getElementById
{ tag: [t.propertyName], color: '#268BD2' },
{ tag: t.comment, color: "#93A1A1", fontStyle: "italic"},
// "string"
{ tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: '#2AA198' },
// render
{ tag: [t.function(t.variableName), t.labelName], color: '#268DCC' },
// ???
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: '#CB4B16' },
// btn, count, fn render()
{ tag: [t.definition(t.name), t.separator], color: '#657b83' },
{ tag: [t.className], color: '#268BD2' },
{ tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: '#D33682' },
{ tag: [t.typeName], color: '#859900', fontStyle: '' },
{ tag: [t.operator, t.operatorKeyword], color: '#859900' },
{ tag: [t.url, t.escape, t.regexp, t.link], color: '#D30102' },
{ tag: [t.meta, t.comment], color: '#93A1A1' },
{ tag: t.strong, fontWeight: 'bold' },
{ tag: t.emphasis, fontStyle: 'italic' },
{ tag: t.link, textDecoration: 'underline' },
{ tag: t.heading, fontWeight: 'bold', color: '#268BD2' },
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: '#657b83' },
{ tag: t.invalid, color: '' }
] as any[]));