【实战】快来和我一起开发一个在线 Web 代码编辑器

⭐️ 本文首发自 前端修罗场(点击加入),是一个由 资深开发者 独立运行 的专业技术社区,我专注 Web 技术、答疑解惑、面试辅导以及职业发展现在加入,私聊我即可获取一次免费的模拟面试机会,帮你评估知识点的掌握程度,获得更全面的学习指导意见,交个朋友,少走弯路,少吃亏!

最近看了掘金刚上线的在线代码编辑器 “码上掘金”,突然想是不是自己也可以写一个在线代码编辑器。

【实战】快来和我一起开发一个在线 Web 代码编辑器_第1张图片

其实在线代码编辑器很早就存在了,例如: CodePen,CodeSanbox,JSFiddle 等等都是大家耳熟能详的。这些编辑器给开发者提供了这样的使用场景:当没有机会使用代码编辑器应用程序时,或者当你想使用计算机甚至手机快速尝试 Web 上的某些内容时,在线 Web 代码编辑器就会进行我们的视野。

【实战】快来和我一起开发一个在线 Web 代码编辑器_第2张图片

【实战】快来和我一起开发一个在线 Web 代码编辑器_第3张图片

本篇文章我希望和大家一起,尝试创建一个在线的 Web 代码编辑器,并在 HTML、CSS 和 JavaScript 的帮助下实时显示结果。我在本文的最后也放置了源代码的下载链接。

我认为这也是一个有趣的项目,因为了解如何构建代码编辑器将使你了解到做这个项目需要处理哪些功能模块。我们第一个需要了解的模块是 CodeMirror

使用 CodeMirror

我们将使用一个名为 CodeMirror 的库来构建我们的编辑器。 CodeMirror 是一个用 JavaScript 实现的通用文本编辑器。 它特别适用于编辑代码,并带有多种语言模式和附加组件,可实现更高级的编辑功能。同时,CodeMirror 带有丰富的 API 和 主题模式可以帮助你扩展应用的功能。

接下来,我们进入正题,开始构建这个项目。

创建 React 项目

我们先从创建一个新的 React 项目开始。 在命令行中,创建一个 React 应用程序并将其命名为 web-code-editor:

npx create-react-app web-code-editor

同时,因为此时 creat-react-app 安装的是 react 18版本,考虑到兼容性,本文需要指定 react 的版本为 17.x。请修改 package.json 的依赖:

"dependencies": {
    "@testing-library/jest-dom": "^5.11.6",
    "@testing-library/react": "^11.2.2",
    "@testing-library/user-event": "^12.5.0",
    "codemirror": "^5.59.1",
    "react": "^17.0.1",
    "react-codemirror2": "^7.2.1",
    "react-dom": "^17.0.1",
    "react-scripts": "4.0.1",
    "web-vitals": "^0.2.4"
  },

然后删除node_modules 文件夹,并重新执行 npm install 重新安装依赖。

我们可以看到,我们在依赖中安装了两个库:codemirrorreact-codemirror2。安装成功后,node_modules\codemirror 文件夹下会有如下目录,这是我们后面要用到的:

【实战】快来和我一起开发一个在线 Web 代码编辑器_第4张图片
接着,替换掉 src\index.js 文件夹的内容为如下代码:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

创建了新的 React 应用程序后,让我们在命令行中 cd 到该项目的目录:

cd web-code-editor

接下来,我们要创建三个选项卡,分别用于 HTML、CSS 和 JavaScript 代码的编辑。

【实战】快来和我一起开发一个在线 Web 代码编辑器_第5张图片

创建按钮组件

接下来,我们将创建一个通用的按钮组件,用于选项卡中。

src 文件夹中创建一个名为 components 的文件夹。 在这个新的组件文件夹中,创建一个名为 Button.jsxJSX 文件。

以下是 Button 组件所需的代码:

import React from 'react'
const Button = ({title, onClick}) => {
  return (
    <div>
      <button
        style={{
          maxWidth: "140px",
          minWidth: "80px",
          height: "30px",
          marginRight: "5px"
        }}
        onClick={onClick}
      >
        {title}
      </button>
    </div>
  )
}
export default Button

上面代码中,我们做了以下几件事:

  • 创建了一个名为 Button 的功能组件,然后我们将其导出。
  • 组件的 props 中解构了 titleonClick。 在这里,title 是一个文本字符串,onClick 是一个在单击按钮时调用的函数。
  • 接下来,我们使用
  • 接着,添加了 onClick 属性并将解构的 onClick props 传递给它。
  • 最后,传入 {title} 作为按钮标签的内容

