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

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

@designable/react

observer

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

使用方式参看:

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

SettingsPanel

右侧配置表单布局面板。

Name Description Type Default
title 标题,支持 DesignerLocales
规则
string ‘’

Workspace

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

Name Description Type Default
id 需要多个工作区时需要各配置唯一的 ID string ‘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 不了解,可以看这里

一码胜千言:

  1. WorkspacePanel.Item = (props) => {
  2. const prefix = usePrefix('workspace-panel-item');
  3. return (
  4. <div
  5. className={prefix}
  6. style={{
  7. ...props.style,
  8. flexGrow: props.flexable ? 1 : 0,
  9. flexShrink: props.flexable ? 1 : 0,
  10. }}
  11. >
  12. {props.children}
  13. </div>
  14. );
  15. };

ToolbarPanel

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

一码胜千言:

  1. <WorkspacePanel.Item
  2. {...props}
  3. style={{
  4. display: 'flex',
  5. justifyContent: 'space-between',
  6. marginBottom: 4,
  7. padding: '0 4px',
  8. ...props.style,
  9. }}
  10. >
  11. {props.children}
  12. </WorkspacePanel.Item>

ViewportPanel

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

一码胜千言:

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

Simulator

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

一码胜千言:

  1. export const Simulator: React.FC<ISimulatorProps> = observer(
  2. (props: ISimulatorProps) => {
  3. const screen = useScreen(); // useScreen()的返回值等于`globalThisPolyfill['__DESIGNABLE_ENGINE__'].screen`
  4. if (screen.type === ScreenType.PC)
  5. return <PCSimulator {...props}>{props.children}</PCSimulator>;
  6. if (screen.type === ScreenType.Mobile)
  7. return <MobileSimulator {...props}>{props.children}</MobileSimulator>;
  8. if (screen.type === ScreenType.Responsive)
  9. return (
  10. <ResponsiveSimulator {...props}>{props.children}</ResponsiveSimulator>
  11. );
  12. return <PCSimulator {...props}>{props.children}</PCSimulator>;
  13. },
  14. {
  15. scheduler: requestIdle,
  16. },
  17. );

ViewPanel

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

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

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

  1. <ViewportPanel style={{ height: '100%' }}>
  2. <ViewPanel type="DESIGNABLE" dragTipsDirection="left">
  3. {() => <ComponentTreeWidget components={{ Input }} />}
  4. </ViewPanel>
  5. <ViewPanel type="JSONTREE" scrollable={false}>
  6. {(tree, onChange) => <SchemaEditorWidget tree={tree} onChange={onChange} />}
  7. </ViewPanel>
  8. <ViewPanel type="MARKUP" scrollable={false}>
  9. {(tree) => <MarkupSchemaWidget tree={tree} />}
  10. </ViewPanel>
  11. <ViewPanel type="PREVIEW">
  12. {(tree) => <PreviewWidget tree={tree} />}
  13. </ViewPanel>
  14. </ViewportPanel>

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

StudioPanel

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

Name Description Type Default
logo 左上角的 logo 区 reactNode —-
actions 右上角的操作区 reactNode —-

CompositePanel

左侧组合布局面板

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

CompositePanel.Item

各布局面板的配置

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

ResourceWidget

组件资源编组容器。

Name Description Type Default
title 标题,支持 DesignerLocales
规则
string —-
sources 包含的组件 Array&lt;ReactNode&gt; —-

useTheme

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

  • 如果什么都不干,获取到的永远都是’light’。
  • 如果希望自己的设计器可以切换主题可以使用@designable/react里的Layout组件做配置(需放在项目根目录)。

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

  1. import React, { useMemo } from 'react';
  2. import {
  3. Layout,
  4. } from '@designable/react';
  5. import App from './App';
  6. // 设置UI主题(是否暗黑模式)
  7. const designerTheme = !window.matchMedia('(prefers-color-scheme: dark)').matches
  8. ? 'light'
  9. : 'dark';
  10. export default function IndexPage() {
  11. return (
  12. <Layout theme={designerTheme}>
  13. <App />
  14. </Layout>
  15. );

@designable/shared

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

  • animation:组件的拖拽动画
  • array:基础的的数组函数
  • clone:拷贝函数
  • coordinate:实现拖拽功能的计算函数集
  • element:dom 元素的计算函数集
  • event:事件引擎
  • keycode:快捷键管理
  • lru:实现动态缓存管理
  • request-idle:封装了一下requestIdleCallback
  • scroller:实现拖拽自动滚动
  • type:验证数据类型
  • uid:提供了一个 uid 方法,用于给每个元素指定唯一的 key

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

  1. import { isPlainObj } from '@designable/shared'; // isPlainObj是子模块types里的函数
  2. const a={a:1};
  3. const b=[1,2,3];
  4. const c='123';
  5. console.log(isPlainObj(a)); // true
  6. console.log(isPlainObj(b)); // false
  7. console.log(isPlainObj(c)); // false

types

做数据类型的验证。

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

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

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

element

dom 元素的计算函数集。

Name Description Type Default
calcElementLayout 计算元素的布局方式,是水平(内链)还是垂直(块) (element: Element) => "vertical" \\&#124; "horizontal" ‘vertical’
calcElementOuterWidth 计算元素的宽度 (innerWidth: number, style: CSSStyleDeclaration) => number ‘vertical’

coordinate

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

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

  1. // 点的位置
  2. export interface IPoint {
  3. x: number;
  4. y: number;
  5. }
  6. // 点的类形态
  7. export class Point implements IPoint {
  8. x: number;
  9. y: number;
  10. constructor(x: number, y: number) {
  11. this.x = x;
  12. this.y = y;
  13. }
  14. }
  15. // 矩形,包含起点坐标以及宽高
  16. export interface IRect {
  17. x: number;
  18. y: number;
  19. width: number;
  20. height: number;
  21. }
  22. // 象限,框选多个组件时使用(猜测:内部指试图区域内,外部指试图区域外)
  23. export enum RectQuadrant {
  24. Inner1 = 'I1', //内部第一象限
  25. Inner2 = 'I2', //内部第二象限
  26. Inner3 = 'I3', //内部第三象限
  27. Inner4 = 'I4', //内部第四象限
  28. Outer1 = 'O1', //外部第一象限
  29. Outer2 = 'O2', //外部第二象限
  30. Outer3 = 'O3', //外部第三象限
  31. Outer4 = 'O4', //外部第四象限
  32. }
  33. export interface IPointToRectRelative {
  34. quadrant: RectQuadrant;
  35. distance: number;
  36. }
Name Description Type Default
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实现“函数在浏览器空闲时被调用”。

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

uid

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

Name Description Type Default
len 字符串长度 string 11 位包含字母数字的随机字符串

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

  1. let IDX = 36,
  2. HEX = '';
  3. while (IDX--) HEX += IDX.toString(36); // 通过依次从36到1转十六进制,最终返回`zyxwvutsrqponmlkjihgfedcba9876543210`
  4. export function uid(len?: number) {
  5. let str = '',
  6. num = len || 11;
  7. while (num--) str += HEX[(Math.random() * 36) | 0]; // 根据长度,给str拼接随机字符
  8. return str;
  9. }

globalThisPolyfill

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

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

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

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

@designable/react-settings-form

SettingsForm

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

Name Description Type Default
uploadAction 资源上传地址 string —-