likes
comments
collection
share

实现一个基于monaco-editor的编辑器组件【bysking】实现一个基于monaco-editor的编辑器组件

作者站长头像
站长
· 阅读数 51

实现一个基于monaco-editor的编辑器组件

  • 支持指定语言的语法校验
  • 支持自定义提示词语
  • 支持语法高亮(默认)

实现一个基于monaco-editor的编辑器组件【bysking】实现一个基于monaco-editor的编辑器组件

安装核心依赖npm包

  "monaco-editor": "^0.36.1",
  "monaco-sql-languages": "^0.12.2"
  • 如何获取校验worker

  • 新建webpack打包项目

    • yarn init -y 初始化一个项目

    • 安装依赖:webpack, webpack-cli

    • 新建配置文件: webpack.config.js


const path = require('path');

module.exports = {
  entry: {
     // 添加想要的worker自行打包
    // 'editor.worker': 'monaco-editor/esm/vs/editor/editor.worker.js',
    'mysql.worker': 'monaco-sql-languages/esm/languages/mysql/mysql.worker.js',
  },
  output: {
    globalObject: 'self',
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [],
  },
};
  • package.json增加构建脚本,默认就是用 webpack.config.js 的配置进行打包

{
  "name": "webpack-worker",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "webpack": "^5.93.0",
    "webpack-cli": "^5.1.4"
  },
  "dependencies": {
    "monaco-editor": "^0.36.1",
    "monaco-sql-languages": "^0.12.2"
  }
}

这样我们就得到了:

mysql.worker.js

把这个文件添加到dis产物目录: lib/mysql.worker.js

  • 实现worker文件加载地址获取:monaco-worker.js

import 'monaco-sql-languages/esm/languages/mysql/mysql.contribution'; // https://www.npmjs.com/package/monaco-sql-languages

const customeGetUrl = (moduleId, label) => {
  const publicPath = process.env.UMI_ENV === 'local' ? '/' : '/tw-sub/';

  const pathUrl =
    window.location.origin + publicPath + `lib/${label}.worker.js`; // 就是上面的worker,用于校验

  // 加载自定义校验worker
  if (['mysql'].includes(label)) {
    return pathUrl;
  }

  return '';
};

const originFn: any = window.MonacoEnvironment?.getWorkerUrl;
if (originFn && window.MonacoEnvironment) {
  window.MonacoEnvironment.getWorkerUrl = (_moduleId, label) => {
    const str = customeGetUrl(_moduleId, label);

    if (str) {
      return str;
    }

    return originFn.call(this, _moduleId, label);
  };
}

  • 组件代码:
import * as monaco from 'monaco-editor';
import { useEffect, useRef } from 'react';
import './monaco-worker';
import { destroyLang, registerLang } from './tool';

interface typeProps {
  readOnly: boolean;
  value?: string;
  onChange?: (value?: string) => void;
  options?: Record<string, unknown>;
}

const MonacoCodeEditor = (props: typeProps) => {
  const { readOnly, value, onChange } = props;

  const monacoEditor = useRef<any>();
  const refCodeContainer = useRef();
  const lastEditorValue = useRef<string>(value);

  // [https://github.com/microsoft/monaco-editor/issues/3358]
  const setMonacoEditorValue = (
    editor: monaco.editor.ICodeEditor | monaco.editor.IStandaloneCodeEditor,
    val: string,
  ) => {
    if (!editor) {
      return;
    }
    editor.updateOptions({ readOnly: false });

    editor.pushUndoStop();
    editor.executeEdits('name-of-edit', [
      {
        // @ts-ignore
        range: editor.getModel()?.getFullModelRange(), // full range
        text: val || '', // target value here
      },
    ]);

    editor.pushUndoStop();
    editor.updateOptions({ readOnly: readOnly });
  };

  const init = () => {
    if (monacoEditor.current) {
      monacoEditor.current?.dispose();
    }
    monacoEditor.current = monaco.editor.create(refCodeContainer.current, {
      value: value,
      language: 'mysql',
      automaticLayout: true, // 自动布局
      theme: 'vs',
      readOnly: readOnly,
      scrollBeyondLastLine: false,
      foldingStrategy: 'indentation', // 代码可分小段折叠
      overviewRulerBorder: false, // 不要滚动条的边框
      cursorStyle: 'line', // 光标样式
      fixedOverflowWidgets: true, // hover提示框不被遮挡
      scrollbar: {
        useShadows: true,
        verticalHasArrows: true,
        horizontalHasArrows: true,
        vertical: 'visible',
        horizontal: 'visible',
        verticalScrollbarSize: 10,
        horizontalScrollbarSize: 10,
        arrowSize: 10,
      },
      minimap: {
        enabled: true,
      },
    });

    registerLang(monaco);

    monacoEditor.current?.onDidChangeCursorPosition(() => {
      const val = monacoEditor.current?.getValue();

      if (val !== lastEditorValue.current) {
        onChange?.(val || '');
        lastEditorValue.current = val || '';
      }
    });
  };

  useEffect(() => {
    init();

    return () => {
      monacoEditor.current?.dispose();
      destroyLang();
    };
  }, []);

  useEffect(() => {
    if (monacoEditor.current) {
      setMonacoEditorValue(monacoEditor.current, value || '');
    }
  }, [value]);

  useEffect(() => {
    monacoEditor.current?.updateOptions({ readOnly: !!readOnly });
  }, [readOnly]);

  useEffect(() => {
    setMonacoEditorValue(monacoEditor.current, value);
  }, [value]);

  return (
    <div
      style={{
        border: '1px solid gray',
        height: '20vh',
      }}
    >
      <div ref={refCodeContainer} style={{ height: '100%' }}></div>
    </div>
  );
};