现在我们已经创建了一个可重用的按钮组件,让我们继续将我们的组件引入 App.js。 请移步到 App.js 并导入新创建的按钮组件:

import Button from './components/Button';

要跟踪打开的选项卡或编辑器,我们需要声明一个 state 来保存打开的编辑器的值。 使用 useState 钩子,我们将该 state 存储单击该选项卡按钮时当前打开的编辑器选项卡的名称。

代码如下:

import React, { useState } from 'react';
import './App.css';
import Button from './components/Button';

function App() {
  const [openedEditor, setOpenedEditor] = useState('html');
  return (
    <div className="App">
    </div>
  );
}
export default App;

上述代码中,值 html 作为 state 的默认值传递,所以 HTML 编辑器将是默认打开的选项卡。

让我们继续编写函数,该函数将使用 setOpenedEditor 来更改单击选项卡按钮时的 state 值。

注意:这里可能不会同时打开两个选项卡,所以我们在编写函数时需要考虑到这一点。

代码如下:

import React, { useState } from 'react';
import './App.css';
import Button from './components/Button';

function App() {
  ...

  const onTabClick = (editorName) => {
    setOpenedEditor(editorName);
  };

  return (
    <div className="App">
    </div>
  );
}
export default App;

在这里,我们传递了一个函数参数,它是当前选择的选项卡的名称。

接着继续为三个选项卡创建 Button 的三个实例:

<div className="App">
  <p>欢迎进入 Web Code Editor !p>
  <div className="tab-button-container">
    

接着,我们使用三元运算符有条件地显示选项卡的内容:

...
return (
    <div className="App">
      ...
      <div className="editor-container">
        {
          openedEditor === 'html' ? (
            <p>HTML editor</p>
          ) : openedEditor === 'css' ? (
            <p>CSS editor</p>
          ) : (
            <p>JavaScript editor</p>
          )
        }
      </div>
    </div>
  );
...

上面代码中,如果 openedEditor 的值为html,则显示 HTML 部分。 否则,如果openedEditor 的值为 css,则显示 CSS 部分。 否则,如果该值既不是 html 也不是 css,那么这意味着该值必须是 js。

我们对三元运算符条件中的不同部分使用了 p 标签 。 后面我们将创建编辑器组件并用编辑器组件本身替换 p 标签

目前的效果如下所示:

【实战】快来和我一起开发一个在线 Web 代码编辑器_第6张图片

我们希望按钮显示在网格中,而不是像上图那样垂直堆叠。 那么移步到你的 App.css文件并将 App.css 的中内容全部删去,接着填入以下代码:

.tab-button-container{
  display: flex;
}

App.js 中我们添加了 className="tab-button-container" 作为包含三个选项卡按钮的 div 标记中的样式属性类。 在这里,我们设置了该容器的样式,使用 CSS 将其显示设置为 flex。

【实战】快来和我一起开发一个在线 Web 代码编辑器_第7张图片

在下一节中,我们将创建我们的编辑器,用它们替换 p 标签。

创建编辑器

因为我们已经在 CodeMirror 编辑器中安装了要处理的库,所以让我们继续在 components 文件夹中创建 Editor.jsx 文件。创建新文件后,让我们在其中编写一些初始代码:

import React, { useState } from 'react';
import 'codemirror/lib/codemirror.css';
import { Controlled as ControlledEditorComponent } from 'react-codemirror2';


const Editor = ({ language, value, setEditorState }) => {
  return (
    <div className="editor-container">
    </div>
  )
}
export default Editor

上述代码中:

  • 我们将 React 与 useState 一起导入。
  • 我们导入了 CodeMirror CSS 文件。
  • 我们从 react-codemirror2 导入 Controlled,将其重命名为 ControlledEditorComponent 以使其更清晰。
  • 然后,我们声明了我们的编辑器功能组件。

在我们的函数组件中,我们从 props 中解构了一些值,包括language、value和 setEditorState。 当在 App.js 中调用编辑器时,这三个 prop 将在编辑器的任何实例中提供。

让我们使用 ControlledEditorComponent 为我们的编辑器编写代码。 代码如下:

import React, { useState } from 'react';
import 'codemirror/lib/codemirror.css';
import 'codemirror/mode/xml/xml';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/css/css';
import { Controlled as ControlledEditorComponent } from 'react-codemirror2';


const Editor = ({ language, value, setEditorState }) => {
  return (
    <div className="editor-container">
      <ControlledEditorComponent
        onBeforeChange={handleChange}
        value= {value}
        className="code-mirror-wrapper"
        options={{
          lineWrapping: true,
          lint: true,
          mode: language,
          lineNumbers: true,
        }}
      />
    </div>
  )
}
export default Editor

