博客 / 詳情

返回

JSON Schema&表單UI快速生成解析

一、JSON Schema

​ JSON(JavaScript Object Notation)是一種輕量&常見的數據交換格式,基本的數據的結構就是key-value,具有易於生成和解析的優點,通過JSON可以靈活地表達程序所需要的數據結構。

​ 但JSON本身並沒有特定的規範(本身結構也不支持註釋),所以對於數據本身的描述是缺失的,比如説開發人員或者程序,就無法判斷下面這份數據裏面的age為string是否是符合預期的類型。

{
  "name": "John Doe",
  "mobile": "1370000001",
  "age": "30"
}

​ JSON Schema定義了一套能夠較為完整地來描述JSON的規範,基於JSON Schema的規範去描述我們所需要的數據結構,或者基於這種規範去開發程序,就能實現預期效果。

使用場景:

1. 數據校驗

​ 可能是JSON Schema最常見的場景,無論是前端還是後台都有校驗數據的需求,表單校驗,CI/CD的自動化測試等等。

以上面的JSON為例,如果要規定age為number,並且必須小於等於20,那麼可以這樣聲明一份JSON Schema

{
  "$schema": "http://json-schema.org/schema",
  "title": "Person",
  "description": "an example",
  "type": "object",
  "properties": {
    "name": {
      "type": "string"
    },
    "mobile": {
      "type": "string"
    },
    "age": {
      "type": "number",
      "maximum": 20
    },
  }
}

​ 那麼當age為string,或者為number,但不在範圍內的時候都會提示校驗失敗

​ 簡單的校驗示例:https://www.jsonschemavalidat...

應用

Ajv.js

​ 一個基於JSON Schema的校驗庫,常用於nodejs、瀏覽器、微信小程序等場景的數據校驗,通過聲明一個JSON Schema來快速驗證數據,而無需進行代碼開發。

示例:

const Ajv = require('ajv');
const ajv = new Ajv();
// schema
const schema = {
  $schema: 'http://json-schema.org/schema',
  ...
};
const validate = ajv.compile(schema);
// 驗證的數據
const validData = {
  ...
  age: '30',
};
const validResult = validate(validData);
if (validResult) {
  // 驗證通過
  console.log('pass');
} else {
  // 驗證不通過
  console.log(validate.errors);
  // [
  //   {
  //     keyword: 'type',
  //     dataPath: '.age',
  //     schemaPath: '#/properties/age/type',
  //     params: { type: 'number' },
  //     message: 'should be number'
  //   }
  // ]
}

​ 不只是JavaScript/Typescript,其他編程語言也有基於JSON Schema實現的校驗器,如Java的Snow 、go的gojsonschema和Python的jschon 等等都是基於此去開發的。所以通過JSON Schema規範,還可以保持前後端校驗的一致。

2. form自動生成

​ JSON Schem雖然有規範約束,但仍然還是一份描述數據的JSON配置,那麼基於這份配置,邏輯上就能自動渲染出功能完整的表單UI。

應用
  1. vue-json-schema-form:基於 vue.js 和JSON Schema 渲染form,最新版本已經支持Vue3
  2. form-render:基於react.js的表單解決方案,最新版本使用Ant Design作為視覺主題
  3. formily:跨端能力,邏輯可跨框架,主要模塊(react.js+antd為例):

    @formily/core:實現狀態管理、表單校驗等邏輯,和UI無關

    @formily/react:實現交互效果,視圖橋接

    @formily/antd:擴展組件庫,開箱即用的表單UI

​ 以上表單渲染庫都有提供對應的表單設計器,可以通過拖拽的形式快速生成JSON Schema,整體流程如下:

image.png

​ 如何選擇合適的庫?如是基於react.js的low code項目,那其實form-render就已經足夠了。formily雖然支持的場景很多,但有一定的接入成本(從官方文檔就能看出),而且包的體積也相對較大。如form-render,只需要引入form-render,然後正確傳入props就能直接渲染出預期的UI。

