Formily 实践 自定义组件、自定义表单设计器

7,593次阅读
没有评论

Formily官网:https://formilyjs.org/

源码地址:https://github.com/alibaba/formily

知乎作者介绍:https://zhuanlan.zhihu.com/p/364750033

本文信息都来自上述官方文档和自己的实践总结,因为本人确切的说是一名后端,对于前端是对文档严重依赖的,当然逼急了也会去看源码,所以难免会出现前端专业缺失的问题,如有错误请指点。

简介

Formily是一个与框架无关的表单解决方案,使用了JSON Schema,多端适配,目前社区已经有React、Vue方案,更是支持了众多流行前端框架如ant、next、element、element-plus、antdv、vant、semi、tdesign-react等。按照作者的说法是:定位是 面向复杂场景的表单解决方案, 面向企业级表单的专业解决方案,有如下特色:

  • 业内领先的思想
  • 丰富的使用场景
  • 极致的细节优化
  • 完善的文档周边

Formily前后有两版本,前后不兼容,但是我没赶上1.0,直接享受2.0,下面是我自己的使用实践。

实践总结

因为本人是后端,所以采用了Ant recat方案,下文全是如此。

快速开始

这里我按照自己的实践,使用ant pro 为项目工程模板。

npm i @ant-design/pro-cli -g
pro create formilyant
cd formilyant
yarn

基础工作准备完毕,开始引用formily组件。当然是按照官方文档走了,https://formilyjs.org/zh-CN/guide/upgrade

yarn add @formily/core
yarn add @formily/react
yarn add   @formily/antd 

@formily/core是formily核心库,formily能力所在之处,因为不依赖具体框架所以就是上文所说的框架无关性从而有了那么多的社区实现,负责管理表单的状态,表单校验,联动等等。

@formily/reactUI桥接库,使用react的项目必须安装它

@formily/antd Formily组件库,注意,这个组件库是用来给formily使用的,这个库是按照formily要求所封装的UI组件库,即Formily不能直接消费UI组件库需要按照formily的格式所封装,也属于UI组件库的桥接库(这里我想过一个问题,为什么Formily不能直接消费框架组件库呢,因为另一个解决方案 XRender 就可以,看了formily的实现后就明白了 )。当然了,formily封装UI组件库也是很简单的正如官网那样,

Formily

打开看还真是这么简单,以Input为例,

import React from 'react'
import { connect, mapProps, mapReadPretty } from '@formily/react'
import { Input as AntdInput } from 'antd'
import { InputProps, TextAreaProps } from 'antd/lib/input'
import { PreviewText } from '../preview-text'
import { LoadingOutlined } from '@ant-design/icons'

type ComposedInput = React.FC<React.PropsWithChildren<InputProps>> & {
  TextArea?: React.FC<React.PropsWithChildren<TextAreaProps>>
}

export const Input: ComposedInput = connect(
  AntdInput,
  mapProps((props, field) => {
    return {
      ...props,
      suffix: (
        <span>
        {field?.['loading'] || field?.['validating'] ? (
          <LoadingOutlined />
        ) : (
          props.suffix
        )}
      </span>
    ),
  }
  }),
  mapReadPretty(PreviewText.Input)
)

Input.TextArea = connect(AntdInput.TextArea, mapReadPretty(PreviewText.Input))

export default Input

是不是很简单,稍后我再下文会写一个我自己封装的组件作详细说明。

官网实例上手实践

import React from 'react'
import { createForm } from '@formily/core'
import { FormProvider, FormConsumer, Field } from '@formily/react'
import {
  FormItem,
  FormLayout,
  Input,
  FormButtonGroup,
  Submit,
} from '@formily/antd'

const form = createForm()

export default () => {
  return (
    <FormProvider form={form}>
      <FormLayout layout="vertical">
        <Field
          name="input"
          title="输入框"
          required
          initialValue="Hello world"
          decorator={[FormItem]}
          component={[Input]}
        />
      </FormLayout>
      <FormConsumer>
        {() => (
          <div
            style={{
              marginBottom: 20,
              padding: 5,
              border: '1px dashed #666',
            }}
          >
            实时响应:{form.values.input}
          </div>
        )}
      </FormConsumer>
      <FormButtonGroup>
        <Submit onSubmit={console.log}>提交</Submit>
      </FormButtonGroup>
    </FormProvider>
  )
}

