CodeMirror 是一款非常老牌的Web编辑器,目前已经发展到 v6 版本,不过 v6 仍处于测试阶段,今天笔者以5.57.0版本进行介绍。Monaco Editor 相较就年轻得多了,尽管如此而它的名气却一点都不小;只因它与VSCode 使用的同一款核心代码。接下来笔者将从“使用方法”、“可扩展性”、“性能”三方面来对这两款编辑器进行比较。

使用方法

初始化方法

相对来说 CodeMirror 的初始化方式比较多,而 Monaco 仅有一种(这不影响各种功能的实现);但 Monaco 胜在自带 Diff 编辑器。

CodeMirror

方式一:插入到某个容器节点内,比如 body,或者 #root。

  1. <div id="root"></div>
  1. import CodeMirror from "codemirror";
  2. import "codemirror/mode/sql/sql";
  3. import "codemirror/lib/codemirror.css";
  4. CodeMirror(document.getElementById("root"), {
  5. value: `-- SQL Mode for CodeMirror
  6. SELECT SQL_NO_CACHE DISTINCT
  7. @var1 AS \`val1\`, @'val2', @global.'sql_mode',
  8. 1.1 AS \`float_val\`, .14 AS \`another_float\`, 0.09e3 AS \`int_with_esp\`,
  9. 0xFA5 AS \`hex\`, x'fa5' AS \`hex2\`, 0b101 AS \`bin\`, b'101' AS \`bin2\`,
  10. DATE '1994-01-01' AS \`sql_date\`, { T "1994-01-01" } AS \`odbc_date\`,
  11. 'my string', _utf8'your string', N'her string',
  12. TRUE, FALSE, UNKNOWN
  13. FROM DUAL
  14. -- space needed after '--'
  15. # 1 line comment
  16. /* multiline
  17. comment! */
  18. LIMIT 1 OFFSET 0;
  19. `,
  20. mode: "text/x-sql",
  21. indentWithTabs: true,
  22. smartIndent: true,
  23. lineNumbers: true,
  24. matchBrackets: true,
  25. autofocus: true
  26. });

方式二:在函数中做任何节点操作,比如 replaceWith (替换某节点)、insertBefore (插入在某节点前面)、append 等等。

  1. <div id="root">
  2. <div id="replace"></div>
  3. </div>
  1. import CodeMirror from "codemirror";
  2. import "codemirror/mode/sql/sql";
  3. import "codemirror/lib/codemirror.css";
  4. CodeMirror(
  5. editor => {
  6. document.getElementById("replace").replaceWith(editor);
  7. },
  8. {
  9. value: `-- SQL Mode for CodeMirror
  10. SELECT SQL_NO_CACHE DISTINCT
  11. @var1 AS \`val1\`, @'val2', @global.'sql_mode',
  12. 1.1 AS \`float_val\`, .14 AS \`another_float\`, 0.09e3 AS \`int_with_esp\`,
  13. 0xFA5 AS \`hex\`, x'fa5' AS \`hex2\`, 0b101 AS \`bin\`, b'101' AS \`bin2\`,
  14. DATE '1994-01-01' AS \`sql_date\`, { T "1994-01-01" } AS \`odbc_date\`,
  15. 'my string', _utf8'your string', N'her string',
  16. TRUE, FALSE, UNKNOWN
  17. FROM DUAL
  18. -- space needed after '--'
  19. # 1 line comment
  20. /* multiline
  21. comment! */
  22. LIMIT 1 OFFSET 0;
  23. `,
  24. mode: "text/x-sql",
  25. indentWithTabs: true,
  26. smartIndent: true,
  27. lineNumbers: true,
  28. matchBrackets: true,
  29. autofocus: true
  30. }
  31. );

方式三:直接替换某个 Textarea,并将该 Textarea 的值做为编辑器的初始值。

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

Monaco Editor