export default MonacoCodeEditor;

  • 实现提示词注册逻辑

import { language as mysqlLanguage } from 'monaco-sql-languages/esm/languages/mysql/mysql.js';

const sqlLanguagesList = ['mysql'];
type typeSql = 'mysql';

// 语言数据对象
export const languageMap = {
  mysql: mysqlLanguage,
};

// 存放已经注册的语言实例
let registedInstanceList: string[] = [];

// 语言注册实例
const languageInsMap: Record<string, any[]> = {};

function registerLangByType(monaco: any, sqlType: typeSql) {
  return monaco.languages.registerCompletionItemProvider(sqlType, {
    // 触发提示的字符
    triggerCharacters: ['.', ' '],
    provideCompletionItems: (model: any, position: any) => {
      const suggestions: any[] = [];
      const { lineNumber, column } = position;
      const textBeforePointer = model.getValueInRange({
        startLineNumber: lineNumber,
        startColumn: 0,
        endLineNumber: lineNumber,
        endColumn: column,
      });

      // 可以根据用户的代码进行特殊推荐
      const textAllBefore = model.getValue();
      const contents = textBeforePointer.trim().split(/\s+/);
      const lastContents = contents[contents?.length - 1]; // 获取最后一段非空字符串

      // 注入SQL默认推荐
      const sqlConfigKey = ['builtinFunctions', 'keywords', 'operators'];
      sqlConfigKey.forEach((key) => {
        const curLanguage = languageMap[sqlType];
        curLanguage[key].forEach((sql: string) => {
          suggestions.push({
            label: sql, // 显示的提示内容;默认情况下,这也是选择完成时插入的文本。
            insertText: sql, // 选择此完成时应插入到文档中的字符串或片段
            kind: monaco.languages.CompletionItemKind.Function, // 此完成项的种类。编辑器根据图标的种类选择图标。
          });
        });
      });

      return {
        suggestions: [...new Set([...suggestions])],
      };
    },
  });
}

export const registerLang = (monaco: any, keyModule = 'defaultModule') => {
  if (!languageInsMap[keyModule]) {
    languageInsMap[keyModule] = [];
  }

  sqlLanguagesList.forEach((sqlType: typeSql) => {
    if (registedInstanceList.includes(sqlType)) {
      return;
    }

    const registerIns = registerLangByType(monaco, sqlType);
    languageInsMap[keyModule].push(registerIns);
    registedInstanceList.push(sqlType);
  });
};

export const destroyLang = () => {
  Object.keys(languageInsMap).forEach((key: string) => {
    // 销毁函数
    languageInsMap[key]?.forEach((ins) => {
      ins?.dispose();
    });
  });

  registedInstanceList = [];
};

  • webpack配置插件

const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
  chainWebpack: (memo) => {
    memo.plugin('monaco-editor-webpack-plugin').use(MonacoWebpackPlugin, [
      // 按需配置
      // { languages: ['javascript'] },
    ]);

    return memo;
  }
  

这样我们就实现了一个编辑器:

实现一个基于monaco-editor的编辑器组件【bysking】实现一个基于monaco-editor的编辑器组件

todo: 编辑器插件逻辑,键盘逻辑

转载自:https://juejin.cn/post/7403547816915910682
评论
请登录