npm start启动查看实际效果

Formily

很好,成功使用了formily.

代码的解释官网很好的解释,看官网文档。

  • createForm用来创建表单核心领域模型,它是作为MVVM设计模式的标准 ViewModel
  • FormProvider组件是作为视图层桥接表单模型的入口,它只有一个参数,就是接收 createForm 创建出来的 Form 实例,并将 Form 实例以上下文形式传递到子组件中
  • FormLayout组件是用来批量控制FormItem样式的组件,这里我们指定布局为上下布局,也就是标签在上,组件在下
  • Field组件是用来承接普通字段的组件
  • name 属性,标识字段在表单最终提交数据中的路径
  • title 属性,标识字段的标题
  • 如果 decorator 指定为 FormItem,那么在 FormItem 组件中会默认以接收 title 属性作为标签
  • 如果指定为某个自定义组件,那么 title 的消费方则由自定义组件来承接
  • 如果不指定 decorator,那么 title 则不会显示在 UI 上
  • required 属性,必填校验的极简写法,标识该字段必填
  • 如果 decorator 指定为 FormItem,那么会自动出现星号提示,同时校验失败也会有对应的状态反馈,这些都是 FormItem 内部做的默认处理
  • 如果 decorator 指定为自定义组件,那么对应的 UI 样式则需要自定义组件实现方自己实现
  • 如果不指定 decorator,那么 required 只是会阻塞提交,校验失败不会有任何 UI 反馈。
  • initialValue 属性,代表字段的默认值
  • decorator 属性,代表字段的 UI 装饰器,通常我们都会指定为 FormItem
  • 注意 decorator 属性传递的是数组形式,第一个参数代表指定组件类型,第二个参数代表指定组件属性
  • component 属性,代表字段的输入控件,可以是 Input,也可以是 Select,等等
  • 注意 component 属性传递的是数组形式,第一个参数代表指定组件类型,第二个参数代表指定组件属性
  • FormConsumer组件是作为响应式模型的响应器而存在,它核心是一个 render props 模式,在作为 children 的回调函数中,会自动收集所有依赖,如果依赖发生变化,则会重新渲染,借助 FormConsumer 我们可以很方便的实现各种计算汇总的需求
  • FormButtonGroup组件作为表单按钮组容器而存在,主要负责按钮的布局
  • Submit组件作为表单提交的动作触发器而存在,其实我们也可以直接使用 form.submit 方法进行提交,但是使用 Submit 的好处是不需要每次都在 Button 组件上写 onClick 事件处理器,同时它还处理了 Form 的 loading 状态,如果 onSubmit 方法返回一个 Promise,且 Promise 正在 pending 状态,那么按钮会自动进入 loading 状态

Formily使用解析

如何能够使用正常使用Fomrily?

  • 官方文档

官方文档是必须看的,而实际上所有东西基本都能在官方文档找到,但是我开始的时候就因为我自己的问题对文档不能很好阅读而走了不少弯路。这里我自己将文档内容理解后总结为官方的两张图,

@formily/core

Formily

@formily/react

Formily

这两张图代表了formily核心架构和表单应用架构,这两张图看了后就能看明白官方文档的内容和如何查找自己想要的内容。

Formily

这个地方会看的比较多,介绍了Formily的全部内容。

Formily

这个是Formily的组件库,是按照Formily要求格式封装UI组件库。

自定义组件

社区维护的组件难免会出现不满足实际项目而出现自定义组件的需求,而自定义组件这部分呢官方文档是看源码,看了源码后和结合文档后就那样明白了。

Formily封装的 Select组件并不支持远程数据源,就比如数据字典这个需求又是比较常见的,所以这里就以它为例介绍Formily的自定义组件封装流程。

  • 首先要实现自己的业务组件,即给Ant 的Select 组件添加一部数据源功能,代码如下