Monaco Editor 的初始化方式没有那么多,仅有下面一种,但这并不影响满足各种场景需求。

  1. <div id="root"></div>
  1. import { editor } from "monaco-editor";
  2. // 一定要保证容器有一定的宽度和高度
  3. editor.create(document.getElementById("root"), {
  4. language: "sql",
  5. value: `-- SQL Mode for CodeMirror
  6. SELECT SQL_NO_CACHE DISTINCT
  7. @var1 AS \`val1\`, @'val2', @global.'sql_mode',
  8. 1.1 AS \`float_val\`, .14 AS \`another_float\`, 0.09e3 AS \`int_with_esp\`,
  9. 0xFA5 AS \`hex\`, x'fa5' AS \`hex2\`, 0b101 AS \`bin\`, b'101' AS \`bin2\`,
  10. DATE '1994-01-01' AS \`sql_date\`, { T "1994-01-01" } AS \`odbc_date\`,
  11. 'my string', _utf8'your string', N'her string',
  12. TRUE, FALSE, UNKNOWN
  13. FROM DUAL
  14. -- space needed after '--'
  15. # 1 line comment
  16. /* multiline
  17. comment! */
  18. LIMIT 1 OFFSET 0;
  19. `
  20. });

Diff 效果

CodeMirror

CodeMirror 在已有的 mode 中有一种 diff mode 可供选择使用,具体效果如下:

  1. .CodeMirror {
  2. border-top: 1px solid #ddd;
  3. border-bottom: 1px solid #ddd;
  4. }
  5. span.cm-meta {
  6. color: #a0b !important;
  7. }
  8. span.cm-error {
  9. background-color: black;
  10. opacity: 0.4;
  11. }
  12. span.cm-error.cm-string {
  13. background-color: red;
  14. }
  15. span.cm-error.cm-tag {
  16. background-color: #2b2;
  17. }
  1. import CodeMirror from "codemirror";
  2. import "codemirror/mode/diff/diff";
  3. import "codemirror/lib/codemirror.css";
  4. import "./styles.css";
  5. CodeMirror(document.getElementById("root"), {
  6. value: `diff --git a/index.html b/index.html
  7. index c1d9156..7764744 100644
  8. --- a/index.html
  9. +++ b/index.html
  10. @@ -95,7 +95,8 @@ StringStream.prototype = {
  11. <script>
  12. var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
  13. lineNumbers: true,
  14. - autoMatchBrackets: true
  15. + autoMatchBrackets: true,
  16. + onGutterClick: function(x){console.log(x);}
  17. });
  18. </script>
  19. </body>
  20. diff --git a/lib/codemirror.js b/lib/codemirror.js
  21. index 04646a9..9a39cc7 100644
  22. --- a/lib/codemirror.js
  23. +++ b/lib/codemirror.js
  24. @@ -399,10 +399,16 @@ var CodeMirror = (function() {
  25. }
  26. function onMouseDown(e) {
  27. - var start = posFromMouse(e), last = start;
  28. + var start = posFromMouse(e), last = start, target = e.target();
  29. if (!start) return;
  30. setCursor(start.line, start.ch, false);
  31. if (e.button() != 1) return;
  32. + if (target.parentNode == gutter) {
  33. + if (options.onGutterClick)
  34. + options.onGutterClick(indexOf(gutter.childNodes, target) + showingFrom);
  35. + return;
  36. + }
  37. +
  38. if (!focused) onFocus();
  39. e.stop();
  40. @@ -808,7 +814,7 @@ var CodeMirror = (function() {
  41. for (var i = showingFrom; i < showingTo; ++i) {
  42. var marker = lines[i].gutterMarker;
  43. if (marker) html.push('<div class="' + marker.style + '">' + htmlEscape(marker.text) + '</div>');
  44. - else html.push("<div>" + (options.lineNumbers ? i + 1 : "\u00a0") + "</div>");
  45. + else html.push("<div>" + (options.lineNumbers ? i + options.firstLineNumber : "\u00a0") + "</div>");
  46. }
  47. gutter.style.display = "none"; // TODO test whether this actually helps
  48. gutter.innerHTML = html.join("");
  49. @@ -1371,10 +1377,8 @@ var CodeMirror = (function() {
  50. if (option == "parser") setParser(value);
  51. else if (option === "lineNumbers") setLineNumbers(value);
  52. else if (option === "gutter") setGutter(value);
  53. - else if (option === "readOnly") options.readOnly = value;
  54. - else if (option === "indentUnit") {options.indentUnit = indentUnit = value; setParser(options.parser);}
  55. - else if (/^(?:enterMode|tabMode|indentWithTabs|readOnly|autoMatchBrackets|undoDepth)$/.test(option)) options[option] = value;
  56. - else throw new Error("Can't set option " + option);
  57. + else if (option === "indentUnit") {options.indentUnit = value; setParser(options.parser);}
  58. + else options[option] = value;
  59. },
  60. cursorCoords: cursorCoords,
  61. undo: operation(undo),
  62. @@ -1402,7 +1406,8 @@ var CodeMirror = (function() {
  63. replaceRange: operation(replaceRange),
  64. operation: function(f){return operation(f)();},
  65. - refresh: function(){updateDisplay([{from: 0, to: lines.length}]);}
  66. + refresh: function(){updateDisplay([{from: 0, to: lines.length}]);},
  67. + getInputField: function(){return input;}
  68. };
  69. return instance;
  70. }
  71. @@ -1420,6 +1425,7 @@ var CodeMirror = (function() {
  72. readOnly: false,
  73. onChange: null,
  74. onCursorActivity: null,
  75. + onGutterClick: null,
  76. autoMatchBrackets: false,
  77. workTime: 200,
  78. workDelay: 300,`,
  79. mode: "text/x-diff",
  80. indentWithTabs: true,
  81. smartIndent: true,
  82. lineNumbers: true,
  83. matchBrackets: true,
  84. autofocus: true
  85. });

