monaco-react:Monaco Editor 的 React 封装,无需配置即可轻松集成

Monaco Editor for React - use the monaco-editor in any React application without needing to use webpack (or rollup/parcel/etc) configuration files / plugins

分支16Tags52
当前项目代码仓暂无内容

@monaco-editor/react · monthly downloads gitHub license npm version PRs welcome

Monaco Editor for React · 在任何 React 应用中使用 monaco-editor,无需配置 webpack(或 rollup/parcel 等)配置文件或插件


  • ✅ 支持 React V19
  • ⌨️ 使用 TypeScript 重写 🔥
  • 多模型编辑器 已支持,尽情使用吧 🎉
  • 🎉 v4 版本已发布 - 如需了解新版本的更新内容以及如何从 v3 迁移,请阅读此文档(另外,若需旧版本 README,可查看此处
  • 🎮 新增开发/ playground 章节 - 现在您可以运行 playground 并深入了解库的内部实现
  • 💫 已集成 @monaco-editor/loader

简介

Monaco 编辑器包装器,可轻松地与任何 React 应用进行一键式集成,无需使用 webpack(或任何其他模块打包器)的配置文件或插件。它可用于由 create-react-appcreate-snowpack-appviteNext.js 或任何其他应用生成器创建的应用 - 您无需 eject 或重新配置它们

动机

monaco-editor 是一款基于 Web 技术的知名代码编辑器,为 VS Code 提供支持。本库负责 monaco-editor 的设置流程,并提供简洁的 API,以便在任何 React 环境中与 monaco 进行交互。

演示

立即体验!

文档

安装

npm install @monaco-editor/react # or @monaco-editor/react@next for React v19

yarn add @monaco-editor/react

或者你可以使用 CDN这里有一个示例

注意:对于 TypeScript 类型定义,此包将 monaco-editor 包用作对等依赖项。因此,如果你需要类型定义且尚未安装 monaco-editor 包,则需要进行安装。

咨询 AI

Monaco-React AI 将帮助你更好地理解此仓库。你可以询问代码示例、安装指南、调试帮助等更多内容。

简介

除了类型之外,该库还导出 EditorDiffEditor 组件,以及 loader 工具和 useMonaco 钩子:

import Editor, { DiffEditor, useMonaco, loader } from '@monaco-editor/react';

使用方法

简单使用

以下是 monaco 编辑器与 React 项目简单集成的示例。
您只需导入并渲染 Editor 组件即可:

import React from 'react';
import ReactDOM from 'react-dom';

import Editor from '@monaco-editor/react';

function App() {
  return <Editor height="90vh" defaultLanguage="javascript" defaultValue="// some comment" />;
}

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);

codesandbox

扩展示例
import React from 'react';
import ReactDOM from 'react-dom';

import Editor from '@monaco-editor/react';

function App() {
  function handleEditorChange(value, event) {
    // here is the current value
  }

  function handleEditorDidMount(editor, monaco) {
    console.log('onMount: the editor instance:', editor);
    console.log('onMount: the monaco instance:', monaco);
  }

  function handleEditorWillMount(monaco) {
    console.log('beforeMount: the monaco instance:', monaco);
  }

  function handleEditorValidation(markers) {
    // model markers
    // markers.forEach(marker => console.log('onValidate:', marker.message));
  }

  return (
    <Editor
      height="90vh"
      defaultLanguage="javascript"
      defaultValue="// some comment"
      onChange={handleEditorChange}
      onMount={handleEditorDidMount}
      beforeMount={handleEditorWillMount}
      onValidate={handleEditorValidation}
    />
  );
}

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);

codesandbox

获取值

获取当前值有两种方式:

  1. editor 实例中获取当前模型的值
import React, { useRef } from 'react';
import ReactDOM from 'react-dom';

import Editor from '@monaco-editor/react';

