通过全局脚本引入 monaco-editor 搭配 ajv 校验,实现可配置的 json 校验

1,900 阅读1分钟

业务背景

我们有一个 pc 端系统,运营人员需要发布策略,这个策略可以理解为一个 json,为了可以让运营更方便的编辑 json。我们采用的技术方案是使用 monaco-editor 体验可以类似 vscode。除此之外还有一个校验问题,为了避免运营人员发布错误的 json。我们需要配置一个校验规则的 schema,可以提示输入也可以提示错误,我们采用了 ajv 来实现。

为什么使用全局的脚本引入的方式引入

由于第三方库不稳定,并且第三方库对于定制的 schema 校验等能力并不完善

以下是业务代码

import React, { useState, useRef, useEffect } from 'react';
import {
  Form, Button, message, notification,
} from 'antd';
import PropTypes from 'prop-types';
import { useSize } from 'ahooks';
import Ajv from 'ajv';
import { renderForm } from '@/utils/renderFormItem';
import { ajaxAndLoading } from '@/utils/util';
import { createPolicy } from '@/service/policy';
import { getFormArr } from './FormConfig';
import schema from './schema';
import './index.scss';

let monacoInited = false;

const loadScriptResultMap = {};
function importScript(src) {
  if (loadScriptResultMap[src]) {
    return loadScriptResultMap[src];
  }
  loadScriptResultMap[src] = new Promise((resolve, reject) => {
    const newScript = document.createElement('script');
    newScript.src = src;
    document.body.append(newScript);
    newScript.addEventListener('load', () => {
      resolve();
    });
    newScript.onerror = () => {
      monacoInited = false;
      document.body.removeChild(newScript);
      reject();
    };
  });
  return loadScriptResultMap[src];
}

function initMocaco() {
  if (monacoInited === true) {
    return Promise.resolve();
  }
  return importScript('https://g.alicdn.com/code/lib/monaco-editor/0.20.0/min/vs/loader.js').then(() => {
    window.require.config({ paths: { vs: '//g.alicdn.com/code/lib/monaco-editor/0.20.0/min/vs' } });
    monacoInited = true;
  }).catch(() => {
    monacoInited = false;
  });
}

function FormWrap(props) {
  const [loading, setLoading] = useState(false);
  const { form, history, userId } = props;

  const actionFunc = () => {
    history.push('/strategy');
    message.success('成功创建策略');
  };

  const handleCancel = () => history.push('/strategy');

  const EditorRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    const { validateFieldsAndScroll } = form;
    validateFieldsAndScroll((err, values) => {
      const ajv = new Ajv({ allErrors: true });
      const validate = ajv.compile(schema.schema);
      const valid = validate(JSON.parse(EditorRef.current.getValue()));
      if (!valid) {
        const errorTip = validate?.errors?.map(v => (
          <React.Fragment key={v}>{v.dataPath} {v.message} <br /></React.Fragment>
        ));
        notification.warning({
          description: errorTip,
          message: '错误',
          placement: 'topRight',
        });
      }
      if (!err && valid) {
        const { name, description, type } = values;
        const params = {
          name,
          description,
          creator: userId,
          content: JSON.stringify(JSON.parse(EditorRef.current.getValue())),
          plugin: type,
        };
        ajaxAndLoading(createPolicy, params, setLoading, null, actionFunc);
      }
    });
  };

  const formSetter = getFormArr(form);
  const needKeys = ['name', 'description', 'type'];
  const filterControl = formSetter.filter(v => needKeys.includes(v.key));

  const ref = useRef(null);
  const size = useSize(ref);
  const container = useRef(null);

  useEffect(() => {
    initMocaco().then(() => {
      window.require(['vs/editor/editor.main'], () => {
        if (container.current) {
          EditorRef.current = window.monaco.editor.create(container.current, {
            value: '{}',
            language: 'json',
            theme: localStorage.getItem('themeEditor') || 'hc-black',
          });
          window.monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
            validate: true,
            schemas: [schema],
          });
        }
      });
    });
    return () => {
      if (EditorRef.current) {
        EditorRef.current.dispose();
        const model = EditorRef.current.getModel();
        if (model) {
          model.dispose();
        }
      }
    };
  }, []);

  return (
    <Form onSubmit={handleSubmit}>
      <div className="ant-advanced-search-form">
        {renderForm(filterControl).map(v => v)}
      </div>
      <div ref={ref}>
        <div style={{ height: 500, width: size.width }} ref={container} />
      </div>
      <div id="test" />
      <Form.Item className="m-t-12">
        <Button type="primary" style={{ marginRight: 8 }} loading={loading} htmlType="submit">
          提交
        </Button>
        <Button onClick={handleCancel}>
          取消
        </Button>
      </Form.Item>
    </Form>
  );
}

FormWrap.propTypes = {
  form: PropTypes.any.isRequired,
  history: PropTypes.object.isRequired,
  userId: PropTypes.string,
};

FormWrap.defaultProps = {
  userId: '',
};

export default Form.create({ name: 'form-wrap' })(FormWrap);