import React, { useCallback, useEffect, useState } from "react";
import { Select } from "antd";
import { request } from "umi";
const { Option } = Select;

const getCode=()=>request('/api/codevalue/sex',{method:'GET'})


const DicSelect: React.FC<any> = (props) => {
  const { value, dicCode, options, readPretty } = props;
  const [poptions, setPOptions] = useState([]);
  const [loading, setLaoding] = useState(true);
  const init = useCallback(async () => {
    if (options && options.length > 0) {
      setPOptions(options);
    } else {
      const res = await getCode({ code: dicCode });
      setPOptions(res);
    }
    setLaoding(false);
  }, [dicCode, options]);
  useEffect(() => {
    init();
  }, [dicCode, options]);
  return (
    <>
    {readPretty ? (
      <>{poptions?.find((x) => x.value == value)?.label}</>
) : (
  <Select {...props} options={null} loading={loading}>
                              {poptions?.map((x) => (
                                <Option key={x.value} value={x.value}>
                                {x.label}
                                </Option>
                                            ))}
  </Select>
  )}
  </>
  );
  };

  export { DicSelect };

上述的代码可能有人看了会说,有些地方不太对,我解释下,上面的代码就是给Select 组件增添了一个props `dicCode`、`readPretty`, `dicCode`就是字典参数,用来获取具体字典项,而 readPretty则是用来适配Fomily的 readPretty 模式下友好显示数据的,如果不适配我这里会显示一个数字(字典value)而不能显示文本(字典label),Formily表单模式有readOnly、disabledreadPrettyhttps://core.formilyjs.org/zh-CN/api/models/form

  • 封装业务组件为Formily所能使用的组件,很简单,如官网所写,代码如下,
import React from "react";
import { connect, mapReadPretty, mapProps } from "@formily/react";

import { LoadingOutlined } from "@ant-design/icons";

import { DicSelect as AntdSelect } from "./DicSelect";

export const DicSelect = connect(
  AntdSelect,
  mapProps(
    {
      dataSource: "options",
      loading: true,
      data: true
    },
    (props, field) => {
      console.log(props, field);
      return {
        ...props,
        suffixIcon:
          field?.["loading"] || field?.["validating"] ? (
            <LoadingOutlined />
          ) : (
            props.suffixIcon
          )
      };
    }
  ),
  mapReadPretty(AntdSelect, { readPretty: true })
);

export default DicSelect;

是的就是这么简单,然后就可以在Fomily里面使用了。

Formily

Fomily 表单设计器

Formily提供了一个基于 designable 的表单设计器,然后呢,表单设计器的文档更为简洁,看了基本就会,然后结合源码更好,这里我看到一些基于此的表单设计器都是直接用源码自己改造,我开始还纳闷为啥直接重写而不应该是提供基础版然后自定义扩展呢,前端都这么风骚非要自己写吗?后来我在官方文档里找到了一些蛛丝马迹,原文如下,

Designable 的核心理念是将设计器搭建变成模块化组合,一切可替换,Designable 本身提供了一系列开箱即用的组件给用户使用,但是如果用户对组件不满意,是可以直接替换组件,从而实现最大化灵活定制,也就是 Designable 本身是不会提供任何插槽 Plugin 相关的 API

下面是我自己的实践,

yarn add @designable/formily-antd

官方案例代码,

