官方使用 designable 工程开发了“表单设计器”,事实上 designable 不局限于此,做页面设计器、bi设计器都可以。
目前designable 工程暂时没有开发文档,可以参看这篇文章进行了解。当然不看也可以,因为我们并不是要去开发 designable 工程,而是使用 designable 里的模块而已,就像我们只需要会用 antd 的组件库,而不需要掌握 antd 开源工程一样。
@designable/react
observer
observer 是一个 HOC, Function Component 变成 Reaction,每次视图重新渲染就会收集依赖,依赖更新会自动重渲染
使用方式参看:
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 不了解,可以看这里。
一码胜千言:
WorkspacePanel.Item = (props) => {const prefix = usePrefix('workspace-panel-item');return (<divclassName={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、预览)的容器组件,并给子组件赋能。
| Name | Description | Type | Default |
|---|---|---|---|
| type | 容器类型,用于标识它是什么视图模式的容器 | string |
‘DESIGNABLE’ |
| scrollable | 容器的内容溢出是否需要滚动条,如果开启样式的 overflow 等于 overlay,关闭则为 hidden。 | boolean |
true |
| 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
主布局面板,整个工程的壳。
| 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 \\| string |
—- |
| onChange | 切换时触发的回调函数(开始切换就执行了,早于展示) | (activeKey: number \\| 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<ReactNode> |
—- |
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)); // trueconsole.log(isPlainObj(b)); // falseconsole.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" \\| "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;}
| 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 位包含字母数字的随机字符串 |
文件里代码蛮短的,可以读一下:
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
右侧设置面板组件(具体如何作拓展配置,待完善)
| Name | Description | Type | Default |
|---|---|---|---|
| uploadAction | 资源上传地址 | string |
—- |
