65.9K
CodeProject 正在变化。 阅读更多。
Home

如何在 React 和 ChakraUI 中创建代码编辑器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2023 年 6 月 2 日

CPOL

8分钟阅读

viewsIcon

7748

downloadIcon

90

一步一步的教程,介绍如何实现你的第一个 React 代码编辑器

如果您是软件工程师、程序员或数据科学家,您一定清楚可靠的代码编辑器所扮演的重要角色。无论您是创建小型脚本、实验新想法还是处理复杂项目,代码编辑器都会成为您编码过程中值得信赖的伙伴。就我自己的笔记应用程序 Ballistic 而言,代码编辑器更是处于核心地位。

在这篇文章中,我们将踏上一段迷人的旅程,深入了解使用 React 和 CodeMirror 实现代码编辑器的细节。文末,您还将找到文中提到的所有源代码,以便在需要时进行参考。

CodeMirror

作为程序员,我们秉持“懒惰”原则,这意味着我们不希望在实现功能齐全的代码编辑器时重新造轮子。相反,我们的第一反应是探索可以在应用程序中利用的现有解决方案。幸运的是,有几个值得注意的选项可用。

  • Ace Editor:Ace 是一个用 JavaScript 编写的强大代码编辑器。它提供一系列功能,包括语法高亮、代码折叠、代码补全和多光标支持。
  • Monaco Editor:Monaco Editor 为 Visual Studio Code 提供强大的支持,提供丰富的编辑体验。其功能集包括 IntelliSense、调试集成、Git 集成等。
  • React-CodeMirror:如果您特别使用 React,React-CodeMirror 是一个绝佳的选择。它充当 CodeMirror 的包装器组件,可将 CodeMirror 的功能无缝集成到您的 React 应用程序中。这使您可以方便地在 React 组件中使用 CodeMirror。

在本文中,我们将重点介绍 React-CodeMirror,原因如下。由于我们的开发围绕 React 应用程序展开,因此该库非常符合我们的需求。此外,React-CodeMirror 是一个在宽松的 MIT 许可证下授权的开源项目。这意味着它已经被许多开发人员彻底使用和测试过,通常在我们开始实施之前就已经解决了许多小问题。一个外部库周围活跃的社区始终是我们决策过程中的一个关键因素,因为在将该库集成到我们的项目中时,它可以提供宝贵的支持和指导。

项目设置

考虑到项目背景和我们选择的选项,让我们开始设置开发环境。我们将从创建一个新的 React 应用开始,并使用 Yarn 作为我们的依赖管理器。

yarn create react-app react-code-editor --template typescript

应用程序设置完成后,我们需要安装必要的 CodeMirror 包。打开终端并运行以下命令:

yarn add @uiw/react-codemirror @uiw/codemirror-theme-github 
@uiw/codemirror-theme-darcula @codemirror/lang-markdown 
@codemirror/lang-python @codemirror/lang-javascript 
@codemirror/lang-cpp @codemirror/lang-html @codemirror/lang-json @codemirror/lang-java

为了加速开发并增强我们的组件实现,我们还将集成 Chakra UI,一个全面的组件库。

yarn add @chakra-ui/react @emotion/react @emotion/styled framer-motion

通过利用 Chakra UI,我们可以获得各种预构建组件,这些组件有助于创建视觉上吸引人且用户友好的界面。

创建基本代码编辑器

现在我们已经设置好了 React 应用程序并安装了所有必要的依赖项,让我们开始构建代码编辑器,方法是编辑 src/App.tsx 文件。将现有代码替换为以下内容:

import React, {useState} from 'react';
import {Center, ChakraProvider, Divider, Heading, VStack} from "@chakra-ui/react";
import {githubLight} from '@uiw/codemirror-theme-github';
import {python} from "@codemirror/lang-python";
import CodeMirror from '@uiw/react-codemirror';

