Monaco Editor VS CodeMirror

React.js

CodeMirror — очень старомодный веб-редактор, он разработан до версии v6, но v6 все еще находится в стадии тестирования, сегодня я представлю его с версией 5.57.0. Редактор Monaco намного моложе, но, несмотря на это, репутация у него не маленькая, просто потому, что он использует тот же основной код, что и VSCode. Далее автор сравнит два редактора по трем аспектам: «метод использования», «масштабируемость» и «производительность».

инструкции

метод инициализации

Условно говоря, у CodeMirror методов инициализации больше, а у Monaco всего один (это не влияет на реализацию различных функций), но Monaco лучше собственного Diff-редактора.

CodeMirror

Способ 1. Вставьте в узел-контейнер, например body или #root.

<div id="root"></div>
import CodeMirror from "codemirror";
import "codemirror/mode/sql/sql";
import "codemirror/lib/codemirror.css";

CodeMirror(document.getElementById("root"), {
  value: `-- SQL Mode for CodeMirror
SELECT SQL_NO_CACHE DISTINCT
    @var1 AS \`val1\`, @'val2', @global.'sql_mode',
    1.1 AS \`float_val\`, .14 AS \`another_float\`, 0.09e3 AS \`int_with_esp\`,
    0xFA5 AS \`hex\`, x'fa5' AS \`hex2\`, 0b101 AS \`bin\`, b'101' AS \`bin2\`,
    DATE '1994-01-01' AS \`sql_date\`, { T "1994-01-01" } AS \`odbc_date\`,
    'my string', _utf8'your string', N'her string',
        TRUE, FALSE, UNKNOWN
  FROM DUAL
  -- space needed after '--'
  # 1 line comment
  /* multiline
  comment! */
  LIMIT 1 OFFSET 0;
`,
  mode: "text/x-sql",
  indentWithTabs: true,
  smartIndent: true,
  lineNumbers: true,
  matchBrackets: true,
  autofocus: true
});

Способ 2. Выполните любую операцию с узлом в функции, например replaceWith (замените узел), insertBefore (вставьте перед узлом), добавьте и т. д.

<div id="root">
  <div id="replace"></div>
</div>
import CodeMirror from "codemirror";
import "codemirror/mode/sql/sql";
import "codemirror/lib/codemirror.css";

CodeMirror(
  editor => {
    document.getElementById("replace").replaceWith(editor);
  },
  {
    value: `-- SQL Mode for CodeMirror
SELECT SQL_NO_CACHE DISTINCT
    @var1 AS \`val1\`, @'val2', @global.'sql_mode',
    1.1 AS \`float_val\`, .14 AS \`another_float\`, 0.09e3 AS \`int_with_esp\`,
    0xFA5 AS \`hex\`, x'fa5' AS \`hex2\`, 0b101 AS \`bin\`, b'101' AS \`bin2\`,
    DATE '1994-01-01' AS \`sql_date\`, { T "1994-01-01" } AS \`odbc_date\`,
    'my string', _utf8'your string', N'her string',
        TRUE, FALSE, UNKNOWN
  FROM DUAL
  -- space needed after '--'
  # 1 line comment
  /* multiline
  comment! */
  LIMIT 1 OFFSET 0;
`,
    mode: "text/x-sql",
    indentWithTabs: true,
    smartIndent: true,
    lineNumbers: true,
    matchBrackets: true,
    autofocus: true
  }
);

Способ 3: Непосредственно замените Textarea и используйте значение Textarea в качестве начального значения редактора.

<div id="root">
  <textarea id="textarea">
-- SQL Mode for CodeMirror
SELECT SQL_NO_CACHE DISTINCT
    @var1 AS `val1`, @'val2', @global.'sql_mode',
    1.1 AS `float_val`, .14 AS `another_float`, 0.09e3 AS `int_with_esp`,
    0xFA5 AS `hex`, x'fa5' AS `hex2`, 0b101 AS `bin`, b'101' AS `bin2`,
    DATE '1994-01-01' AS `sql_date`, { T "1994-01-01" } AS `odbc_date`,
    'my string', _utf8'your string', N'her string',
        TRUE, FALSE, UNKNOWN
  FROM DUAL
  -- space needed after '--'
  # 1 line comment
  /* multiline
  comment! */
  LIMIT 1 OFFSET 0;
  </textarea>