function App() {
  const editorRef = useRef(null);

  function handleEditorDidMount(editor, monaco) {
    editorRef.current = editor;
  }

  function showValue() {
    alert(editorRef.current.getValue());
  }

  return (
    <>
      <button onClick={showValue}>Show value</button>
      <Editor
        height="90vh"
        defaultLanguage="javascript"
        defaultValue="// some comment"
        onMount={handleEditorDidMount}
      />
    </>
  );
}

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);

codesandbox

  1. 通过 onChange 属性获取当前模型的值
import React from 'react';
import ReactDOM from 'react-dom';

import Editor from '@monaco-editor/react';

function App() {
  function handleEditorChange(value, event) {
    console.log('here is the current model value:', value);
  }

  return (
    <Editor
      height="90vh"
      defaultLanguage="javascript"
      defaultValue="// some comment"
      onChange={handleEditorChange}
    />
  );
}

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);

codesandbox

(通过 `editor` 实例获取 `DiffEditor` 的值)
import React, { useRef } from 'react';
import ReactDOM from 'react-dom';

import { DiffEditor } from '@monaco-editor/react';

function App() {
  const diffEditorRef = useRef(null);

  function handleEditorDidMount(editor, monaco) {
    diffEditorRef.current = editor;
  }

  function showOriginalValue() {
    alert(diffEditorRef.current.getOriginalEditor().getValue());
  }

  function showModifiedValue() {
    alert(diffEditorRef.current.getModifiedEditor().getValue());
  }

  return (
    <>
      <button onClick={showOriginalValue}>show original value</button>
      <button onClick={showModifiedValue}>show modified value</button>
      <DiffEditor
        height="90vh"
        language="javascript"
        original="// the original code"
        modified="// the modified code"
        onMount={handleEditorDidMount}
      />
    </>
  );
}

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);

codesandbox

editor instance

editor 实例通过 onMount 属性的第一个参数暴露,第二个参数是 monaco 实例

import React, { useRef } from 'react';
import ReactDOM from 'react-dom';

import Editor from '@monaco-editor/react';

function App() {
  const editorRef = useRef(null);

  function handleEditorDidMount(editor, monaco) {
    // here is the editor instance
    // you can store it in `useRef` for further usage
    editorRef.current = editor;
  }

  return (
    <Editor
      height="90vh"
      defaultLanguage="javascript"
      defaultValue="// some comment"
      onMount={handleEditorDidMount}
    />
  );
}

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);

codesandbox

monaco instance

获取 monaco 实例有三种方式:

  1. 通过 onMount/beforeMount
import React, { useRef } from 'react';
import ReactDOM from 'react-dom';

import Editor from '@monaco-editor/react';

function App() {
  const monacoRef = useRef(null);

  function handleEditorWillMount(monaco) {
    // here is the monaco instance
    // do something before editor is mounted
    monaco.languages.typescript.javascriptDefaults.setEagerModelSync(true);
  }

  function handleEditorDidMount(editor, monaco) {
    // here is another way to get monaco instance
    // you can also store it in `useRef` for further usage
    monacoRef.current = monaco;
  }

  return (
    <Editor
      height="90vh"
      defaultLanguage="javascript"
      defaultValue="// some comment"
      beforeMount={handleEditorWillMount}
      onMount={handleEditorDidMount}
    />
  );
}

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);

codesandbox

  1. 通过 loader 工具
import { loader } from '@monaco-editor/react';

loader.init().then((monaco) => console.log('here is the monaco instance:', monaco));

codesandbox

  1. 通过 useMonaco 钩子
import React from 'react';
import ReactDOM from 'react-dom';

import Editor, { useMonaco } from '@monaco-editor/react';

function App() {
  const monaco = useMonaco();

  useEffect(() => {
    if (monaco) {
      console.log('here is the monaco instance:', monaco);
    }
  }, [monaco]);

  return <Editor height="90vh" defaultValue="// some comment" defaultLanguage="javascript" />;
}

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);

codesandbox

useMonaco