function App() {
    const [text, setText] = useState("print(\\"Hello world!\\")");
    return (
        <ChakraProvider>
            <Center h={"100vh"}>
                <VStack boxShadow={'md'} p={4} borderStyle={"solid"} 
                 borderWidth={1} rounded={"lg"}>
                    <Heading>Code Editor</Heading>
                    <Divider/>
                    <CodeMirror
                        value={text}
                        onChange={(newValue) => setText(newValue)}
                        theme={githubLight}
                        extensions={[python()]}
                        basicSetup={{autocompletion: true}}
                        minWidth={'500px'}
                        minHeight={'500px'}
                    />
                </VStack>
            </Center>
        </ChakraProvider>

    );
}

export default App;

在此代码片段中,我们使用了一些 Chakra UI 组件创建了一个基本版本的代码编辑器。具体来说,我们使用了 @uiw/react-codemirror 中的 CodeMirror 组件。通过配置其属性,我们指定代码编辑器应解释 Python 代码,并且大小为 500px x 500px。在此阶段,我们有一个简单的代码编辑器,看起来像这样:

请注意,在 App 组件内部,我们使用 React 的 useState hook 创建了一个状态变量 text 和一个相应的设置函数 setText,其初始值为 'print("Hello world!")'

该组件返回一个 JSX 结构,其中包括 CenterVStackHeadingDivider 等 Chakra UI 组件。@uiw/react-codemirror 中的 CodeMirror 组件用于渲染代码编辑器本身。

CodeMirror 组件配置了诸如 value(绑定到 text 状态)、onChange(使用新值更新 text 状态)、theme(使用 GitHub Light 主题)、extensions(使用 Python 语言模式)、basicSetup(启用自动补全)以及 minWidthminHeight(设置代码编辑器尺寸)等属性。

添加更多支持的语言

为了使我们的代码编辑器更具通用性并支持多种编程语言,我们需要添加语言选择功能。让我们从创建支持的语言及其对应的 CodeMirror 语言支持对象的概述开始。

import { LanguageSupport } from '@codemirror/language';
import {markdown} from "@codemirror/lang-markdown";
import {javascript} from "@codemirror/lang-javascript";
import {cpp} from "@codemirror/lang-cpp";
import {html} from "@codemirror/lang-html";
import {json} from "@codemirror/lang-json";
import {java} from "@codemirror/lang-java";

const EXTENSIONS: { [key: string]: LanguageSupport[] } = {
    markdown: [markdown()],
    python: [python()],
    javascript: [javascript()],
    typescript: [javascript()],
    cpp: [cpp()],
    'c++': [cpp()],
    html: [html()],
    json: [json()],
    java: [java()],
};

现在,我们可以通过提供一个下拉菜单来进行语言选择来增强用户体验,并将选定的语言存储在另一个状态变量中。

function App() {
    const [language, setLanguage] = useState("python");
    const [text, setText] = useState("print(\\"Hello world!\\")");
    return (
        <ChakraProvider>
            <Center h={"100vh"}>
                <VStack boxShadow={'md'} p={4} borderStyle={"solid"} 
                 borderWidth={1} rounded={"lg"}>
                    <HStack w={"100%"} justify={"space-between"}>
                        <Heading>Code Editor</Heading>
                        <Menu>
                            <MenuButton as={Button}>
                                {language}
                            </MenuButton>
                            <MenuList>
                                {Object.entries(EXTENSIONS).map(([language, _]) => (
                                    <MenuItem onClick={() => 
                                     setLanguage(language)}>{language}</MenuItem>
                                ))}

                            </MenuList>
                        </Menu>
                    </HStack>

                    <Divider/>
                    <CodeMirror
                        value={text}
                        onChange={(newValue) => setText(newValue)}
                        theme={githubLight}
                        extensions={[EXTENSIONS[language]]}
                        basicSetup={{autocompletion: true}}
                        minWidth={'500px'}
                        minHeight={'500px'}
                    />
                </VStack>
            </Center>
        </ChakraProvider>
    );
}