</div>
import CodeMirror from "codemirror";
// 5.x版本的CodeMirror核心代码已经全部改造成了es6的Module语法;
// 但各种mode文件还依然使用着requireJs的书写语法,所以只要直接引入就好
import "codemirror/mode/sql/sql";
import "codemirror/lib/codemirror.css";

CodeMirror.fromTextArea(document.getElementById("textarea"), {
  mode: "text/x-sql",
  indentWithTabs: true,
  smartIndent: true,
  lineNumbers: true,
  matchBrackets: true,
  autofocus: true
});

Monaco Editor

Методов инициализации Monaco Editor не так много, только следующий, но это не влияет на требования различных сценариев.

<div id="root"></div>
import { editor } from "monaco-editor";

// 一定要保证容器有一定的宽度和高度
editor.create(document.getElementById("root"), {
  language: "sql",
  value: `-- SQL Mode for CodeMirror
  SELECT SQL_NO_CACHE DISTINCT
    @var1 AS \`val1\`, @'val2', @global.'sql_mode',
    1.1 AS \`float_val\`, .14 AS \`another_float\`, 0.09e3 AS \`int_with_esp\`,
    0xFA5 AS \`hex\`, x'fa5' AS \`hex2\`, 0b101 AS \`bin\`, b'101' AS \`bin2\`,
    DATE '1994-01-01' AS \`sql_date\`, { T "1994-01-01" } AS \`odbc_date\`,
    'my string', _utf8'your string', N'her string',
        TRUE, FALSE, UNKNOWN
  FROM DUAL
  -- space needed after '--'
  # 1 line comment
  /* multiline
  comment! */
  LIMIT 1 OFFSET 0;
`
});

Эффект разницы

CodeMirror

CodeMirror имеет режим сравнения в существующем режиме на выбор, конкретные эффекты заключаются в следующем:

.CodeMirror {
  border-top: 1px solid #ddd;
  border-bottom: 1px solid #ddd;
}
span.cm-meta {
  color: #a0b !important;
}
span.cm-error {
  background-color: black;
  opacity: 0.4;
}
span.cm-error.cm-string {
  background-color: red;
}
span.cm-error.cm-tag {
  background-color: #2b2;
}

import CodeMirror from "codemirror";
import "codemirror/mode/diff/diff";
import "codemirror/lib/codemirror.css";
import "./styles.css";