从示例的 value 值可见,在使用 diff 时需要自己先将两个文件的内容差异区分出来,然后组装成如示例所示的文本内容后才能使用,以达到预期效果。效果图如下:
截屏2020-09-02 14.25.44.png

Monaco Editor

相比之下 Monaco Editor 的 diff 功能就强大得多了,请看示例:

  1. import { editor } from "monaco-editor";
  2. import "./styles.css";
  3. const originalModel = editor.createModel(
  4. `(function (global, undefined) {
  5. "use strict";
  6. undefinedVariable = {};
  7. undefinedVariable.prop = 5;
  8. function initializeProperties(target, members) {
  9. var keys = Object.keys(members);
  10. var properties;
  11. var i, len;
  12. for (i = 0, len = keys.length; i < len; i++) {
  13. var key = keys[i];
  14. var enumerable = key.charCodeAt(0) !== /*_*/95;
  15. var member = members[key];
  16. if (member && typeof member === 'object') {
  17. if (member.value !== undefined || typeof member.get === 'function' || typeof member.set === 'function') {
  18. if (member.enumerable === undefined) {
  19. member.enumerable = enumerable;
  20. }
  21. properties = properties || {};
  22. properties[key] = member;
  23. continue;
  24. }
  25. }
  26. // These next lines will be deleted
  27. if (!enumerable) {
  28. properties = properties || {};
  29. properties[key] = { value: member, enumerable: enumerable, configurable: true, writable: true }
  30. continue;
  31. }
  32. target[key] = member;
  33. }
  34. if (properties) {
  35. Object.defineProperties(target, properties);
  36. }
  37. }
  38. })(this);`,
  39. "text/javascript"
  40. );
  41. var modifiedModel = editor.createModel(
  42. `(function (global, undefined) {
  43. "use strict";
  44. var definedVariable = {};
  45. definedVariable.prop = 5;
  46. function initializeProperties(target, members) {
  47. var keys = Object.keys(members);
  48. var properties;
  49. var i, len;
  50. for (i = 0, len = keys.length; i < len; i++) {
  51. var key = keys[i];
  52. var enumerable = key.charCodeAt(0) !== /*_*/95;
  53. var member = members[key];
  54. if (member && typeof member === 'object') {
  55. if (member.value !== undefined || typeof member.get === 'function' || typeof member.set === 'function') {
  56. if (member.enumerable === undefined) {
  57. member.enumerable = enumerable;
  58. }
  59. properties = properties || {};
  60. properties[key] = member;
  61. continue;
  62. }
  63. }
  64. target[key] = member;
  65. }
  66. if (properties) {
  67. Object.defineProperties(target, properties);
  68. }
  69. }
  70. })(this);`,
  71. "text/javascript"
  72. );
  73. const diffEditor = editor.createDiffEditor(
  74. document.getElementById("root"),
  75. {
  76. enableSplitViewResizing: false
  77. }
  78. );
  79. diffEditor.setModel({
  80. original: originalModel,
  81. modified: modifiedModel
  82. });

已上面的示例看出,只要直接提供需要比较的内容,Monaco 的 diff editor 就会自动进行对比,然后显示出来。是在一个编辑器内显示,还是分两个编辑器显示效果,是可以进行配置的。这一点比起 CodeMirror 的 diff mode 方便太多了。效果图如下:
截屏2020-09-02 14.26.41.png

可扩展性

在扩展方面,笔者将从“新增一种新的语言( mode )”、“功能扩展”两方面进行对比介绍。

增加一种新的语言(mode)