useMonaco 是一个 React 钩子,返回 monaco 的实例。但有一个重要注意事项需要考虑:初始化过程由 loader 工具(@monaco-editor/loader 的引用)处理:该过程是异步且仅执行一次的。因此,如果初始化的第一个发起者是 useMonaco 钩子,由于其异步加载特性,首次返回值将为 null。只需检查 useMonaco 的返回值即可。

import React, { useEffect } from 'react';
import ReactDOM from 'react-dom';

import Editor, { useMonaco } from '@monaco-editor/react';

function App() {
  const monaco = useMonaco();

  useEffect(() => {
    // do conditional chaining
    monaco?.languages.typescript.javascriptDefaults.setEagerModelSync(true);
    // or make sure that it exists by other ways
    if (monaco) {
      console.log('here is the monaco instance:', monaco);
    }
  }, [monaco]);

  return <Editor height="90vh" defaultValue="// some comment" defaultLanguage="javascript" />;
}

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);

codesandbox

loader-config

该库导出(命名导出)了名为 loader 的工具。本质上,它是 @monaco-editor/loader 的引用。默认情况下,monaco 文件会从 CDN 下载。您可以更改此行为以及其他与 monacoAMD 加载器相关的设置。我们提供了一个默认的 配置文件,您可以通过以下方式对其进行修改:

import { loader } from '@monaco-editor/react';

// you can change the source of the monaco files
loader.config({ paths: { vs: '...' } });

// you can configure the locales
loader.config({ 'vs/nls': { availableLanguages: { '*': 'de' } } });

// or
loader.config({
  paths: {
    vs: '...',
  },
  'vs/nls': {
    availableLanguages: {
      '*': 'de',
    },
  },
});
monaco-editor 用作 npm 包

v4.4.0 版本开始,可以将 monaco-editor 用作 npm 包;从 node_modules 中导入它,并将 monaco 源代码包含到你的 bundle 中(而非使用 CDN)。要实现此功能,你可以按以下步骤操作:

import * as monaco from 'monaco-editor';
import { loader } from '@monaco-editor/react';

loader.config({ monaco });

// ...

注意:您需要知道,这可能需要额外的 webpack 插件,例如 monaco-editor-webpack-plugin,或者在不弹出 CRA 生成的应用的情况下,可能无法使用它。

如果您使用 Vite,则需要执行以下操作:

import { loader } from '@monaco-editor/react';

import * as monaco from 'monaco-editor';
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker';
import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker';
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';

self.MonacoEnvironment = {
  getWorker(_, label) {
    if (label === 'json') {
      return new jsonWorker();
    }
    if (label === 'css' || label === 'scss' || label === 'less') {
      return new cssWorker();
    }
    if (label === 'html' || label === 'handlebars' || label === 'razor') {
      return new htmlWorker();
    }
    if (label === 'typescript' || label === 'javascript') {
      return new tsWorker();
    }
    return new editorWorker();
  },
};

loader.config({ monaco });

loader.init().then(/* ... */);

codesandbox

注意:您传入的对象将与默认对象进行深度合并。

多模型编辑器

当您渲染 Editor 组件时,会创建一个默认模型。需要重点说明的是,当您更改 languagevalue 属性时,它们会影响组件挂载时自动创建的同一个模型。在大多数情况下这没问题,但当开发者想要实现多模型编辑器以支持类似 IDE 中的标签/文件功能时,就会遇到问题。以前,要处理多个模型,他们必须在组件外部手动操作。现在,多模型 API 已得到支持 🎉 让我们看看它是如何工作的。创建模型有三个参数:valuelanguagepathmonaco.editor.createModel(value, language, monaco.Uri.parse(path)))。您可以将最后一个参数(path)视为模型的标识符。Editor 组件现在有一个 path 属性。当您指定 path 属性时,Editor 组件会检查是否存在具有该路径的模型。如果存在,将显示现有模型;否则,将创建(并存储)一个新模型。使用这种技术,您可以将文件与路径关联起来,创建一个完整的多模型编辑器。您可以打开文件、进行一些更改、选择另一个文件,当您回到第一个文件时,将显示之前的模型及其整个视图状态、文本选择、撤销堆栈、滚动位置等。(简单演示)