有了语言选择下拉菜单后,用户现在可以选择他们想使用的编程语言。代码编辑器将根据所选语言动态更新。这是更新后的应用程序应该的样子:

现在,您拥有了一个更通用的代码编辑器,它支持多种编程语言,提供了更好的用户体验,并扩展了应用程序的使用范围。

引入后端 API 调用

在实践中,将最终用户编写的文本保存到程序的后端应用程序是很常见的。目前,我们将编写的文本存储在 React 状态变量 text 中。但是,如果网页刷新,用户所做的所有更改都将丢失。

让我们暂停一下,仔细看看 setText 函数及其影响。目前,每一次按键都会触发对 setText 的调用。但是,如果此函数还调用 API 更新数据库并在 <App /> 组件中重新渲染更新后的文本,则可能会导致过多的 API 调用。这会负面影响前端性能并导致用户体验不佳。

为了解决这个问题,将 API 调用与立即向用户显示的更新分离开来是有益的。一种有效的方法是对 API 调用进行“防抖”(debounce),确保它在短时间延迟后或在用户完成输入后执行。这减少了不必要的 API 调用并提高了性能,从而带来更流畅的用户体验。

防抖 API 调用

防抖函数是一个有用的工具,它可以延迟函数的执行,直到在函数再次被调用之前的一段时间已经过去。通过减少不必要的函数调用,它在优化应用程序性能方面特别有价值。让我们引入一个名为 updateBackend 的新函数,它代表后端调用。将以下代码片段添加到您的 App 组件中:

import { debounce } from 'ts-debounce';

// ...

const updateBackend = (newText: string) => {
    console.log(`Updated backend with: ${newText}`);
};
const updateTextDebounced = 
      useRef(debounce((newText: string) => updateBackend(newText), 1000 * 2));
useEffect(() => {
    updateTextDebounced.current(text);
}, [text]);

请注意,我们正在使用 ts-debounce 等外部库来实现防抖功能。但是,如果您愿意,也可以创建自己的防抖函数。在代码中,我们使用 useStateuseEffect hook 来处理和防抖文本输入更改。此外,useRef hook 确保当文本正在更新时,防抖函数每两秒只创建一次。

重要的是要注意,防抖函数还有许多其他用途。以下是一些示例:

  1. 搜索栏:当用户在搜索栏中输入搜索词时,我们可能希望触发搜索函数来显示相关结果。但是,在每次按键时触发搜索函数可能会导致不必要的 API 调用并影响应用程序性能。通过使用防抖函数,我们可以在用户停止输入一段时间后触发搜索函数,从而减少 API 调用并提高整体性能。
  2. 调整大小事件:如果我们页面上的某个元素需要根据窗口大小进行调整,使用防抖函数可以延迟调整大小函数,直到用户完成窗口调整大小。这种方法可以防止在调整大小事件期间过度调用该函数。
  3. 鼠标移动事件:在页面上的元素需要根据鼠标位置进行更改的情况下,使用防抖函数可以延迟 update 函数,直到用户停止移动鼠标。这种方法避免了在每次 mousemove 事件时触发函数,从而提高了性能。
  4. 表单验证:当用户填写表单时,通常需要验证输入并显示任何错误。通过使用防抖函数,我们可以在用户完成输入后延迟验证函数,从而减少不必要的验证调用并提高性能。

防抖函数用途广泛,在任何需要延迟函数执行直到在函数再次被调用之前的一段时间已经过去的情况下都有益。这种方法减少了不必要的函数调用,从而提高了应用程序性能。

您的代码编辑器的最终完整 React 组件现在如下所示:

import React, {useEffect, useRef, useState} from 'react';
import {
    Button,
    Center,
    ChakraProvider,
    Divider,
    Heading,
    HStack,
    Menu,
    MenuButton,
    MenuItem,
    MenuList,
    VStack
} from "@chakra-ui/react";
import {githubLight} from '@uiw/codemirror-theme-github';
import {python} from "@codemirror/lang-python";
import CodeMirror from '@uiw/react-codemirror';