目前 CodeMirror 支持100+种 mode (语言),Monaco Editor 支持几十种,两者都几乎涵盖了主流语言。另外这两款编辑器都具备自定义语言的能力,下面我们分别来看。

CodeMirror

CodeMirror 可以通过 CodeMirror.defineMode 来注册一种新的 mode (帮助文档)。示意代码如下:

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

CodeMirror 还提供了 innerMode 用于嵌套语法的场景,比如 HTML 中会混杂 CSS、JavaScript 语法。示意代码如下:

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

注:想要了解嵌套组合多个 mode 如何运用,可以参考 mode 帮助文档内容,和源码中的 mode/htmlmixed 中的内容。

在 CodeMirror 中定义了 mode 之后还需要跟 MIME 关联起来,在使用时指定的 mode 其实是 MIME。笔者认为CodeMirror 如此设计解决了相似语言的快捷定义。这点在 SQL 语言中很常见,关系型数据库有很多种,这些数据库的基本语法基本一致,但很多数据库使用的非标准 SQL (如 MySQL、SQLServer 等),有自己的特点,那么使用MIME来定义不同的 SQL 语言就极为合适了。定义MIME的示例如下:

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

另外 CodeMirror 还提供了定义 Simple Mode 的形式 CodeMirror.defineSimpleMode 可以更便捷的定义一种全新的语言,读者有兴趣可以自行了解。

Monaco Editor