上述代码中:

CodeMirror 的 mode 指定编辑器适用于哪种语言。 我们导入了三种模式,因为我们有这个项目的三个编辑器:

  • XML:(codemirror/mode/xml/xml) 模式适用于 HTML。
  • JavaScript:(codemirror/mode/javascript/javascript) 模式适用于 JavaScript。
  • CSS:(codemirror/mode/css/css)模式适用于 CSS。

注意:因为编辑器是作为可重用的组件构建的,所以我们不能在编辑器中直接把模式写死。 所以,我们通过我们解构的 language 来提供模式。

接下来,我们来讨论一下 ControlledEditorComponent 中的东西:

  • onBeforeChange
    每当你向编辑器写入或从编辑器中删除时,都会调用此方法。 可以将其想象为通常在输入字段中用于跟踪更改的 onChange 处理程序。 使用它,我们将能够在有新更改的任何时候获取编辑器的值并将其保存到编辑器的状态。

  • value = {value}
    这只是编辑器在任何给定时间的内容。 我们将一个名为 value 的 prop 传递给该属性。 value 保存该编辑器值的状态。 这将由编辑器的实例提供。

  • className="code-mirror-wrapper"
    这个类名不是我们自己设置的样式。 它由我们在上面导入的 CodeMirror 的 CSS 文件提供。

  • options
    这是一个具有我们希望编辑器具有的不同功能的对象。 CodeMirror 中有许多令人惊叹的选项。 让我们看看我们在这里使用的那些:

    • lineWrapping: true 这意味着当行满时代码应该换行到下一行。
    • lint: true 允许检测提示。
    • mode:language 如上所述,此模式采用编辑器将要使用的语言。 上面已经导入了语言,但是编辑器将根据通过 prop 提供给编辑器的 language 值应用语言。
    • lineNumbers: true 这指定编辑器应该有每一行的行号

接下来,我们为 onBeforeChange 处理程序编写 handleChange 函数:

const handleChange = (editor, data, value) => {
    setEditorState(value);
}

我们只需要value,因为它是我们想要在 setEditorState 属性中传递的值。 setEditorState 属性代表我们在 App.js 中声明的每个状态的值,保存每个编辑器的值。 完整代码如下:

import React, { useState } from 'react';
import 'codemirror/lib/codemirror.css';
import 'codemirror/mode/xml/xml';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/css/css';
import { Controlled as ControlledEditorComponent } from 'react-codemirror2';

const Editor = ({ language, value, setEditorState }) => {
    const handleChange = (editor, data, value) => {
        setEditorState(value);
    }
    return (
        <div className="editor-container">
        <ControlledEditorComponent
            onBeforeChange={handleChange}
            value= {value}
            className="code-mirror-wrapper"
            options={{
            lineWrapping: true,
            lint: true,
            mode: language,
            lineNumbers: true,
            }}
        />
        </div>
    )
}
export default Editor

接下来,我们将添加一个下拉菜单,允许我们为编辑器选择不同的主题

CodeMirror 主题

CodeMirror 有多个主题可供我们选择。 访问官方网站以查看可用的不同主题的演示。
【实战】快来和我一起开发一个在线 Web 代码编辑器_第8张图片

让我们创建一个包含不同主题的下拉列表,用户可以在我们的编辑器中选择这些主题。 本文中,我们将添加五个主题,但你可以添加任意数量的主题。

首先,让我们在 Editor.jsx 组件中导入我们的主题:

import 'codemirror/theme/dracula.css';
import 'codemirror/theme/material.css';
import 'codemirror/theme/mdn-like.css';
import 'codemirror/theme/the-matrix.css';
import 'codemirror/theme/night.css';

接下来,创建一个包含我们导入的所有主题的数组:

const themeArray = ['dracula', 'material', 'mdn-like', 'the-matrix', 'night']

让我们声明一个 useState 挂钩来保存所选主题的值,并将默认主题设置为 dracula

const [theme, setTheme] = useState("dracula")

让我们创建下拉列表:

...
return (
    <div className="editor-container">
      <div style={{marginBottom: "10px"}}>
          <label for="cars">选择主题: </label>
          <select name="theme" onChange={(el) => {
          setTheme(el.target.value)
          }}>
          {
              themeArray.map( theme => (
              <option value={theme}>{theme}</option>
              ))
          }
          </select>
      </div>
    // ...
    </div>
  )
...

在上面的代码中,我们使用 label 标签向我们的下拉列表添加标签,然后添加 select 标签来创建我们的下拉列表。

