「表单设计器开发指南」的内容补充*

1,059 阅读9分钟

声明:本文为非原创文章,转载自www.yuque.com/xjchenhao/d…

官方使用 designable 工程开发了“表单设计器”,事实上 designable 不局限于此,做页面设计器、bi设计器都可以。

目前designable 工程暂时没有开发文档,可以参看这篇文章进行了解。当然不看也可以,因为我们并不是要去开发 designable 工程,而是使用 designable 里的模块而已,就像我们只需要会用 antd 的组件库,而不需要掌握 antd 开源工程一样。

@designable/react

observer

observer 是一个 HOC, Function Component 变成 Reaction,每次视图重新渲染就会收集依赖,依赖更新会自动重渲染

使用方式参看:

  • @formily/react 文档的observer部分。

  • @formily/reactive 文档的observer部分。

SettingsPanel

右侧配置表单布局面板。

NameDescriptionTypeDefault
title标题,支持 DesignerLocales规则string''

Workspace

工作区组件,是一个 HOC。用于管理工作区内的拖拽行为,树节点数据等等。

NameDescriptionTypeDefault
id需要多个工作区时需要各配置唯一的 IDstring'index'
title标题string''
description描述string''

title 和 description 字段在源码中没有看到使用,应该是预留字段。

WorkspacePanel

工作区布局面板,纯粹的布局组件(如果把设计器分为左中右三部分,工作区布局面板就是中间部分的“壳”),挂了.dn-workspace-panel样式,仅此而已。

不过它的子组件WorkspacePanel.Item在其它地方用的满多的,比如 ToolbarPanel、ViewportPanel 都是用它做跟组件。

WorkspacePanel.Item

设置display:flex对容器里,使用本组件,可以方便的通过flexable属性配置 flex-grow 和 flex-shrink 的开启与关闭。

如果对 flex-grow、flex-shrink 不了解,可以看这里

一码胜千言:

WorkspacePanel.Item = (props) => {
  const prefix = usePrefix('workspace-panel-item');
  return (
    <div
      className={prefix}
      style={{
        ...props.style,
        flexGrow: props.flexable ? 1 : 0,
        flexShrink: props.flexable ? 1 : 0,
      }}
    >
      {props.children}
    </div>
  );
};

ToolbarPanel

工具栏布局面板(设计器最中心区域上面的“操作区”),包括撤销、选择、视图切换等操作。

一码胜千言:

<WorkspacePanel.Item
  {...props}
  style={{
    display: 'flex',
    justifyContent: 'space-between',
    marginBottom: 4,
    padding: '0 4px',
    ...props.style,
  }}
>
  {props.children}
</WorkspacePanel.Item>

ViewportPanel

工具栏布局面板(设计器最中心区域的“编辑区”),它用模拟器组件Simulator给子组件做了布局(它的介绍就在楼下)。

一码胜千言:

<WorkspacePanel.Item {...props} flexable>
  <Simulator>{props.children}</Simulator>
</WorkspacePanel.Item>

Simulator

视图模拟器组件,通过适当配置,可以实现给中间的操作区套上“手机壳”、“自适应壳“的展示。

一码胜千言:

export const Simulator: React.FC<ISimulatorProps> = observer(
  (props: ISimulatorProps) => {
    const screen = useScreen(); // useScreen()的返回值等于`globalThisPolyfill['__DESIGNABLE_ENGINE__'].screen`
    if (screen.type === ScreenType.PC)
      return <PCSimulator {...props}>{props.children}</PCSimulator>;
    if (screen.type === ScreenType.Mobile)
      return <MobileSimulator {...props}>{props.children}</MobileSimulator>;
    if (screen.type === ScreenType.Responsive)
      return (
        <ResponsiveSimulator {...props}>{props.children}</ResponsiveSimulator>
      );
    return <PCSimulator {...props}>{props.children}</PCSimulator>;
  },
  {
    scheduler: requestIdle,
  },
);

ViewPanel

视图布局面板,也就是设计器最中心区域的“壳”。它是四种视图模式(可视化设计、json、jsx、预览)的容器组件,并给子组件赋能。

NameDescriptionTypeDefault
type容器类型,用于标识它是什么视图模式的容器string'DESIGNABLE'
scrollable容器的内容溢出是否需要滚动条,如果开启样式的 overflow 等于 overlay,关闭则为 hidden。booleantrue
dragTipsDirection可视化设计(type=DESIGNABLE)的模式时,缺省状态的操作动画,从左往右还是从右往左。string'left'