Monaco Editor也可以注册新语言,简单示例如下:

  1. // Register a new language
  2. monaco.languages.register({ id: 'mySpecialLanguage' });
  3. // Register a tokens provider for the language
  4. // 定义新语言的主体内容在这里
  5. monaco.languages.setMonarchTokensProvider('mySpecialLanguage', {
  6. tokenizer: { // 相当于CodeMirror中的token;
  7. root: [
  8. [/\[error.*/, "custom-error"], // 第一项是匹配规则,第二项是token名
  9. [/\[notice.*/, "custom-notice"],
  10. [/\[info.*/, "custom-info"],
  11. [/\[[a-zA-Z 0-9:]+\]/, "custom-date"],
  12. ]
  13. }
  14. });
  15. // Define a new theme that contains only rules that match this language
  16. // 在CodeMirror中是直接在css文件中定义样式
  17. monaco.editor.defineTheme('myCoolTheme', {
  18. base: 'vs',
  19. inherit: false,
  20. rules: [
  21. // 这里的token跟上面的tokenizer一一对应,
  22. { token: 'custom-info', foreground: '808080' },
  23. { token: 'custom-error', foreground: 'ff0000', fontStyle: 'bold' },
  24. { token: 'custom-notice', foreground: 'FFA500' },
  25. { token: 'custom-date', foreground: '008800' },
  26. ]
  27. });

Monaco 的语言定义是配置化的,语言之间的嵌套是通过 nextEmbedded 属性来进行配置的,HTML标签中的 style 标签的定义如下(更多内容可以参考 Monarch):

  1. root: [
  2. [/<style\s*>/, { token: 'keyword', bracket: '@open'
  3. , next: '@css_block', nextEmbedded: 'text/css' }],
  4. [/<\/style\s*>/, { token: 'keyword', bracket: '@close' }],
  5. ...
  6. ]

Monaco 看似配置化更简单,实则要理解每个配置属性的含义,一开始学习使用的成本略微有点高。

功能扩展

在编辑器中,我们最常见的功能有智能提示、代码折叠、自动闭合字符(如’’, “”)等。所有这些在 CodeMirror 中都是以 addon 的形式额外载入的,而在 Monaco 都已经内置,只要根据需要进行配置使用即可。下面我们以智能提示为例,来感受下两者的区别。

CodeMirror
  1. import CodeMirror from 'codemirror';
  2. import 'codemirror/mode/sql/sql';
  3. import 'codemirror/lib/codemirror.css';
  4. // 引入智能提示插件
  5. import 'codemirror/addon/hint/show-hint';
  6. import 'codemirror/addon/hint/sql-hint';
  7. import 'codemirror/addon/hint/show-hint.css';
  8. const myCodeMirror = CodeMirror(document.getElementById('root'), {
  9. value: `-- SQL Mode for CodeMirror
  10. SELECT SQL_NO_CACHE DISTINCT
  11. @var1 AS \`val1\`, @'val2', @global.'sql_mode',
  12. 1.1 AS \`float_val\`, .14 AS \`another_float\`, 0.09e3 AS \`int_with_esp\`,
  13. 0xFA5 AS \`hex\`, x'fa5' AS \`hex2\`, 0b101 AS \`bin\`, b'101' AS \`bin2\`,
  14. DATE '1994-01-01' AS \`sql_date\`, { T "1994-01-01" } AS \`odbc_date\`,
  15. 'my string', _utf8'your string', N'her string',
  16. TRUE, FALSE, UNKNOWN
  17. FROM DUAL
  18. -- space needed after '--'
  19. # 1 line comment
  20. /* multiline
  21. comment! */
  22. LIMIT 1 OFFSET 0;
  23. `,
  24. mode: 'text/x-sql',
  25. indentWithTabs: true,
  26. smartIndent: true,
  27. lineNumbers: true,
  28. matchBrackets: true,
  29. autofocus: true,
  30. });
  31. myCodeMirror.on('change', (cm, changeObj) => {、
  32. const { origin, text = [] } = changeObj;
  33. if (origin === '+input' && text[0]) {
  34. // 执行只能提示
  35. cm.execCommand('autocomplete');
  36. }
  37. });

Monaco Editor
  1. monaco.editor.create(document.getElementById("container"), {
  2. value: `-- SQL Mode for CodeMirror
  3. SELECT SQL_NO_CACHE DISTINCT
  4. @var1 AS \`val1\`, @'val2', @global.'sql_mode',
  5. 1.1 AS \`float_val\`, .14 AS \`another_float\`, 0.09e3 AS \`int_with_esp\`,
  6. 0xFA5 AS \`hex\`, x'fa5' AS \`hex2\`, 0b101 AS \`bin\`, b'101' AS \`bin2\`,
  7. DATE '1994-01-01' AS \`sql_date\`, { T "1994-01-01" } AS \`odbc_date\`,
  8. 'my string', _utf8'your string', N'her string',
  9. TRUE, FALSE, UNKNOWN
  10. FROM DUAL
  11. -- space needed after '--'
  12. # 1 line comment
  13. /* multiline
  14. comment! */
  15. LIMIT 1 OFFSET 0;
  16. `,
  17. language: "sql"
  18. });
  19. monaco.languages.registerCompletionItemProvider('json', {
  20. provideCompletionItems: function(model, position) {
  21. return {
  22. suggestions: [
  23. {
  24. label: '"lodash"',
  25. kind: monaco.languages.CompletionItemKind.Function,
  26. documentation: "The Lodash library exported as Node.js modules.",
  27. insertText: '"lodash": "*"',
  28. range: range
  29. },
  30. ];
  31. };
  32. }
  33. });

在这两款编辑器的比较中,没有说哪一款的扩展性更好。但笔者觉得,CodeMirror 的封装更为松散些,开发者可以基于它“随心所欲”的写代码,甚至可以直接修改 mode 或者 addon 的源码;Monaco Editor 的封装就要更加严密,开发者必须要在其定义的框架规则之内进行二次开发。

性能

CodeMirror 核心文件压缩后仅70+KB,而 Monaco Editor 压缩后则有1.9M,所以在初始化时 CodeMirror 的性能略胜一筹。在包文件上有如此大的差异的原因,笔者认为得益于 CodeMirror 的松散封装,无论是功能还是语言的处理都是需要用到什么,引入什么文件来解决的;而 Monaco Editor 的封装更为严密,无论是编辑器的功能扩展还是新增语言,在 Monaco 中都是以配置的形式完成的,这也就意味着配置的解析都是在编辑器内部完成的。

在大文本处理上,Monaco Editor 的表现就更为优异了。笔者将87M多的 SQL 内容(大概有 270 多万行内容)分别拷贝入两个编辑器内,Monaco Editor有卡顿感,但还是能正常解析并进行编辑;但 CodeMirror 就基本处于假死状态了,等了很久都没见渲染出来。通常情况下,我们的文本内容都不会有这么大,对于十几兆的内容CodeMirror处理起来也是毫无压力的。

笔者无意于评价这两款开源编辑器谁更优秀——无疑两者都是非常优秀的。读者大可以根据自己的喜好自由选择其中任何一款进行使用,笔者相信无论是哪一款都可以很好的满足读者的需求。

以上为笔者个人的见解,如有错误,欢迎批评指正!

参考资料:
CodeMirror 帮助文档:https://codemirror.net/
CodeMirror 源码:https://github.com/codemirror/CodeMirror
Monaco Editor帮助文档:https://microsoft.github.io/monaco-editor/index.html