​ 常見的在低代碼平台中,都會有表單模塊,但這部分的邏輯通常並不是整個低代碼項目的核心,那就可以交由form-render這類表單渲染庫去做,基於此就能減少開發單獨維護表單映射或者校驗的代碼。當然也有可能需要開發部分定製的widgets,以適配於較為複雜或者更切合業務的情況。

二、form-render

​ from-render整體可以分為core和widgets。core實現了表單映射、校驗和監聽等等,widgets就是一些UI組件實現了。

core

映射

widgets包含了內置組件和擴展組件,內置的組件已經提供,基本包含在這裏:https://x-render.gitee.io/gen...。同時還支持由開發者自定義一些擴展組件,提供props.widgets傳入自定義的object就能把擴展form組件註冊到widgets映射表內。

// form-render-core/src/index.js
<ConfigProvider locale={zhCN} {...configProvider}>
    <FRCore widgets={{ ...defaultWidgets, ...widgets }} {...rest} />
</ConfigProvider>

​ 如果要覆蓋默認組件,可以使用mapping註冊到form映射表內

  // form-render-core/src/index.js
  const tools = useMemo(
    () => ({
      widgets,
      mapping: { ...defaultMapping, ...mapping },
  ...

​ 需要注意的是這裏只是form映射表,同時還需要將自定義的widgets註冊到表內。無論是內置或者擴展的組件都會,只要實現了一個基於映射表的getWidgetName方法就能獲取到需要映射的組件名,渲染出對應的UI。

// form-render-core/src/core/RenderField/ExtendedWidget.js
// JSON Schema指定widget
let widgetName = getWidgetName(schema, mapping);
const customName = schema.widget || schema['ui:widget']; 
if (customName && widgets[customName]) {
  widgetName = customName;
}
const readOnlyName = schema.readOnlyWidget || 'html'; // 指定readOnly模式下的widget,或者使用默認html
if (readOnly && !isObjType(schema) && !isListType(schema)) {
  // 基礎組件的readOnly會默認使用readOnlyName
  widgetName = readOnlyName;
}
if (!widgetName) {
  widgetName = 'input';
  return <ErrorSchema schema={schema} />;
}
const Widget = widgets[widgetName];
const extraSchema = extraSchemaList[widgetName];
...
// form-render-core/src/core/RenderField/index.js
// 單屬性UI最基礎的內容
const RenderField = props => {
    ...
  return (
    <>
      {_showTitle && titleElement}
      <div
        className={`${contentClass} ${hideTitle ? 'fr-content-no-title' : ''}`}
        style={contentStyle}
      >
        {/* Widget渲染 */}
        <ExtendedWidget {...widgetProps} />
        {/* 説明信息 */}
        <Extra {...widgetProps} />
        {/* ErrorMessage,校驗相關 */}
        <ErrorMessage {...messageProps} />
      </div>
    </>
  );
}

校驗

​ 需要實現兩個基礎的校驗方法,validateSingle(單屬性校驗)和validateAll(表單校驗),具體的校驗邏輯可以通過一些開源工具去實現,如form-render使用的是async-validator作為校驗工具,async-validator是一個表單異步校驗的工具,Ajv.js也可以異步校驗,只需要初始化的時候帶上schema內帶上{$async: true}

Ajv.js async-validator
server 支持 支持
client 支持 支持
同步校驗 支持 不支持
異步校驗 支持 支持
package size 119.6 kb 14.2kb

​ 多數情況下的表單校驗都會選擇異步執行,所以包括form-render這類表單渲染庫,或者一些開源組件庫(如element)會使用async-validator作為校驗工具。

// form-render-core/src/core/RenderField/index.js
const validateSingle = (data, schema = {}, path, options = {}) => {
    ...
  /**
   * getDescriptorSimple會轉換成匹配async-validator的數據結構,如果是其他的校驗工具,可能就是另一種轉換了
   * 以path為key,rules為value,和result的[path]: data是對應的
   */
  const descriptor = getDescriptorSimple(schema, path);
  let validator;
  try {
    // 校驗
    validator = new Validator(descriptor);
  } catch (error) {
    return Promise.resolve();
  }
  // 錯誤提示的模板 type number string
  let messageFeed = locale === 'en' ? en : cn;
  merge(messageFeed, validateMessages);
  validator.messages(messageFeed);
  return validator
    .validate({ [path]: data })
    .then(res => {
      return [{ field: path, message: null }];
    })
    .catch(({ errors, fields }) => {
        // 
      return errors;
    });
};

​ validateAll只需要基於validateSingle遍歷完成校驗即可。validateSingle除了作為validateAll的一部分,同時也會在validateField中使用,為單個屬性實時校驗使用。

  const onChange = value => {
    // 節流、表單方法等
    ...
    validateField({
      path: dataPath, // 路徑
      formData: formDataRef.current, // 表單數據
      flatten, // schema 的轉換結構,[path]: {parent, children, schema}
      options: {
        locale,
        validateMessages,
      },
    })
    ...
  };

​ 只是有校驗是不夠的,最重要的是同時要提示數據校驗不通過的原因,所以還需要實現message動態模板,以及ErrorMessage組件承載錯誤提示。如form-render,實現了validateMessageCN.js作為message模板,ErrorMessage.js作為錯誤提示組件。

監聽

​ 數據監聽常見於低代碼的場景中,預期是希望用户輸入對應的屬性後,能實時在渲染器響應,同步渲染UI。form-render提供了watch屬性,用於數據的監聽的喚起回調。

// form-render-core/src/Watcher.js
    ...
  /**
   * formData當前表單的數據,watchKey被監聽的key
   * getValueByPath主要是處理#和普通的key
   * 如果是#,返回的就是formData
 
   */
  const value = getValueByPath(formData, watchKey);
  // callback
  const watchObj = watch[watchKey];

  useEffect(() => {
    const runWatcher = () => {
      if (typeof watchObj === 'function') {
        try {
          // 執行回調函數,並把value傳遞到外層
          watchObj(value);
        } catch (error) {
          console.log(`${watchKey}對應的watch函數執行報錯:`, error);
        }
      } else if (watchObj && typeof watchObj.handler === 'function') {
        try {
          // 適配多個參數的情況,其實目前的話,主要是handler和immediate
          watchObj.handler(value);
        } catch (error) {
          console.log(`${watchKey}對應的watch函數執行報錯:`, error);
        }
      }
    };

    if (firstMount) {
      const immediate = watchObj && watchObj.immediate;
      if (immediate) {
        // 如果immediate為true,會在首次加載的時候觸發一次watch
        runWatcher();
      }
    } else {
      runWatcher();
    }
  ...

​ 需要注意的是,存在對象或者數組嵌套的情況,getValueByPath也需要有根據path來獲取value的能力。如form-render是通過lodash-es模塊的get方法來實現的。

​ 通過watch映射表構建多個watch實例。

...
{
  {/* watchList = Object.keys(watch) */}
  watchList.length > 0
  ? watchList.map((item, idx) => {
        {/* null */}
      return (
        <Watcher
          key={idx.toString()}
          watchKey={item}
          watch={watch}
          formData={formData}
          firstMount={firstMount}
        />
      );
    })
  : null
}
...

widgets

​ widgets主要是包含了內置組件,部分組件是直接使用了組件庫提供的組件,如TextArea、InputNumber等,這些組件只需要調整下樣式就能直接用於表單渲染了;但大部分組件都是經過封裝後再使用的,如Slider、Color和Date組件等,不同的組件封裝的邏輯不同,比如Slider包含了組件庫的Slider和InputNumber,並對schema做解構,構建成對應的props。

<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dd3dbb93298e432daee11d2b07857266~tplv-k3u1fbpfcp-zoom-1.image" style="zoom:36%;" />

form-render自定義組件:input, checkbox, checkboxes, color, date, time, dateRange, timeRange, imageInput, url, list, map, multiSelect, radio, select, slider, switch, upload, html, rate

form-render組件庫組件:number, textarea, treeSelect

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.