CodeMirror(document.getElementById("root"), {
  value: `diff --git a/index.html b/index.html
index c1d9156..7764744 100644
--- a/index.html
+++ b/index.html
@@ -95,7 +95,8 @@ StringStream.prototype = {
      <script>
        var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
          lineNumbers: true,
-        autoMatchBrackets: true
+        autoMatchBrackets: true,
+      onGutterClick: function(x){console.log(x);}
        });
      </script>
    </body>
diff --git a/lib/codemirror.js b/lib/codemirror.js
index 04646a9..9a39cc7 100644
--- a/lib/codemirror.js
+++ b/lib/codemirror.js
@@ -399,10 +399,16 @@ var CodeMirror = (function() {
      }
  
      function onMouseDown(e) {
-      var start = posFromMouse(e), last = start;    
+      var start = posFromMouse(e), last = start, target = e.target();
        if (!start) return;
        setCursor(start.line, start.ch, false);
        if (e.button() != 1) return;
+      if (target.parentNode == gutter) {    
+        if (options.onGutterClick)
+          options.onGutterClick(indexOf(gutter.childNodes, target) + showingFrom);
+        return;
+      }
+
        if (!focused) onFocus();
  
        e.stop();
@@ -808,7 +814,7 @@ var CodeMirror = (function() {
        for (var i = showingFrom; i < showingTo; ++i) {
          var marker = lines[i].gutterMarker;
          if (marker) html.push('<div class="' + marker.style + '">' + htmlEscape(marker.text) + '</div>');
-        else html.push("<div>" + (options.lineNumbers ? i + 1 : "\u00a0") + "</div>");
+        else html.push("<div>" + (options.lineNumbers ? i + options.firstLineNumber : "\u00a0") + "</div>");
        }
        gutter.style.display = "none"; // TODO test whether this actually helps
        gutter.innerHTML = html.join("");
@@ -1371,10 +1377,8 @@ var CodeMirror = (function() {
          if (option == "parser") setParser(value);
          else if (option === "lineNumbers") setLineNumbers(value);
          else if (option === "gutter") setGutter(value);
-        else if (option === "readOnly") options.readOnly = value;
-        else if (option === "indentUnit") {options.indentUnit = indentUnit = value; setParser(options.parser);}
-        else if (/^(?:enterMode|tabMode|indentWithTabs|readOnly|autoMatchBrackets|undoDepth)$/.test(option)) options[option] = value;
-        else throw new Error("Can't set option " + option);
+        else if (option === "indentUnit") {options.indentUnit = value; setParser(options.parser);}
+        else options[option] = value;
        },
        cursorCoords: cursorCoords,
        undo: operation(undo),
@@ -1402,7 +1406,8 @@ var CodeMirror = (function() {
        replaceRange: operation(replaceRange),
  
        operation: function(f){return operation(f)();},
-      refresh: function(){updateDisplay([{from: 0, to: lines.length}]);}
+      refresh: function(){updateDisplay([{from: 0, to: lines.length}]);},
+      getInputField: function(){return input;}
      };
      return instance;
    }
@@ -1420,6 +1425,7 @@ var CodeMirror = (function() {
      readOnly: false,
      onChange: null,
      onCursorActivity: null,
+    onGutterClick: null,
      autoMatchBrackets: false,
      workTime: 200,
      workDelay: 300,`,
  mode: "text/x-diff",
  indentWithTabs: true,
  smartIndent: true,
  lineNumbers: true,
  matchBrackets: true,
  autofocus: true
});

Из значения примера видно, что при использовании diff вам нужно самостоятельно различать различия содержимого двух файлов, а затем собирать текстовое содержимое, как показано в примере, прежде чем использовать его для достижения ожидаемого эффекта. Схема эффекта выглядит следующим образом:截屏2020-09-02 14.25.44.png

Monaco Editor

Напротив, функция сравнения Monaco Editor гораздо мощнее, см. пример:

import { editor } from "monaco-editor";
import "./styles.css";

const originalModel = editor.createModel(
  `(function (global, undefined) {
  "use strict";
  undefinedVariable = {};
  undefinedVariable.prop = 5;

  function initializeProperties(target, members) {
    var keys = Object.keys(members);
    var properties;
    var i, len;
    for (i = 0, len = keys.length; i < len; i++) {
      var key = keys[i];
      var enumerable = key.charCodeAt(0) !== /*_*/95;
      var member = members[key];
      if (member && typeof member === 'object') {
        if (member.value !== undefined || typeof member.get === 'function' || typeof member.set === 'function') {
          if (member.enumerable === undefined) {
            member.enumerable = enumerable;
          }
          properties = properties || {};
          properties[key] = member;
          continue;
        } 
      }
      // These next lines will be deleted
      if (!enumerable) {
        properties = properties || {};
        properties[key] = { value: member, enumerable: enumerable, configurable: true, writable: true }
        continue;
      }
      target[key] = member;
    }
    if (properties) {
      Object.defineProperties(target, properties);
    }
  }
})(this);`,
  "text/javascript"
);
var modifiedModel = editor.createModel(
  `(function (global, undefined) {
  "use strict";
  var definedVariable = {};
  definedVariable.prop = 5;

  function initializeProperties(target, members) {
    var keys = Object.keys(members);
    var properties;
    var i, len;
    for (i = 0, len = keys.length; i < len; i++) {
      var key = keys[i];
      var enumerable = key.charCodeAt(0) !== /*_*/95;
      var member = members[key];
      if (member && typeof member === 'object') {
        if (member.value !== undefined || typeof member.get === 'function' || typeof member.set === 'function') {
          if (member.enumerable === undefined) {
            member.enumerable = enumerable;
          }
          properties = properties || {};
          properties[key] = member;
          continue;
        } 
      }
      target[key] = member;
    }
    if (properties) {
      Object.defineProperties(target, properties);
    }
  }
})(this);`,
  "text/javascript"
);

