@Formily-designable渲染的一点认知

1,706 阅读2分钟

image.png

前言

formily的思考

formily作为阿里巴巴团队出品,而且经历过几次迭代,目前是一个相对成熟稳定的低码数平台, 提供@formily/core、 @formily/reactive、 @formily/react、@formily/vue等相对健全的生态系统

  • formily可以帮助我们解决问题
  • formily被选择的原因

formily学习过程

  1. @formily/reactive进行依赖收集
  2. @formily/core依赖追踪,高效更新,按需渲染的能力,响应式模型
  3. @formily/vue实现与ViewModel进行一个绑定关系
  4. @formily/element0plus基于 element-plus 封装的针对表单场景专业级(Professional)组件库

设计器Designable

CompositePanelItem 左侧操作区

【packages/prototypes/src/widgets/ResourceWidget】 操作区渲染组件,主要接收 propssourcestitle

export interface IResourceWidgetProps {
  title: VNode
  sources?: IResourceLike[]
  className?: string
  defaultExpand?: boolean
  children?: VNode[]
}

主要渲染展示函数为renderNode 主要数据处理逻辑为 sources

    export interface IResource {
        title?: string | IDesignerMiniLocales;
        description?: string | IDesignerMiniLocales;
        icon?: any;
        thumb?: string;
        span?: number;
        node?: TreeNode;
    }

      // 判断是否Resource数组, 如果是Resource包裹 进行转换再合并
      // props.sources = 之前上文组件传进来
      
      const sources = props.sources.reduce<IResource[]>((buf, source) => {
        //判断是否 Resource[], 取出里面的数据结构
        if (isResourceList(source)) {
          return buf.concat(source)
        } else if (isResourceHost(source)) {
          return buf.concat(source.Resource)
        }
        return buf
      }, [])

中间render组件渲染 ComponentTreeWidget

【packages/prototypes/src/widgets/ComponentTreeWidget/index.tsx】 组件渲染区域,承载@formily/element-plus-renderer 这个包中的 Components组件,通过在app.vue中导入后,通过props传入该组件

组件注册: GlobalRegistry.registerDesignerBehaviors(props.components!) 这个方法是DESIGNER_GlobalRegistry中的一个hooks

image.png reSortBehaviors 空的sources&已经存在的Behaviors 不做操作

      return () => {
        const dataId: Record<string, string> = {}
        if (
          designerRef.value &&
          treeRef.value &&
          designerRef.value?.props?.nodeIdAttrName
        ) {
          dataId[designerRef.value.props.nodeIdAttrName!] = treeRef.value.id
        }
        return (
          <div class={cls(prefixRef.value)} {...dataId}>
            <TreeNodeWidget {...{ node: treeRef.value }} />
          </div>
        )
      }

在这里出现了designerRef、treeRef, 其中tree 是通过 useTree()拿到整棵树的treeNode, designerRef也是通过hooks方法useDesigner()拿到设计器的实例

TreeNodeWidget

递归TreeNode 渲染


export const TreeNodeWidgetComponent = defineComponent({
  name: 'DnTreeNodeWidget',
  props: {
    node: Object as PropType<TreeNode>,
  },
  setup(props) {
    console.log(props, 'prosp')
    const designerRef = useDesigner(props.node?.designerProps?.effects)
    const componentsRef = useComponents()

    provide(TreeNodeSymbol, toRef(props, 'node'))

    return () => {
      const node = props.node!

      // default slot
      const renderChildren = () => {
        if (node?.designerProps?.selfRenderChildren) return []
        return node?.children
          ?.filter((child) => {
            const slot = child.props?.['x-slot']
            return !slot || slot === 'default'
          })
          ?.map((child) => {
            return <TreeNodeWidget {...{ node: child }} key={child.id} />
          })
      }

      // 支持 x-slot
      const renderSlots = () => {
        if (node?.designerProps?.selfRenderChildren) return []
        const result = node?.children?.reduce((buffer, child) => {
          const slot = child.props?.['x-slot']
          if (slot) {
            if (!buffer[slot]) buffer[slot] = []
            buffer[slot].push(<TreeNodeWidget node={child} key={child.id} />)
          }
          return buffer
        }, {} as Record<string, VNode[]>)
        return Object.entries(result).reduce((buffer, [key, value]) => {
          buffer[key] = () => value
          return buffer
        }, {} as Record<string, () => VNode[]>)
      }
      const renderProps = (extendsProps: any = {}) => {
        const props = {
          ...node.designerProps?.defaultProps,
          ...extendsProps,
          ...node.props,
          ...node.designerProps?.getComponentProps?.(node),
        }
        if (node.depth === 0) {
          delete props.style
        }
        return props
      }

      const renderComponent = () => {
        const componentName = node.componentName
        const Component = componentsRef.value?.[componentName]

        const dataId = {}
        if (Component) {
          if (designerRef.value) {
            dataId[designerRef.value?.props?.nodeIdAttrName] = node.id
          }
          const { style, ...attrs } = renderProps(dataId)
          return (
            <Component
              {...attrs}
              key={node.id}
              style={style}
              v-slots={renderSlots()}
            >
              {renderChildren()}
            </Component>
          )
        } else {
          if (node?.children?.length) {
            return <>{renderChildren()}</>
          }
        }
      }
      if (!node) return null
      if (node.hidden) return null
      return renderComponent()
    }
  },
})

预览组件preview-widget

【playground/src/widgets/preview-widget.tsx】 该组件主要用来展示组件预览状态, 通过SchemaField 方式接收一个 Schema 数据, 确立顶层上下文节点Form const formRef = shallowRef<IForm>(createForm()), 使用到 @formily/vue 中的createSchemaField 去进行把 @formily/element-plus 中开发的基础组件注册

// connect经常是与mapProps 结合起来一起使用的,达到无入侵的适配效果

const { SchemaField } = createSchemaField({
  components: {
    ...ElementUI,
    Card,
    Text,
    Rate,
    Slider,
    TreeSelect,
    Password: connect(
      ElementUI.Input,
      mapProps({}, (args) => ({
        ...args,
        type: 'password',
        showPassword: true,
      }))
    ),
  },
})
参数类型
treeTreeNode

设计器组件分析