import 'antd/dist/antd.less'
import React, { useMemo } from 'react'
import ReactDOM from 'react-dom'
import {
  Designer, //设计器根组件,主要用于下发上下文
  DesignerToolsWidget, //画板工具挂件
  ViewToolsWidget, //视图切换工具挂件
  Workspace, //工作区组件,核心组件,用于管理工作区内的拖拽行为,树节点数据等等...
  OutlineTreeWidget, //大纲树组件,它会自动识别当前工作区,展示出工作区内树节点
  ResourceWidget, //拖拽源挂件
  HistoryWidget, //历史记录挂件
  StudioPanel, //主布局面板
  CompositePanel, //左侧组合布局面板
  WorkspacePanel, //工作区布局面板
  ToolbarPanel, //工具栏布局面板
  ViewportPanel, //视口布局面板
  ViewPanel, //视图布局面板
  SettingsPanel, //右侧配置表单布局面板
  ComponentTreeWidget, //组件树渲染器
} from '@designable/react'
import { SettingsForm } from '@designable/react-settings-form'
import {
  createDesigner,
  GlobalRegistry,
  Shortcut,
  KeyCode,
} from '@designable/core'
import {
  LogoWidget,
  ActionsWidget,
  PreviewWidget,
  SchemaEditorWidget,
  MarkupSchemaWidget,
} from './widgets'
import { saveSchema } from './service'
import {
  Form,
  Field,
  Input,
  Select,
  TreeSelect,
  Cascader,
  Radio,
  Checkbox,
  Slider,
  Rate,
  NumberPicker,
  Transfer,
  Password,
  DatePicker,
  TimePicker,
  Upload,
  Switch,
  Text,
  Card,
  ArrayCards,
  ObjectContainer,
  ArrayTable,
  Space,
  FormTab,
  FormCollapse,
  FormLayout,
  FormGrid,
} from '../src'

GlobalRegistry.registerDesignerLocales({
  'zh-CN': {
    sources: {
      Inputs: '输入控件',
      Layouts: '布局组件',
      Arrays: '自增组件',
      Displays: '展示组件',
    },
  },
  'en-US': {
    sources: {
      Inputs: 'Inputs',
      Layouts: 'Layouts',
      Arrays: 'Arrays',
      Displays: 'Displays',
    },
  },
})

const App = () => {
  const engine = useMemo(
    () =>
      createDesigner({
        shortcuts: [
          new Shortcut({
            codes: [
              [KeyCode.Meta, KeyCode.S],
              [KeyCode.Control, KeyCode.S],
            ],
            handler(ctx) {
              saveSchema(ctx.engine)
            },
          }),
        ],
        rootComponentName: 'Form',
      }),
    []
  )
  return (
    <Designer engine={engine}>
      <StudioPanel logo={<LogoWidget />} actions={<ActionsWidget />}>
        <CompositePanel>
          <CompositePanel.Item title="panels.Component" icon="Component">
            <ResourceWidget
              title="sources.Inputs"
              sources={[
                Input,
                Password,
                NumberPicker,
                Rate,
                Slider,
                Select,
                TreeSelect,
                Cascader,
                Transfer,
                Checkbox,
                Radio,
                DatePicker,
                TimePicker,
                Upload,
                Switch,
                ObjectContainer,
              ]}
            />
            <ResourceWidget
              title="sources.Layouts"
              sources={[
                Card,
                FormGrid,
                FormTab,
                FormLayout,
                FormCollapse,
                Space,
              ]}
            />
            <ResourceWidget
              title="sources.Arrays"
              sources={[ArrayCards, ArrayTable]}
            />
            <ResourceWidget title="sources.Displays" sources={[Text]} />
          </CompositePanel.Item>
          <CompositePanel.Item title="panels.OutlinedTree" icon="Outline">
            <OutlineTreeWidget />
          </CompositePanel.Item>
          <CompositePanel.Item title="panels.History" icon="History">
            <HistoryWidget />
          </CompositePanel.Item>
        </CompositePanel>
        <Workspace id="form">
          <WorkspacePanel>
            <ToolbarPanel>
              <DesignerToolsWidget />
              <ViewToolsWidget
                use={['DESIGNABLE', 'JSONTREE', 'MARKUP', 'PREVIEW']}
              />
            </ToolbarPanel>
            <ViewportPanel>
              <ViewPanel type="DESIGNABLE">
                {() => (
                  <ComponentTreeWidget
                    components={{
                      Form,
                      Field,
                      Input,
                      Select,
                      TreeSelect,
                      Cascader,
                      Radio,
                      Checkbox,
                      Slider,
                      Rate,
                      NumberPicker,
                      Transfer,
                      Password,
                      DatePicker,
                      TimePicker,
                      Upload,
                      Switch,
                      Text,
                      Card,
                      ArrayCards,
                      ArrayTable,
                      Space,
                      FormTab,
                      FormCollapse,
                      FormGrid,
                      FormLayout,
                      ObjectContainer,
                    }}
                  />
                )}
              </ViewPanel>
              <ViewPanel type="JSONTREE" scrollable={false}>
                {(tree, onChange) => (
                  <SchemaEditorWidget tree={tree} onChange={onChange} />
                )}
              </ViewPanel>
              <ViewPanel type="MARKUP" scrollable={false}>
                {(tree) => <MarkupSchemaWidget tree={tree} />}
              </ViewPanel>
              <ViewPanel type="PREVIEW">
                {(tree) => <PreviewWidget tree={tree} />}
              </ViewPanel>
            </ViewportPanel>
          </WorkspacePanel>
        </Workspace>
        <SettingsPanel title="panels.PropertySettings">
          <SettingsForm uploadAction="https://www.mocky.io/v2/5cc8019d300000980a055e76" />
        </SettingsPanel>
      </StudioPanel>
    </Designer>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))

