使用 CodeMirror 6 和 React 构建一个支持只读模式的 JSON 编辑器

发布于:2025-03-04 ⋅ 阅读:(14) ⋅ 点赞:(0)

       在现代 Web 开发中,代码编辑器是一个常见的需求,尤其是在需要编辑 JSON、JavaScript 或其他结构化数据的场景中。本文将介绍如何使用 CodeMirror 6 和 React 构建一个功能丰富的 JSON 编辑器,并支持通过开关切换只读模式。

1. 项目依赖

首先,我们需要安装以下依赖:

  • CodeMirror 6:一个功能强大的代码编辑器框架。

  • Ant Design:用于构建 UI 组件,这里我们使用 Switch 组件来切换只读模式。

  • React:用于构建用户界面。

 安装依赖的命令如下:

npm install @codemirror/commands @codemirror/lang-json @codemirror/lint @codemirror/state @codemirror/view antd react react-dom

2. 实现 JSON 编辑器

2.1 初始化编辑器

在组件挂载时,我们使用 useEffect 钩子初始化 CodeMirror 编辑器实例,并将其渲染到指定的 DOM 容器中。

useEffect(() => {
    if (containerRef.current) {
        const e = new EditorView({
            doc: code,
            extensions: extensionConfig,
            parent: containerRef.current,
        });
        setEditor(e);
    }
}, []);
2.2 编辑器扩展配置

编辑器的扩展配置包括行号显示、linting 结果边栏、默认键盘快捷键、JSON 语言支持等。

const extensionConfig = [
    lineNumbers(),     // 显示行号
    lintGutter(),      // 显示 linting 结果的边栏
    keymap.of(defaultKeymap),      // 使用默认键盘快捷键
    json(),            // 启用 JSON 语言支持 json()
];
2.3 自定义 JSON 解析 Linter

为了确保用户输入的 JSON 代码是有效的,我们实现了一个自定义的 JSON 解析 linter。如果文档为空,则不返回诊断信息;否则,使用 CodeMirror 提供的 jsonParseLinter 进行检查。

const jsonParseLinterWithCheck = () => {
    return (view: EditorView) => {
        const doc = view.state.doc.toString();
        if (doc.trim().length === 0) {
            return [];
        }
        const diagnostics = jsonParseLinter()(view);
        return diagnostics;
    };
};
2.4 编辑器更新监听器

我们为编辑器添加了一个更新监听器,用于在用户输入内容时更新 React 组件的状态。

const updateListener = EditorView.updateListener.of((vu) => {
    if (vu.docChanged) {
        const currentDoc = vu.state.doc.toString();
        setCode(currentDoc);
    }
});
2.5 只读模式切换

通过 Ant Design 的 Switch 组件,用户可以切换编辑器的只读模式。当 isReadonly 状态变化时,我们使用 StateEffect.reconfigure 动态更新编辑器的配置。

useEffect(() => {
    if (editor !== undefined && isReadonly !== undefined) {
        editor.dispatch({
            effects: StateEffect.reconfigure.of([
                ...extensionConfig,
                EditorState.readOnly.of(isReadonly),
            ]),
        });
    }
}, [isReadonly, editor]);

3. 完整代码

以下是完整的 React 组件代码:

import { defaultKeymap } from '@codemirror/commands';
import { json, jsonParseLinter } from '@codemirror/lang-json';
import { lintGutter, linter } from '@codemirror/lint';
import { EditorState, StateEffect } from '@codemirror/state';
import { EditorView, keymap, lineNumbers } from '@codemirror/view';
import { Switch } from 'antd';
import React, { useEffect, useRef, useState } from 'react';
// 自定义的 JSON 解析 linter,检查 JSON 代码的有效性
const jsonParseLinterWithCheck = () => {
    return (view: EditorView) => {
        // 如果文档为空,不返回诊断信息
        const doc = view.state.doc.toString();
        if (doc.trim().length === 0) {
            return [];
        }
        // 使用 JSON 解析 linter 进行诊断
        const diagnostics = jsonParseLinter()(view);
        return diagnostics;
    };
};
const TestDemo: React.FC = () => {
    const containerRef = useRef<HTMLDivElement>(null);
    const [editor, setEditor] = useState<EditorView>();
    const [code, setCode] = useState<string>('');
    const [isReadonly, setIsReadonly] = useState<boolean>(false);
    // 编辑器更新 监听器
    const updateListener = EditorView.updateListener.of((vu) => {
        if (vu.docChanged) {
            // if条件可添加 && !vu.transactions.some((tr) => tr.annotation(External))  排除远程变更
            // 如果if条件满足执行的代码(1)只用于更新本地状态(如显示当前内容),且没有其他副作用,一般不需要排除远程变更。
            //                       (2)会触发其他逻辑(如保存内容到服务器),则需要排除远程变更,避免重复触发。
            const currentDoc = vu.state.doc.toString();
            setCode(currentDoc);
        }
    });
    // 编辑器的扩展配置
    // (1)显示行号 lineNumbers()
    // (2)显示 linting 结果的边栏  lintGutter()
    // (3)使用默认键盘快捷键 keymap.of(defaultKeymap)
    // (4)启用 JSON 语言支持 json()
    // (5)启用自定义的 linter  linter(jsonParseLinterWithCheck())
    const extensionConfig = [lineNumbers(), lintGutter(), keymap.of(defaultKeymap), json(), linter(jsonParseLinterWithCheck()), updateListener];
    useEffect(() => {
        if (containerRef.current) {
            // 创建编辑器实例
            const e = new EditorView({
                doc: code,
                extensions: extensionConfig,
                parent: containerRef.current,
            });
            setEditor(e);
        }
    }, []);
    // 当isReadonly状态或编辑器实例变化时重置编辑器配置(只读状态)
    useEffect(() => {
        if (editor !== undefined && isReadonly !== undefined) {
            editor.dispatch({
                effects: StateEffect.reconfigure.of([...extensionConfig, EditorState.readOnly.of(isReadonly)]),
            });
        }
    }, [isReadonly, editor]);
    return (
        <>
            <Switch
                style={{ margin: '0 0 15px 20px' }}
                checkedChildren="只读"
                unCheckedChildren="编辑"
                onChange={(value) => {
                    setIsReadonly(value);
                }}
            />
            {/* 编辑器渲染的容器 */}
            <div ref={containerRef} />
        </>
    );
};

export default TestDemo;

4. 运行效果

  • 用户可以在编辑器中输入 JSON 代码,编辑器会实时检查 JSON 的有效性。

  • 点击 Switch 组件可以切换编辑器的只读模式,在只读模式下,用户无法编辑内容。