因为我们需要用我们创建的 themeArray 中的主题名称填充下拉列表,所以我们使用 .map 数组方法来映射 themeArray 并使用 option 标签单独显示名称。

同时,在选择标签时,我们传递了 onChange 属性来跟踪和更新主题状态。 每当在下拉列表中选择一个新选项时,该值都是从返回给我们的对象中获取的。 接下来,我们使用 state hook 中的 setTheme 将新值设置为 state 持有的值。

至此,我们已经创建了下拉菜单,设置了主题的状态,并编写了函数来使用新值设置状态。 为了使 CodeMirror 使用我们的主题,我们需要做的最后一件事是将主题传递给 ControlledEditorComponent 中的 option 对象。 在 option 对象中,让我们添加一个名为 theme 的值,并将其值设置为所选主题的状态值。

这是 ControlledEditorComponent 现在的样子:

<ControlledEditorComponent
  onBeforeChange={handleChange}
  value= {value}
  className="code-mirror-wrapper"
  options={{
    lineWrapping: true,
    lint: true,
    mode: language,
    lineNumbers: true,
    theme: theme,
  }}
/>

现在,我们就已经添加了一个可以在编辑器中选择的不同主题的下拉列表。

下面是 Editor.jsx 中的完整代码目前的样子:

import React, { useState } from 'react';
import 'codemirror/lib/codemirror.css';
import 'codemirror/mode/xml/xml';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/css/css';

import 'codemirror/theme/dracula.css';
import 'codemirror/theme/material.css';
import 'codemirror/theme/mdn-like.css';
import 'codemirror/theme/the-matrix.css';
import 'codemirror/theme/night.css';
import { Controlled as ControlledEditorComponent } from 'react-codemirror2';

const Editor = ({ language, value, setEditorState }) => {
    const [theme, setTheme] = useState("dracula")
    const themeArray = ['dracula', 'material', 'mdn-like', 'the-matrix', 'night']
    const handleChange = (editor, data, value) => {
        setEditorState(value);
    }
    return (
        <div className="editor-container">
        <div style={{marginBottom: "10px"}}>
            <label for="cars">选择主题: </label>
            <select name="theme" onChange={(el) => {
            setTheme(el.target.value)
            }}>
            {
                themeArray.map( theme => (
                <option value={theme}>{theme}</option>
                ))
            }
            </select>
        </div>
        <ControlledEditorComponent
            onBeforeChange={handleChange}
            value= {value}
            className="code-mirror-wrapper"
            options={{
              lineWrapping: true,
              lint: true,
              mode: language,
              lineNumbers: true,
              theme: theme,
            }}
        />
        </div>
    )
}
export default Editor

接着,我们转到 App.css 添加一个 editor-container 样式:

.editor-container{
  padding-top: 0.4%;
}

现在我们的编辑器已经准备好了,让我们回到 App.js 并在那里使用它们。

使用编辑器组件

我们需要做的第一件事是在此处导入 Editor.jsx 组件:

import Editor from './components/Editor';

在 App.js 中,让我们分别声明保存 HTML、CSS 和 JavaScript 编辑器内容的状态。

const [html, setHtml] = useState('');
const [css, setCss] = useState('');
const [js, setJs] = useState('');

这些状态会作为内容提供给给编辑器组件。

接下来,让我们将条件渲染中用于 HTML、CSS 和 JavaScript 的 p 标记替换为我们刚刚创建的编辑器组件:

function App() {
  ...
  return (
    <div className="App">
      <p>欢迎进入 Web Code Editor !</p>
      <div className="tab-button-container">
        <Button title="HTML" onClick={() => {
          onTabClick('html')
        }} />
        <Button title="CSS" onClick={() => {
          onTabClick('css')
        }} />
        <Button title="JavaScript" onClick={() => {
          onTabClick('js')
        }} />
      </div>
      <div className="editor-container">
        {
          openedEditor === 'html' ? (
            <Editor
              language="xml"
              value={html}
              setEditorState={setHtml}
            />
          ) : openedEditor === 'css' ? (
            <Editor
              language="css"
              value={css}
              setEditorState={setCss}
            />
          ) : (
            <Editor
              language="javascript"
              value={js}
              setEditorState={setJs}
            />
          )
        }
      </div>
    </div>
  );
}
export default App;

上述代码中:我们用编辑器组件的实例替换了 p 标签。 然后,我们分别提供了它们的language、value和 setEditorState 属性,以匹配它们对应的状态。

效果如下:

【实战】快来和我一起开发一个在线 Web 代码编辑器_第9张图片

添加 Iframes

我们将使用内联框架 (iframe) 来显示在编辑器中输入的代码的结果。

MDN: HTML 内联框架元素 (