子组件的上下文中可以获取到ViewPanel提供的 tree 变量,完成一次配置多处使用的设计。如下:

<ViewportPanel style={{ height: '100%' }}>
  <ViewPanel type="DESIGNABLE" dragTipsDirection="left">
    {() => <ComponentTreeWidget components={{ Input }} />}
  </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>

另外,ViewPanel 会在被导入时自动获取workbench.type,在四种视图模式中选择需要展示的状态。

StudioPanel

主布局面板,整个工程的壳。

NameDescriptionTypeDefault
logo左上角的 logo 区reactNode---
actions右上角的操作区reactNode---

CompositePanel

左侧组合布局面板

NameDescriptionTypeDefault
direction大类的分组(包含组件、大纲树、历史记录 icon 的小区域)包含放在组件板块的左侧还是右侧string'left'
showNavTitle是否需要在大类的 icon 下显示标题booleanfalse
defaultOpen是否默认展开booleantrue
defaultPinning是否默认“固定”面板 只有 direction="right"时才能看到效果,因为没有叠放在设计区上面,体现不出“固定”的作用booleanfalse
defaultActiveKey默认展示哪个分组,注意下标从 0 开始number0
activeKey展示哪个分组,如果是 number 则按“下标”显示,如果是 string 则与其子组件CompositePanel.Itemkey字段对应number | string---
onChange切换时触发的回调函数(开始切换就执行了,早于展示)(activeKey: number | string) => void---

CompositePanel.Item

各布局面板的配置

NameDescriptionTypeDefault
title标题,支持 DesignerLocales规则string---
iconicon 标识。支持 DesignerIcon的内置 icon,也可以写 icon 资源路径string---
key组件标识,与 CompositePanel 组件的activeKey字段配合使用string---

ResourceWidget

组件资源编组容器。

NameDescriptionTypeDefault
title标题,支持 DesignerLocales规则string---
sources包含的组件Array&lt;ReactNode&gt;---

useTheme

获取主题函数。执行useTheme()可获得darkorlight

  • 如果什么都不干,获取到的永远都是'light'。

  • 如果希望自己的设计器可以切换主题可以使用@designable/react里的Layout组件做配置(需放在项目根目录)。

下面是“跟随系统的暗黑模式改变主题”的示例。

import React, { useMemo } from 'react';
import {
  Layout,
} from '@designable/react';

import App from './App';

// 设置UI主题(是否暗黑模式)
const designerTheme = !window.matchMedia('(prefers-color-scheme: dark)').matches
  ? 'light'
  : 'dark';