下面是一个简单示例:假设我们有一个类似 JSON 的文件结构表示,大致如下:

const files = {
  'script.js': {
    name: 'script.js',
    language: 'javascript',
    value: someJSCodeExample,
  },
  'style.css': {
    name: 'style.css',
    language: 'css',
    value: someCSSCodeExample,
  },
  'index.html': {
    name: 'index.html',
    language: 'html',
    value: someHTMLCodeExample,
  },
};

以下是我们简单的多模型编辑器实现:

import React from 'react';
import ReactDOM from 'react-dom';

import Editor from '@monaco-editor/react';

function App() {
  const [fileName, setFileName] = useState('script.js');

  const file = files[fileName];

  return (
    <>
      <button disabled={fileName === 'script.js'} onClick={() => setFileName('script.js')}>
        script.js
      </button>
      <button disabled={fileName === 'style.css'} onClick={() => setFileName('style.css')}>
        style.css
      </button>
      <button disabled={fileName === 'index.html'} onClick={() => setFileName('index.html')}>
        index.html
      </button>
      <Editor
        height="80vh"
        theme="vs-dark"
        path={file.name}
        defaultLanguage={file.language}
        defaultValue={file.value}
      />
    </>
  );
}

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);

属性:

  • defaultValue
  • defaultLanguage
  • defaultPath
  • value
  • language
  • path
  • saveViewState

这些属性将为您在使用多模型编辑器时提供更大的灵活性。

注意

defaultValuedefaultLanguagedefaultPath 仅在创建新模型期间被考虑
valuelanguagepath 始终被跟踪
saveViewState 是一个指示器,用于决定是否在模型切换时保存模型的视图状态

codesandbox

onValidate

onValidate 是一个附加属性。当当前模型的内容发生更改且当前模型标记准备就绪时,会触发一个事件。触发时将携带当前模型的标记信息。

import React from 'react';
import ReactDOM from 'react-dom';

import Editor from '@monaco-editor/react';

function App() {
  function handleEditorValidation(markers) {
    // model markers
    markers.forEach((marker) => console.log('onValidate:', marker.message));
  }

  return (
    <Editor
      height="90vh"
      defaultLanguage="javascript"
      defaultValue="// let's write some broken code 😈"
      onValidate={handleEditorValidation}
    />
  );
}

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);

codesandbox

需要说明的是,根据monaco-editor的划分,所有支持的语言分为两类:

  1. 具备丰富IntelliSense和验证功能的语言
  • TypeScript
  • JavaScript
  • CSS
  • LESS
  • SCSS
  • JSON
  • HTML
  1. 仅支持基本语法着色的语言
  • XML
  • PHP
  • C#
  • C++
  • Razor
  • Markdown
  • Diff
  • Java
  • VB
  • CoffeeScript
  • Handlebars
  • Batch
  • Pug
  • F#
  • Lua
  • Powershell
  • Python
  • Ruby
  • SASS
  • R
  • Objective-C

正如您所料,onValidate属性仅对第一组语言生效

注意事项

针对electron用户

作为一个常规的React组件,它在electron-react环境中也能正常工作,无需额外的webpack配置或其他附加操作。但开发者在使用过程中通常会遇到一些可能令人困惑的情况,具体如下:

  1. 加载界面卡住 通常是因为您的环境不允许加载外部资源。默认情况下,组件会从CDN加载monaco资源。您可以查看默认配置。不过,您完全可以更改此行为;该库是可完全配置的。相关内容请阅读此处。因此,如果您希望从本地文件加载,可以按以下方式操作:
import { loader } from '@monaco-editor/react';