const diffEditor = editor.createDiffEditor(
  document.getElementById("root"),
  {
    enableSplitViewResizing: false
  }
);
diffEditor.setModel({
  original: originalModel,
  modified: modifiedModel
});

Как видно из приведенного выше примера, если содержимое, которое необходимо сравнить, предоставлено напрямую, редактор различий Monaco автоматически сравнит и затем отобразит его. Можно настроить отображение эффекта в одном редакторе или отображение эффекта в двух редакторах. Это гораздо удобнее, чем режим сравнения CodeMirror. Схема эффекта выглядит следующим образом:截屏2020-09-02 14.26.41.png

Масштабируемость

С точки зрения расширения автор сравнит и представит два аспекта «добавление нового языка (мода)» и «расширение функций».

Добавить новый язык (режим)

В настоящее время CodeMirror поддерживает более 100 типов.mode(язык), Monaco Editor поддерживает десятки из них, и оба охватывают почти основные языки. Кроме того, в этих двух редакторах есть возможность настройки языка, рассмотрим их отдельно.

CodeMirror

CodeMirror может пройтиCodeMirror.defineModeзарегистрировать новый режим (справочная документация). Схематический код выглядит следующим образом:

// 第一个参数:用小写字母命名的modeName
// 第二个参数:回调函数,返回模式对象
CodeMirror.defineMode("sql", function(config, parserConfig) {
  // config 是CodeMirror的配置对象
  // parserConfig 是可选的模式配置对象
  
  // 你的模式定义代码;解析编辑器的内容
  
  return {
    token: (stream, state) => { return style }, // 必选,返回高亮样式
    indent: (state, textAfter) => { return 0 }, // 可选,定义缩进规则,返回缩进空格数
    // 其他可选项
  };
}

CodeMirror также предоставляетinnerModeИспользуется для сценариев с вложенным синтаксисом, таких как HTML, где смешаны синтаксис CSS и JavaScript. Схематический код выглядит следующим образом:

// 代码来源于CodeMirror源码
CodeMirror.defineMode("htmlmixed", function (config, parserConfig) {
	var htmlMode = CodeMirror.getMode(config, {
      name: "xml",
      htmlMode: true
    });
  
  return {
    // 定义起始状态,这里可以区分当前是在哪个标签内(style/script)
    startState: function () {
      var state = CodeMirror.startState(htmlMode);
      return {token: html, inTag: null, localMode: null, localState: null, htmlState: state};
    },
  	token: function (stream, state) {
      return state.token(stream, state);
    },
    innerMode: function (state) {
      return {state: state.localState || state.htmlState, mode: state.localMode || htmlMode};
    },
    // 其他项已经被省略,需要的小伙伴请自行查阅CodeMirror源码
  };
}, "xml", "javascript", "css");

Примечание. Чтобы узнать, как использовать вложенные комбинации нескольких режимов, вы можете обратиться ксправочная документация режимасодержание, а в исходном кодеmode/htmlmixedсодержание в .

После того, как режим определен в CodeMirror, его необходимо связать с MIME.Режим, указанный при его использовании, на самом деле является MIME. Автор считает, что CodeMirror предназначен для решения проблемы определения сходных языков. Это очень распространено в языке SQL.Существует много видов реляционных баз данных.Основной синтаксис этих баз данных в основном одинаков, но нестандартный SQL (такой как MySQL, SQLServer и т. д.), используемый во многих базах данных, имеет свои особенности. собственные характеристики, поэтому используйте MIME для определения. Другой язык SQL идеально подходит. Пример определения MIME выглядит следующим образом:

CodeMirror.defineMIME("text/x-mysql", {
  name: 'sql', // name 对应关联的mode
  // 其他配置项,会作为 defineMode 第二个回调函数的第二个入参传入
});

Кроме того, CodeMirror также предоставляет определенияSimple ModeформаCodeMirror.defineSimpleModeСовершенно новый язык можно определить более удобно, и заинтересованные читатели могут узнать о нем самостоятельно.

Monaco Editor

Monaco Editor также может регистрировать новые языки, простой пример:

// Register a new language
monaco.languages.register({ id: 'mySpecialLanguage' });

// Register a tokens provider for the language
// 定义新语言的主体内容在这里
monaco.languages.setMonarchTokensProvider('mySpecialLanguage', {
	tokenizer: { // 相当于CodeMirror中的token;
		root: [
			[/\[error.*/, "custom-error"], // 第一项是匹配规则,第二项是token名
			[/\[notice.*/, "custom-notice"],
			[/\[info.*/, "custom-info"],
			[/\[[a-zA-Z 0-9:]+\]/, "custom-date"],
		]
	}
});

// Define a new theme that contains only rules that match this language
// 在CodeMirror中是直接在css文件中定义样式
monaco.editor.defineTheme('myCoolTheme', {
	base: 'vs',
	inherit: false,
	rules: [
    // 这里的token跟上面的tokenizer一一对应,
		{ token: 'custom-info', foreground: '808080' },
		{ token: 'custom-error', foreground: 'ff0000', fontStyle: 'bold' },
		{ token: 'custom-notice', foreground: 'FFA500' },
		{ token: 'custom-date', foreground: '008800' },
	]
});

Определение языка Monaco настраивается, а вложение между языками осуществляется черезnextEmbeddedатрибут для настройки в теге HTMLstyleОпределение тега выглядит следующим образом (дополнительную информацию см.Monarch):

root: [
  [/<style\s*>/,   { token: 'keyword', bracket: '@open'
                   , next: '@css_block', nextEmbedded: 'text/css' }],

  [/<\/style\s*>/, { token: 'keyword', bracket: '@close' }],
  ...
]

Monaco кажется проще в настройке, но на самом деле, чтобы понять смысл каждого атрибута конфигурации, стоимость обучения его использованию в начале немного высока.

расширение функции

В редакторе нашими наиболее распространенными функциями являются интеллектуальные подсказки, свертывание кода, автоматические закрывающие символы (такие как '', "") и так далее. Все они дополнительно подгружаются в виде аддонов в CodeMirror, а в Monaco уже встроены, главное, чтобы они были настроены и использовались по мере необходимости. Давайте возьмем умные подсказки в качестве примера, чтобы почувствовать разницу между ними.

CodeMirror
import CodeMirror from 'codemirror';
import 'codemirror/mode/sql/sql';
import 'codemirror/lib/codemirror.css';
// 引入智能提示插件
import 'codemirror/addon/hint/show-hint';
import 'codemirror/addon/hint/sql-hint';
import 'codemirror/addon/hint/show-hint.css';

const myCodeMirror = CodeMirror(document.getElementById('root'), {
  value: `-- SQL Mode for CodeMirror
SELECT SQL_NO_CACHE DISTINCT
    @var1 AS \`val1\`, @'val2', @global.'sql_mode',
    1.1 AS \`float_val\`, .14 AS \`another_float\`, 0.09e3 AS \`int_with_esp\`,
    0xFA5 AS \`hex\`, x'fa5' AS \`hex2\`, 0b101 AS \`bin\`, b'101' AS \`bin2\`,
    DATE '1994-01-01' AS \`sql_date\`, { T "1994-01-01" } AS \`odbc_date\`,
    'my string', _utf8'your string', N'her string',
        TRUE, FALSE, UNKNOWN
  FROM DUAL
  -- space needed after '--'
  # 1 line comment
  /* multiline
  comment! */
  LIMIT 1 OFFSET 0;
`,
  mode: 'text/x-sql',
  indentWithTabs: true,
  smartIndent: true,
  lineNumbers: true,
  matchBrackets: true,
  autofocus: true,
});

myCodeMirror.on('change', (cm, changeObj) => {、
  const { origin, text = [] } = changeObj;
  if (origin === '+input' && text[0]) {
    // 执行只能提示
    cm.execCommand('autocomplete');
  }

});
Monaco Editor
monaco.editor.create(document.getElementById("container"), {
    value: `-- SQL Mode for CodeMirror
SELECT SQL_NO_CACHE DISTINCT
    @var1 AS \`val1\`, @'val2', @global.'sql_mode',
    1.1 AS \`float_val\`, .14 AS \`another_float\`, 0.09e3 AS \`int_with_esp\`,
    0xFA5 AS \`hex\`, x'fa5' AS \`hex2\`, 0b101 AS \`bin\`, b'101' AS \`bin2\`,
    DATE '1994-01-01' AS \`sql_date\`, { T "1994-01-01" } AS \`odbc_date\`,
    'my string', _utf8'your string', N'her string',
        TRUE, FALSE, UNKNOWN
  FROM DUAL
  -- space needed after '--'
  # 1 line comment
  /* multiline
  comment! */
  LIMIT 1 OFFSET 0;
`,
    language: "sql"
});

monaco.languages.registerCompletionItemProvider('json', {
  provideCompletionItems: function(model, position) {
    return {
      suggestions: [
        {
          label: '"lodash"',
          kind: monaco.languages.CompletionItemKind.Function,
          documentation: "The Lodash library exported as Node.js modules.",
          insertText: '"lodash": "*"',
          range: range
        },
      ];
    };
  }
});

При сравнении этих двух редакторов не сказано, какой из них более масштабируемый. Но автор считает, что инкапсуляция CodeMirror более свободная, и разработчики могут писать код на его основе «как хотят», и даже могут напрямую модифицировать исходный код мода или аддона; инкапсуляция Monaco Editor более строгая, и разработчики необходимо определить его в своей вторичной разработке, осуществляемой в рамках правил.

представление

Размер основного файла CodeMirror составляет всего 70+ КБ после сжатия, тогда как размер Monaco Editor после сжатия составляет 1,9 МБ, поэтому производительность CodeMirror во время инициализации немного выше. Причина такой большой разницы в файлах пакетов заключается в том, что автор считает, что благодаря свободной инкапсуляции CodeMirror, будь то обработка функций или языков, какие файлы необходимо использовать и какие файлы вводятся для решения; и Инкапсуляция Monaco Editor более Строго, будь то расширение функций редактора или новый язык, в Monaco это делается в виде конфигурации, а это значит, что парсинг конфигурации производится внутри редактора.

При обработке большого текста производительность Monaco Editor еще выше. Автор скопировал более 87M SQL-контента (около 2,7 млн ​​строк контента) в два редактора, Monaco Editor застрял, но его все еще можно было нормально разобрать и отредактировать; но CodeMirror был в основном в приостановленном состоянии, потребовалось много времени, чтобы смотрите рендеринг. При нормальных обстоятельствах наш текстовый контент не будет таким большим, и CodeMirror не обязан обрабатывать контент размером более десяти мегабайт.

Я не собираюсь судить, какой из двух редакторов с открытым исходным кодом лучше — оба, несомненно, превосходны. Читатели могут свободно выбирать любой из них для использования в соответствии со своими предпочтениями.Я считаю, что независимо от того, какой из них используется, он вполне может удовлетворить потребности читателей.

Вышесказанное является личным мнением автора, если есть ошибки, прошу покритиковать и исправить!

Использованная литература: Справочная документация CodeMirror:codemirror.net/Исходный код CodeMirror:GitHub.com/co зеркало/…Справочная документация редактора Monaco:Microsoft.GitHub.IO/Monaco-edit…

Категории