这里有个意外,就是如上安装代码后,启动代码,发现表单设计器不能正常显示,即UI样式不正常,后来如下官方文档解决,我至今没搞明白这个问题。

Formily

上图是我项目里的实际案例,即左侧自定义,左侧组件加入自定义组件。这里解释下表单设计器基本使用,这里只说最基本的。

表单设计器的基本应用其实就三个块,即左侧组件区域,中间组件呈现区域,右侧组件设置区域。表单设计器使用了Formily的表单方案为右侧配置实现,更重要的是都是基于JSONSCHEMA的,这样大家就好理解。

  • 组件区域(左侧),表单设计器所需要的组件同样需要桥接才能在表单设计器使用,但是这个桥接更多的是使用json描述然后让表单设计器能够接收组件而非指的是功能,即一个Formily组件需要用一些包装(json描述)才能在设计器展现,但是这里的包装和将一个组件包装成Formily组件毫无关系。下面是包装一个组件到表单设计器
import React from 'react'
import { Select as FormilySelect } from '@formily/antd'
import { createBehavior, createResource } from '@designable/core'
import { DnFC } from '@designable/react'
import { createFieldSchema } from '../Field'
import { AllSchemas } from '../../schemas'
import { AllLocales } from '../../locales'

export const Select: DnFC<React.ComponentProps<typeof FormilySelect>> =
  FormilySelect

Select.Behavior = createBehavior({
  name: 'Select',
  extends: ['Field'],
  selector: (node) => node.props['x-component'] === 'Select',
  designerProps: {
    propsSchema: createFieldSchema(AllSchemas.Select),
  },
  designerLocales: AllLocales.Select,
})

Select.Resource = createResource({
  icon: 'SelectSource',
  elements: [
    {
      componentName: 'Field',
      props: {
        title: 'Select',
        'x-decorator': 'FormItem',
        'x-component': 'Select',
      },
    },
  ],
})

代码最核心的即BehaviorResourceResource描述了组件,即呈现在表单设计器左侧组件区域,而Behavior则呈现在表单设计器右侧的设置表单,具体的配置都在AllSchemas.Select,切记这里的配置是为组件而消费的数据,而不是配置组件本身。

分享一下我的实践里可能会用到的东西,即表单字段、表单名称

Formily

因为右侧组件配置区域其实就是Formily表单,所以可以直接使用Formily的表单事件 `onMount`直接修改表单名称组件从 Input Select,并设置 DataSource,这样即可将字段设置为字段字符而显示为友好的中文名称。

form.fields["field-group.name"]?.setComponent('Select')
form.fields["field-group.name"]?.setDataSource(modelItems);

而表单名称则是同理使用Formily的字段联动能力,当表单字段发生变化,则将表单名称自动更改为字段中文名称。

Formily

最开始我自定义的字典组件(DicSelect)呢,同理我在桥接字典组件到表单设计器时,在Behavior增添一个 dicCode的表单项即可将数据传递到组件消费。

Formily

好了,上面既是我目前的实践总结,文章里没有写原理,因为大家看文档即可明白,还有就是多看源码。

本文中的例子代码地址 github

4
老三的古代
版权声明:本站原创文章,由 老三的古代2022-08-15发表,共计11179字。
转载提示:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)