loader.config({ paths: { vs: '../path-to-monaco' } });
  1. 根据你的 electron 环境,可能需要使用绝对 URL这里 获取的工具函数可以帮助你实现这一点。假设你已安装 monaco-editor 包,并且希望从 node_modules 而非 CDN 加载 monaco:在这种情况下,你可以编写如下代码:
function ensureFirstBackSlash(str) {
  return str.length > 0 && str.charAt(0) !== '/' ? '/' + str : str;
}

function uriFromPath(_path) {
  const pathName = path.resolve(_path).replace(/\\/g, '/');
  return encodeURI('file://' + ensureFirstBackSlash(pathName));
}

loader.config({
  paths: {
    vs: uriFromPath(path.join(__dirname, '../node_modules/monaco-editor/min/vs')),
  },
});

关于这个主题,还有几个相关问题可能也会有所帮助 - 1 2 3 4

此外,有一篇博客文章介绍了如何在 Electron 的离线模式下使用 @monaco-editor/react。您可能会觉得它很有用。

如果您在 electron 中结合使用 monacoreact 时遇到了上述未描述的问题,请告诉我们,以便我们改进此部分内容,使其更具帮助性。

针对 Next.js 用户

与其他 React 组件一样,本组件在 Next.js 中也能顺利工作。源代码中需要预解析的部分已针对服务器端渲染进行了优化,因此在通常情况下都能正常运行。但如果您想访问例如monaco instance,则需要注意它会访问 document 对象,这需要浏览器环境。基本上,您只需避免在浏览器环境之外运行该部分代码即可,有多种方法可以实现这一点。其中一种方法在这里有描述。

如果您在 Next.js 中使用 monaco 时遇到了上述未描述的问题,请告诉我们,以便我们改进此部分内容,使其更具帮助性。

创建您自己的编辑器

此库在底层使用了@monaco-editor/loader,它提供了一个名为 loader 的工具。loader 工具是一组函数的集合,用于将 monaco 编辑器设置到您的浏览器中。loader.init() 负责处理整个初始化过程,并返回 monaco 的实例 - loader.init().then(monaco => console.log("here is the monaco instance:", monaco))Editor 组件就是使用了这个工具,获取 monaco instance 并创建编辑器。这里Editor 组件的实现。您可以使用相同的技术来创建自己的 Editor。您只需导入 loader 工具,获取 monaco instance,然后使用您自己的自定义逻辑创建编辑器。实现此目的的最简方法如下:

import loader from '@monaco-editor/loader';

loader.init().then((monaco) => {
  const wrapper = document.getElementById('root');
  wrapper.style.height = '100vh';
  const properties = {
    value: 'function hello() {\n\talert("Hello world!");\n}',
    language: 'javascript',
  };

  monaco.editor.create(wrapper, properties);
});

就是这些了。你可以将它包装成一个 React 组件、Vue 组件、Angular 组件,或者保留为原生 JavaScript 形式,随你喜欢;它是用纯 js 编写的。

codesandbox

开发环境 - 实验场

拥有一个可以探索库内部机制的地方总是很重要的。playground 是一个极简的 React 应用,它直接使用该库的源代码。因此,如果你打算提交 PR、检查某些内容,或者只是想尝试库的最新状态,你可以运行这个实验场并尽情使用。

  • 克隆仓库
git clone https://github.com/suren-atoyan/monaco-react.git
  • 进入库文件夹
 cd monaco-react
  • 安装库的依赖项
 npm install # yarn
  • 前往代码游乐场
 cd playground
  • 安装该演示环境的依赖项
npm install # yarn
  • 并运行演示环境
npm run dev # yarn dev

monaco-react ├── playground │ ├── src/ # 示例应用源代码 ├── src/ # 库源代码 └── ...

如果您想修改库中的内容,请前往 monaco-react/src/...,库将自动重新构建,并且示例应用会使用最新的构建版本

属性

Editor

