0


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

⭐️ 本文首发自 前端修罗场(点击加入),是

一个由 资深开发者 独立运行 的专业技术社区

,我专注

Web 技术、答疑解惑、面试辅导以及职业发展

现在加入,私聊我即可获取一次免费的模拟面试机会

,帮你评估知识点的掌握程度,获得更全面的学习指导意见,交个朋友,少走弯路,少吃亏!

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

在这里插入图片描述

其实在线代码编辑器很早就存在了,例如:

CodePen,CodeSanbox,JSFiddle

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

在这里插入图片描述

在这里插入图片描述

本篇文章我希望和大家一起,尝试创建一个在线的 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

重新安装依赖。

我们可以看到,我们在依赖中安装了两个库:

codemirror

react-codemirror2

。安装成功后,

node_modules\codemirror

文件夹下会有如下目录,这是我们后面要用到的:

在这里插入图片描述
接着,替换掉

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-vitalsreportWebVitals();

创建了新的 React 应用程序后,让我们在命令行中

cd

到该项目的目录:

cd web-code-editor

接下来,我们要创建三个选项卡,分别用于

HTML、CSS 和 JavaScript

代码的编辑。

在这里插入图片描述

创建按钮组件

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

src

文件夹中创建一个名为

components

的文件夹。 在这个新的组件文件夹中,创建一个名为

Button.jsx

JSX

文件。

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

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

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

  • 创建了一个名为 Button 的功能组件,然后我们将其导出。
  • 组件的 props 中解构了 titleonClick。 在这里,title 是一个文本字符串,onClick 是一个在单击按钮时调用的函数。
  • 接下来,我们使用 <button> 标签来声明按钮,并使用 style 属性来设置按钮的样式。
  • 接着,添加了 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';functionApp(){const[openedEditor, setOpenedEditor]=useState('html');return(<div className="App"></div>);}exportdefault App;

上述代码中,值

html

作为

state

的默认值传递,所以 HTML 编辑器将是默认打开的选项卡。

让我们继续编写函数,该函数将使用

setOpenedEditor

来更改单击选项卡按钮时的

state

值。

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

代码如下:

import React,{ useState }from'react';import'./App.css';import Button from'./components/Button';functionApp(){...constonTabClick=(editorName)=>{setOpenedEditor(editorName);};return(<div className="App"></div>);}exportdefault App;

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

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

<divclassName="App"><p>欢迎进入 Web Code Editor !</p><divclassName="tab-button-container">
    <Button title="HTML" onClick={() => {
      onTabClick('html')
    }} />
    <Button title="CSS" onClick={() => {
      onTabClick('css')
    }} />
    <Button title="JavaScript" onClick={() => {
      onTabClick('js')
    }} />
  </div></div>

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

...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 标签

目前的效果如下所示:

请添加图片描述

我们希望按钮显示在网格中,而不是像上图那样垂直堆叠。 那么移步到你的

App.css

文件并将

App.css

的中内容全部删去,接着填入以下代码:

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

App.js

中我们添加了

className="tab-button-container" 

作为包含三个选项卡按钮的 div 标记中的样式属性类。 在这里,我们设置了该容器的样式,使用 CSS 将其显示设置为 flex。

在这里插入图片描述

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

创建编辑器

因为我们已经在 CodeMirror 编辑器中安装了要处理的库,所以让我们继续在

components

文件夹中创建

Editor.jsx

文件。创建新文件后,让我们在其中编写一些初始代码:

import React,{ useState }from'react';import'codemirror/lib/codemirror.css';import{ Controlled as ControlledEditorComponent }from'react-codemirror2';constEditor=({ language, value, setEditorState })=>{return(<div className="editor-container"></div>)}exportdefault 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';constEditor=({ 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>)}exportdefault 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

函数:

consthandleChange=(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';constEditor=({ language, value, setEditorState })=>{consthandleChange=(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>)}exportdefault Editor

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

CodeMirror 主题

CodeMirror 有多个主题可供我们选择。 访问官方网站以查看可用的不同主题的演示。
在这里插入图片描述

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

首先,让我们在 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 现在的样子:

<ControlledEditorComponentonBeforeChange={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';constEditor=({ language, value, setEditorState })=>{const[theme, setTheme]=useState("dracula")const themeArray =['dracula','material','mdn-like','the-matrix','night']consthandleChange=(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>)}exportdefault 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

标记替换为我们刚刚创建的编辑器组件:

functionApp(){...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>);}exportdefault App;

上述代码中:我们用编辑器组件的实例替换了 p 标签。 然后,我们分别提供了它们的

language、value和 setEditorState

属性,以匹配它们对应的状态。

效果如下:

在这里插入图片描述

添加 Iframes

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

MDN: HTML 内联框架元素 (

<iframe>

) 表示嵌套的浏览上下文,将另一个 HTML 页面嵌入到当前页面中。

Iframes 如何在 React 中工作

iframe 通常与纯 HTML 一起使用。 将 iframe 与 React 一起使用不需要很多更改,主要是将属性名称转换为驼峰式。 一个例子是

srcdoc

会变成

srcDoc

创建 iframe 容器来容纳编辑器的结果

让我们继续,在

App.js

中创建一个 iframe 来容纳我们的编辑器的结果。

return(<div className="App">// ...<div><iframe
          srcDoc={srcDoc}
          title="output"
          sandbox="allow-scripts"
          frameBorder="1"
          width="100%"
          height="100%"/></div></div>);

在这里,我们创建了 iframe 并将其存放在 div 容器标签中。 在 iframe 中,我们传递了一些我们需要的属性:

  • srcDoc: srcDoc 属性是用驼峰写的,因为这是在 React 中编写 iframe 属性的方法。 使用 iframe 时,我们可以在页面上嵌入外部网页或呈现指定的 HTML 内容。 要加载和嵌入外部页面,我们将使用 src 属性。 在我们的例子中,我们没有加载外部页面; 相反,我们想创建一个新的内部 HTML 文档来存放我们的结果。为此,我们需要 srcDoc 属性。 该属性采用我们想要嵌入的 HTML 文档。
  • title: title 属性用于描述内联框架的内容。
  • sandbox: 这个属性有很多用途。 在我们的例子中,我们使用它来允许脚本在我们的 iframe 中使用 allow-scripts 值运行。 因为我们正在使用 JavaScript 编辑器,所以这会很快派上用场。
  • frameBorder: 仅定义了 iframe 的边框厚度。
  • width 和 height: 定义了 iframe 的宽度和高度。

让我们继续并声明将保存 srcDoc 的 HTML 模板文档的状态。 如果你仔细查看上面的代码块,你会发现我们向 srcDoc 属性传递了一个值:

srcDoc={srcDoc}

。 让我们使用

useState() hook

来声明 srcDoc 状态。 为此,在 App.js 文件中,转到我们定义其他 State 的位置并添加以下状态:

const[srcDoc, setSrcDoc]=useState(``);

现在我们已经创建了 state,接下来要做的就是在我们在代码编辑器中输入时在状态中显示结果。 但有一点值得注意,就是我们不希望在每次输入时都重新渲染组件,这就涉及到后续优化的地方

配置 iframe 以显示结果

每当 HTML、CSS 和 JavaScript 的任何编辑器分别发生变化时,我们都希望触发

useEffect()

,这将在 iframe 中呈现更新的结果。 让我们在

App.js

文件中编写

useEffect()

来执行此操作:

首先,导入

useEffect()

钩子:

import React,{ useState,  useEffect }from'react';

代码如下:

useEffect(()=>{const timeOut =setTimeout(()=>{setSrcDoc(`
          <html>
            <body>${html}</body>
            <style>${css}</style>
            <script>${js}</script>
          </html>
        `)},250);return()=>clearTimeout(timeOut)},[html, css, js])

在这里,我们编写了一个

useEffect()

hook,只要我们为 html、css 和 js 编辑器声明的值状态发生更改或更新,该 hook 就会运行。

你可能会问:为什么我们需要使用

setTimeout()

如果我们在没有它的情况下编写它,那么每次在编辑器中按下一个键,我们的 iframe 都会更新,这通常不利于性能。 所以我们使用 setTimeout() 将更新延迟 250 毫秒,让我们有足够的时间知道用户是否还在打字。 也就是说,每次用户按下一个键时,它都会重新开始计数,因此 iframe 只会在用户空闲(未键入)250 毫秒时更新。 这是避免每次按下键时都必须更新 iframe 的一种很酷的方法。

在我们的代码中,我们传递了一个 HTML 模板,获取包含用户在 HTML 编辑器中键入的代码的 html 状态,并将其放置在模板的 body 标记之间。 我们还获取了包含用户在 CSS 编辑器中输入的样式的 css 状态,并在样式标签之间传递了它。 最后,我们获取了包含用户在 JavaScript 编辑器中键入的 JavaScript 代码的 js 状态,并在脚本标签之间传递了它。从而这就形成了一个包含 HTML、CSS、Javascript的网页。

请注意,在设置

setSrcDoc

时,我们使用了反引号

(``)

而不是普通引号

(' ')

。 这是因为反引号允许我们传入相应的状态值,就像我们在上面的代码中所做的那样。

useEffect() 钩子中的 return 语句是一个清理函数,它在完成时清除

setTimeout()

以避免内存泄漏

以下是我们的项目目前的样子:

在这里插入图片描述

CodeMirror 插件

使用 CodeMirror 插件,我们可以使用其他代码编辑器中的更多功能来增强我们的编辑器。 让我们来看一个在输入开始标签时自动添加结束标签的示例,以及在输入开始括号时自动结束括号的另一个示例:

首先要做的是将插件导入到我们的

Editor.jsx

文件中:

import'codemirror/addon/edit/closetag';import'codemirror/addon/edit/closebrackets';

让我们在

ControlledEditorComponent

选项中传递它:

<ControlledEditorComponent...options={{...autoCloseTags:true,autoCloseBrackets:true,}}/>

在这里插入图片描述

可以看出,我们实现了标签自动补全的功能。

当然,如果你想的话,你可以将大量这些插件添加到你的编辑器中,以使其具有更丰富的功能。本文中,我们就不尝试所有功能了。

至此,我们大致完成了一个在线编辑器的应用。接下来,我想和大家讨论一下关于如何提升应用性能和可访问性。

性能与可访问性

看看我们的代码编辑器,有些东西肯定是可以改进的。为了获得更好的可访问性,你可以采取以下措施来改进:

  1. 你可以在当前打开的编辑器的按钮上设置一个 active 类,高亮显示该按钮。这样可以让用户清楚地知道他们当前正在使用哪个编辑器,从而提高可访问性。
  2. 你可能希望编辑器占用比我们这里更多的屏幕空间。 你可以尝试的另一件事是通过单击停靠在侧面某处的按钮来弹出 iframe。 这样做会给编辑器更多的屏幕空间。
  3. 这种编辑器对于想要在移动设备上进行快速练习的人很有用,因此需要完全适应移动设备。
  4. 目前,我们可以在加载的多个主题中切换编辑器组件的主题,但页面的总体主题保持不变。 你可以让用户在整个布局的深色和浅色主题之间切换。 这将有利于可访问性,减轻人们长时间看明亮的屏幕对眼睛的压力。
  5. 我们没有考虑 iframe 的安全问题,主要是因为我们在 iframe 中加载了内部 HTML 文档,而不是外部文档。 所以我们不需要考虑太多,因为 iframe 非常适合我们的用例。
  6. 对于 iframe,另一个考虑因素是页面加载时间,因为 iframe 中加载的内容通常不受你的控制。 在我们的应用程序中,这不是问题,因为我们的 iframe 内容不是外部的。

当你构建任何应用程序时,性能和可访问性都值得考虑很多,因为它们将决定你的应用程序对其用户的有用性和可用性。

写在最后

本文创建的 Web 代码编辑器还有很多可以改进的地方,希望你能在此基础上做很多的扩展,丰富编辑器的功能与界面!

**如果你觉得这篇文章还不错,请点击下方小红心 👍🏻 ❤️,鼓励一下!我会继续为你带来优质的内容~我是前端修罗场,

一个独立运行的专业技术社区

,感谢你的关注与支持!**

参考

  • CodeMirror 官方文档
  • 提升 React 应用性能的方法
  • 本文源代码下载

本文转载自: https://blog.csdn.net/ImagineCode/article/details/125772097
版权归原作者 前端修罗场 所有, 如有侵权,请联系我们删除。

“【实战】快来和我一起开发一个在线 Web 代码编辑器”的评论:

还没有评论