export default function IndexPage() {
  return (
    <Layout theme={designerTheme}>
      <App />
    </Layout>
  );

@designable/shared

该模块(主模块)里包含多个子模块

  • animation:组件的拖拽动画

  • array:基础的的数组函数

  • clone:拷贝函数

  • coordinate:实现拖拽功能的计算函数集

  • element:dom 元素的计算函数集

  • event:事件引擎

  • keycode:快捷键管理

  • lru:实现动态缓存管理

  • request-idle:封装了一下requestIdleCallback

  • scroller:实现拖拽自动滚动

  • type:验证数据类型

  • uid:提供了一个 uid 方法,用于给每个元素指定唯一的 key

它们里面的 api 均被注册在主模块的入口文件中,可以直接在顶层应用,例如:

import { isPlainObj } from '@designable/shared'; // isPlainObj是子模块types里的函数

const a={a:1};
const b=[1,2,3];
const c='123';

console.log(isPlainObj(a)); // true
console.log(isPlainObj(b)); // false
console.log(isPlainObj(c)); // false

types

做数据类型的验证。

NameDescription
isStr是否是字符串
isNum是否是数字
isBool是否是布尔型
isFn是否是函数
isPlainObj是否是字面量形式或者 new Object()形式定义的对象
isObj是否是对象。 这个比较泛,例如数组也会返回为 true,需要验证严谨的 object 类型,请使用 isPlainObj
isRegExp是否是正则表达式
isArr是否是数组
isValid验证值是否存在
isFn是否是函数
isHTMLElement是否是 html 的 dom 节点

除此之外,还提供了一个取值函数。

NameDescription
getType获取数据类型。返回Object.prototype.toString的结果,例如:[object Number]

element

dom 元素的计算函数集。

NameDescriptionTypeDefault
calcElementLayout计算元素的布局方式,是水平(内链)还是垂直(块)(element: Element) => "vertical" | "horizontal"'vertical'
calcElementOuterWidth计算元素的宽度(innerWidth: number, style: CSSStyleDeclaration) => number'vertical'

coordinate

实现拖拽功能的计算函数集。

先看下定义的几个接口类型,内部一直在使用(外面能用,export 出去了)。

// 点的位置
export interface IPoint {
  x: number;
  y: number;
}

// 点的类形态
export class Point implements IPoint {
  x: number;
  y: number;
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

// 矩形,包含起点坐标以及宽高
export interface IRect {
  x: number;
  y: number;
  width: number;
  height: number;
}

// 象限,框选多个组件时使用(猜测:内部指试图区域内,外部指试图区域外)
export enum RectQuadrant {
  Inner1 = 'I1', //内部第一象限
  Inner2 = 'I2', //内部第二象限
  Inner3 = 'I3', //内部第三象限
  Inner4 = 'I4', //内部第四象限
  Outer1 = 'O1', //外部第一象限
  Outer2 = 'O2', //外部第二象限
  Outer3 = 'O3', //外部第三象限
  Outer4 = 'O4', //外部第四象限
}

export interface IPointToRectRelative {
  quadrant: RectQuadrant;
  distance: number;
}
NameDescriptionTypeDefault
isPointInRect点是否在矩形内,前两个参数分别是点和矩形。 第三个参数是“是否敏感”,如果关闭,点在矩形内就可以返回 true 了;如果关闭,点在矩形的宽高-10%的范围内才返回 true(point: IPoint, rect: IRect, sensitive?: boolean) => boolean---
getRectPoints返回矩形的四个点的坐标(source: IRect) => Point[]---
isRectInRect矩形是否在矩形内,前两个参数分别是点和矩形。(target: IRect, source: IRect) => boolean---
isCrossRectInRect两个矩形是否存在交集(target: IRect, source: IRect) => boolean---
calcQuadrantOfPonitToRect计算点在矩形的哪个象限(point: IPoint, rect: IRect) => RectQuadrant---
calcDistanceOfPointToRect点跟矩形之间的距离(point: IPoint, rect: IRect) => number---
calcDistancePointToEdge点到边之间的距离(point: IPoint, rect: IRect) => number---
isNearAfter计算点的位置是否在矩形的下方或者右下方 比如拖拽组件时,把组件放置在另一个组件的“下方或者右下方”可以视为插入在其后面,否则就插入到其前面(point: IPoint, rect: IRect, inline?: boolean) => boolean---
calcRelativeOfPointToRect点在矩形里的相对位置(point: IPoint, rect: IRect) => IPointToRectRelative---
calcBoundingRect可以把多个矩形合并计算成一个更大的矩形(rects: IRect[]) => IRect---
calcRectByStartEndPoint按起点和终点计算矩形startPoint: IPoint, endPoint: IPoint, scrollX?: number, scrollY?: number---

request-idle

基于requestIdleCallback实现“函数在浏览器空闲时被调用”。

NameDescriptionTypeDefault
requestIdle预执行函数,返回函数 requestIdleCallbackId(callback: function, options?: { timeout?: number} ) => number---
cancelIdle需要清除的预执行函数(如果还没有被执行的话),需传递 requestIdleCallbackId(id: number) => void---

uid

只提供了一个 uid 方法,返回指定长度的随机字符串(字母+数字),用于给每个元素指定唯一的 key

NameDescriptionTypeDefault
len字符串长度string11 位包含字母数字的随机字符串

文件里代码蛮短的,可以读一下:

let IDX = 36,
  HEX = '';
while (IDX--) HEX += IDX.toString(36); // 通过依次从36到1转十六进制,最终返回`zyxwvutsrqponmlkjihgfedcba9876543210`

export function uid(len?: number) {
  let str = '',
    num = len || 11;

  while (num--) str += HEX[(Math.random() * 36) | 0]; // 根据长度,给str拼接随机字符
  return str;
}

globalThisPolyfill

全局变量,从模块里解构出来直接用就行了。

如果在浏览器环境就是 window,如果是 node 环境 就是 global。

可以给全局对象增加 globalThisPolyfill 属性。

返回优先级:window > 全局变量的 globalThisPolyfill 属性 > global

@designable/react-settings-form

SettingsForm

右侧设置面板组件(具体如何作拓展配置,待完善)