import {LanguageSupport} from '@codemirror/language';
import {markdown} from "@codemirror/lang-markdown";
import {javascript} from "@codemirror/lang-javascript";
import {cpp} from "@codemirror/lang-cpp";
import {html} from "@codemirror/lang-html";
import {json} from "@codemirror/lang-json";
import {java} from "@codemirror/lang-java";
import {debounce} from "ts-debounce";

const EXTENSIONS: { [key: string]: LanguageSupport } = {
    markdown: markdown(),
    python: python(),
    javascript: javascript(),
    typescript: javascript(),
    cpp: cpp(),
    'c++': cpp(),
    html: html(),
    json: json(),
    java: java(),
};

function App() {
    const [language, setLanguage] = useState("python");
    const [text, setText] = useState("print(\\"Hello world!\\")");

    const updateBackend = (newText: string) => {
        console.log(`Updated backend with: ${newText}`);
    };
    const updateTextDebounced = 
    useRef(debounce((newText: string) => updateBackend(newText), 1000 * 2));
    useEffect(() => {
        updateTextDebounced.current(text);
    }, [text]);

    return (
        <ChakraProvider>
            <Center h={"100vh"}>
                <VStack boxShadow={'md'} p={4} 
                 borderStyle={"solid"} borderWidth={1} rounded={"lg"}>
                    <HStack w={"100%"} justify={"space-between"}>
                        <Heading>Code Editor</Heading>
                        <Menu>
                            <MenuButton as={Button}>
                                {language}
                            </MenuButton>
                            <MenuList>
                                {Object.entries(EXTENSIONS).map(([language, _]) => (
                                    <MenuItem key={language} 
                                     onClick={() => setLanguage(language)}>
                                     {language}</MenuItem>
                                ))}

                            </MenuList>
                        </Menu>
                    </HStack>

                    <Divider/>
                    <CodeMirror
                        value={text}
                        onChange={(newValue) => setText(newValue)}
                        theme={githubLight}
                        extensions={[EXTENSIONS[language]]}
                        basicSetup={{autocompletion: true}}
                        minWidth={'500px'}
                        minHeight={'500px'}
                    />
                </VStack>
            </Center>
        </ChakraProvider>
    );
}

export default App;

结论

总之,这篇博文指导您完成了使用 CodeMirror(一种功能强大的基于 JavaScript 的文本编辑器)在 React 中实现代码编辑器的过程。通过遵循分步说明,您已学会如何创建可以渲染具有语法高亮的代码或 Markdown 文本的功能组件。

任何文本编辑器的关键方面之一是更新文本的能力。但是,在每次文本更改时直接调用 API 端点可能会导致过多的 API 调用,从而影响前端性能并导致不理想的用户体验。

为了解决这个问题,我们引入了防抖函数(debounce function)的概念。通过集成防抖函数,我们可以将 API 调用的执行延迟一小段时间,或者直到用户完成输入。这种优化减少了不必要的 API 调用,提高了性能,并确保了更流畅的用户体验。

防抖函数在需要通过减少不必要的函数调用来优化性能的情况下,是一个有价值的工具。无论是代码编辑器、搜索栏还是任何其他交互式组件,实现防抖逻辑都可以显著提高应用程序的效率。

通过应用这篇博文中共享的知识和技术,您现在已掌握了创建具有改进用户体验的丰富功能代码编辑器的技能。请记住,在应用程序中需要性能优化的其他领域探索和调整防抖函数。

祝您编码愉快,尽情构建您的下一个 React 和 CodeMirror 文本编辑器!

您可以在 https://github.com/GlennViroux/react-code-editor 找到本博文中使用的所有源代码。

历史

  • 2023 年 6 月 2 日:初始版本
© . All rights reserved.