名称 类型 默认值 描述
defaultValue string 当前模型的默认值
defaultLanguage string 当前模型的默认语言
defaultPath string 当前模型的默认路径。将作为第三个参数传递给 .createModel 方法 - monaco.editor.createModel(..., ..., monaco.Uri.parse(defaultPath))
value string 当前模型的值
language enum: ... 当前模型的语言(所有 支持的语言
path string 当前模型的路径。将作为第三个参数传递给 .createModel 方法 - monaco.editor.createModel(..., ..., monaco.Uri.parse(defaultPath))
theme enum: "light" | "vs-dark" "light" Monaco 的主题。可用选项为 "vs-dark" | "light"。通过 monaco.editor.defineTheme 定义新主题
line number 要跳转到的行号
loading React Node "Loading..." 编辑器挂载前的加载屏幕
options object {} IStandaloneEditorConstructionOptions
overrideServices object {} IEditorOverrideServices
saveViewState boolean true 指示在模型更改之间是否保存模型的视图状态
keepCurrentModel boolean false 指示编辑器卸载时是否销毁当前模型
width union: number | string "100%" 编辑器包装器的宽度
height union: number | string "100%" 编辑器包装器的高度
className string 编辑器容器的类名
wrapperProps object {} 应用于包装器元素的属性
beforeMount func noop 签名: function(monaco: Monaco) => void
编辑器挂载前触发的事件。第一个参数为 monaco 实例
onMount func noop 签名: function(editor: monaco.editor.IStandaloneCodeEditor, monaco: Monaco) => void
编辑器挂载时触发的事件。第一个参数为 editor 实例,第二个参数为 monaco 实例
onChange func 签名: function(value: string | undefined, ev: monaco.editor.IModelContentChangedEvent) => void
当前模型内容更改时触发的事件
onValidate func noop 签名: function(markers: monaco.editor.IMarker[]) => void
当前模型内容更改且当前模型标记就绪时触发的事件

DiffEditor

名称 类型 默认值 描述
original string 原始源(左侧)的值
modified string 修改源(右侧)的值
language enum: ... 两个模型(原始模型和修改模型)的语言(所有 支持的语言
originalLanguage enum: ... 此属性使您可以单独指定原始源的语言,否则,它将使用 language 属性的值
modifiedLanguage enum: ... 此属性使您可以单独指定修改源的语言,否则,它将使用 language 属性的值
originalModelPath string “原始”模型的路径。将作为第三个参数传递给 .createModel 方法 - monaco.editor.createModel(..., ..., monaco.Uri.parse(originalModelPath))
modifiedModelPath string “修改”模型的路径。将作为第三个参数传递给 .createModel 方法 - monaco.editor.createModel(..., ..., monaco.Uri.parse(modifiedModelPath))
keepCurrentOriginalModel boolean false 指示 DiffEditor 卸载时是否销毁当前原始模型
keepCurrentModifiedModel boolean false 指示 DiffEditor 卸载时是否销毁当前修改模型
theme enum: "light" | "vs-dark" "light" Monaco 的主题。可用选项为 "vs-dark" | "light"。通过 monaco.editor.defineTheme 定义新主题
line number 要跳转到的行号
loading React Node "Loading..." 编辑器挂载前的加载屏幕
options object {} IDiffEditorConstructionOptions
width union: number | string "100%" 编辑器包装器的宽度
height union: number | string "100%" 编辑器包装器的高度
className string 编辑器容器的类名
wrapperProps object {} 应用于包装器元素的属性
beforeMount func noop 签名: function(monaco: Monaco) => void
编辑器挂载前触发的事件。第一个参数为 monaco 实例
onMount func noop 签名: function(editor: monaco.editor.IStandaloneCodeEditor, monaco: Monaco) => void
编辑器挂载时触发的事件。第一个参数为 editor 实例,第二个参数为 monaco 实例

许可协议

MIT

项目介绍

在 React 应用程序中轻松使用 Monaco 编辑器——无需配置 webpack(或 rollup/parcel 等)文件/插件即可集成 monaco-editor。【此简介由AI生成】

定制我的领域
184.74 K320访问 GitHub