Files
dooring/packages/dooringx-dumi-doc/docs/Guide/develop.md
2021-11-25 12:15:19 +08:00

17 KiB
Raw Blame History

title, toc, order
title toc order
dooringx-lib插件开发 menu 3

插件导入

dooringx-lib的插件需要一个类型为Partial<InitConfig>的对象。

对于多个插件需要使用dooringx-lib导出的userConfigMerge来进行合并。

userConfigMerge不是所有属性都会合并部分属性会进行覆盖。

 * 部分无法合并属性如果b传了会以b为准
 * initstore不合并
 * leftallregistmap合并
 * leftRenderListCategory合并
 * rightRenderListCategory合并
 * rightGlobalCustom 不合并
 * initComponentCache合并
 * initFunctionMap合并
 * initDataCenterMap合并
 * initCommandModule合并
 * initFormComponents合并

config支持部分配置异步导入比如左侧分类等这个是实验性功能所以不推荐这么做。

左侧面板

左侧面板传入leftRenderListCategory即可。

leftRenderListCategory: [
  {
        type: 'basic',
        icon: <HighlightOutlined />,
        displayName: '基础组件',
  },
  {
        type: 'xxc',
        icon: <ContainerOutlined />,
        custom: true,
        customRender: <div>我是自定义渲染</div>,
  },
],

type是分类左侧组件显示在哪个分类由该字段决定。

icon则是左侧分类小图标。

当custom为true时可以使用customRender自定义渲染。

左侧组件

插件导入

左侧组件要至于对象的LeftRegistMap中。

左侧组件支持同步导入或者异步导入。

const LeftRegistMap: LeftRegistComponentMapItem[] = [
  {
      type: 'basic',
      component: 'button',
      img: 'icon-anniu',
      displayName: '按钮',
      urlFn: () => import('./registComponents/button'),
  },
];

如果需要异步导入组件则需要填写urlFn需要一个返回promise的函数。也可以支持远程载入组件只要webpack配上就行了。

如果需要同步导入组件则需要将组件放入配置项的initComponentCache中这样在载入时便会注册进componentRegister里。

initComponentCache: {
    modalMask: { component: MmodalMask },  
},

组件编写

组件需要导出一个由ComponentItemFactory生成的对象。

const MButton = new ComponentItemFactory(
	'button',
	'按钮',
	{
		style: [
			createPannelOptions<FormMap, 'input'>('input', {
				receive: 'text', 
				label: '文字',
			}),
		],
		animate: [createPannelOptions<FormMap, 'animateControl'>('animateControl', {})],
		actions: [createPannelOptions<FormMap, 'actionButton'>('actionButton', {})],
	},
	{
		props: {
			...
			text:'yehuozhili'// input配置项组件接收的初始值
		},
	},
	(data, context, store, config) => {
		return <ButtonTemp data={data} store={store} context={context} config={config}></ButtonTemp>;
	},
	true
);

export default MButton;

其中第一个参数为组件注册名,第二个参数用来展示使用。

第三个参数用来配置右侧面板的配置项组件。其中键为右侧面板的分类,值为配置项组件数组。

第四个参数会配置组件的初始值,特别注意的是,制作组件必须要有初始宽度高度(非由内容撑开),否则会在适配时全选时产生问题。

这个初始值里有很多有用的属性比如fixed代表使用固定定位可以结合配置项更改该值使得组件可以fixed定位。

还有canDrag类似于锁定命令锁定的元素不可拖拽。

初始值里的rotate需要个对象value代表旋转角度canRotate 代表是否可以操作旋转。0.7.0版本开始支持)

第五个参数是个函数你将获得配置项中的receive属性暂且都默认该配置为receive传来的配置比如上例中receive的是text则该函数中data里会收到该字段。

context一般只有preview和edit用来进行环境判断。

config可以拿到所有数据用来制作事件时使用。

第六个参数resize 是为了判断是否能进行缩放当为false时无法进行缩放。

第七个参数needPosition某些组件移入画布后会默认采取拖拽的落点该配置项默认为true,就是需要拖拽的位置为false时将使用组件自身top和left定位来放置。

事件注册

时机注册

前面说了事件有时机和函数所以组件内可以使用hook注册时机

useDynamicAddEventCenter(pr, `${pr.data.id}-init`, '初始渲染时机'); //注册名必须带id 约定!
useDynamicAddEventCenter(pr, `${pr.data.id}-click`, '点击执行时机');

useDynamicAddEventCenter第一个参数是render的四个参数组成的对象。第二个参数是注册的时机名必须跟id相关这是约定否则多个组件可能会导致名称冲突并且方便查找该时机。

注册完时机后你需要将时机放入对应的触发位置上比如这个button的点击执行时机就放到onclick中

<Button
    onClick={() => {
        eventCenter.runEventQueue(`${pr.data.id}-click`, pr.config);
    }}
>
    yehuozhili
</Button> 

其中第一个参数则为注册的时机名第二个为render函数中最后个参数config

函数注册

函数由组件抛出,可以加载到事件链上。比如,注册个改变文本函数,那么我可以在任意组件的时机中去调用该函数,从而触发该组件改变文本。

函数注册需要放入useEffect中在组件卸载时需要卸载函数否则会导致函数越来越多。

注意id要带上组件id因为一个组件可以拖出n个组件生成n个函数。

useEffect(() => {
		const functionCenter = eventCenter.getFunctionCenter();
		const unregist = functionCenter.register(
			`${pr.data.id}+改变文本函数`,
			async (ctx, next, config, args, _eventList, iname) => {
				const userSelect = iname.data;
				const ctxVal = changeUserValue(
					userSelect['改变文本数据源'],
					args,
					'_changeval',
					config,
					ctx
				);
				const text = ctxVal[0];
				setText(text);
				next();
			},
			[
				{
					name: '改变文本数据源',
					data: ['ctx', 'input', 'dataSource'],
					options: {
						receive: '_changeval',
						multi: false,
					},
				},
			],
			`${pr.data.id}+改变文本函数`
		);
		return () => {
			unregist();
		};
}, []);

函数中参数与配置见后面函数开发。

右侧面板

右侧面板的配置和左侧面板一样:

export interface RightMapRenderListPropsItemCategory {
    type: string;
    icon: ReactNode;
    custom?: boolean;
    customRender?: (type: string, current: IBlockType) => ReactNode;
}

type会影响左侧组件在开发时第三个参数的键名。那个键名中即代表该右侧中展示的type。

icon则是可以放文字或者图标用来进行面板切换的。

如果custom为true该面板下的显示可以通过customRender自定义。

右侧组件

右侧组件导入

导入时,只要将开发的组件配成一个对象放入initFormComponents即可。

initFormComponents: Formmodules,

右侧组件开发

首先为了良好的开发体验需要定义个formMap类型

export interface FormBaseType {
	receive?: string;
}
export interface FormInputType extends FormBaseType {
	label: string;
}
export interface FormActionButtonType {}
export interface FormAnimateControlType {}
export interface FormMap {
	input: FormInputType;
	actionButton: FormActionButtonType;
	animateControl: FormAnimateControlType;
}

formMap的键名就是initFormComponents键名formMap的值对应组件需要收到的值。

以input组件为例FormInputType此时有2个属性label,receive。

那么在其开发该组件时props会收到

interface MInputProps {
	data: CreateOptionsRes<FormMap, 'input'>;
	current: IBlockType;
    config: UserConfig;
}

也就是其中data是formMap类型而current是当前点击的组件config就不用说了。

还记得在左侧组件开发中的第三个参数吗?这样就都关联起来了:


style: [
			createPannelOptions<FormMap, 'input'>('input', {
				receive: 'text',  
				label: '文字',
			}),
		],

createPannelOptions 这个函数的泛型里填入对应的组件,将会给收到的配置项良好的提示。

在配置项组件里所要做的就是接收组件传来的配置项然后去修改current的属性

function MInput(props: MInputProps) {
	const option = useMemo(() => {
		return props.data?.option || {};
	}, [props.data]);
	return (
		<Row style={{ padding: '10px 20px' }}>
			<Col span={6} style={{ lineHeight: '30px' }}>
				{(option as any)?.label || '文字'}
			</Col>
			<Col span={18}>
				<Input
					value={props.current.props[(option as any).receive] || ''}
					onChange={(e) => {
						const receive = (option as any).receive;
						const clonedata = deepCopy(store.getData());
						const newblock = clonedata.block.map((v: IBlockType) => {
							if (v.id === props.current.id) {
								v.props[receive] = e.target.value;
							}
							return v;
						});
						store.setData({ ...clonedata, block: [...newblock] });
					}}
				></Input>
			</Col>
		</Row>
	);
}

由于可以很轻松的拿到store所以可以在任意地方进行修改数据。

将组件的value关联current的属性onChange去修改store这样就完成了个双向绑定。

注意如果你的右侧组件需要用到block以外的属性可能需要去判断是否处于弹窗模式。

命令开发

命令的导入

命令对象导入到插件的initCommandModule里即可

initCommandModule: commandModules,

命令的开发

命令需要导出一个CommanderItemFactory生成的对象。

import { CommanderItemFactory } from 'dooringx-lib';
const undo = new CommanderItemFactory(
	'redo',
	'Control+Shift+z',
	(store) => {
		store.redo();
	},
	'重做'
);

export default undo;

第一个参数是注册名。 第二个参数是快捷键名快捷键映射是键盘事件key值

	Cancel: 3,
	Help: 6,
	Backspace: 8,
	Tab: 9,
	Clear: 12,
	Enter: 13,
	Shift: 16,
	Control: 17,
	Alt: 18,
	Pause: 19,
	CapsLock: 20,
	Escape: 27,
	Convert: 28,
	NonConvert: 29,
	Accept: 30,
	ModeChange: 31,
	' ': 32,
	PageUp: 33,
	PageDown: 34,
	End: 35,
	Home: 36,
	ArrowLeft: 37,
	ArrowUp: 38,
	ArrowRight: 39,
	ArrowDown: 40,
	Select: 41,
	Print: 42,
	Execute: 43,
	PrintScreen: 44,
	Insert: 45,
	Delete: 46,
	0: 48,
	')': 48,
	1: 49,
	'!': 49,
	2: 50,
	'@': 50,
	3: 51,
	'#': 51,
	4: 52,
	$: 52,
	5: 53,
	'%': 53,
	6: 54,
	'^': 54,
	7: 55,
	'&': 55,
	8: 56,
	'*': 56,
	9: 57,
	'(': 57,
	a: 65,
	A: 65,
	b: 66,
	B: 66,
	c: 67,
	C: 67,
	d: 68,
	D: 68,
	e: 69,
	E: 69,
	f: 70,
	F: 70,
	g: 71,
	G: 71,
	h: 72,
	H: 72,
	i: 73,
	I: 73,
	j: 74,
	J: 74,
	k: 75,
	K: 75,
	l: 76,
	L: 76,
	m: 77,
	M: 77,
	n: 78,
	N: 78,
	o: 79,
	O: 79,
	p: 80,
	P: 80,
	q: 81,
	Q: 81,
	r: 82,
	R: 82,
	s: 83,
	S: 83,
	t: 84,
	T: 84,
	u: 85,
	U: 85,
	v: 86,
	V: 86,
	w: 87,
	W: 87,
	x: 88,
	X: 88,
	y: 89,
	Y: 89,
	z: 90,
	Z: 90,
	OS: 91,
	ContextMenu: 93,
	F1: 112,
	F2: 113,
	F3: 114,
	F4: 115,
	F5: 116,
	F6: 117,
	F7: 118,
	F8: 119,
	F9: 120,
	F10: 121,
	F11: 122,
	F12: 123,
	F13: 124,
	F14: 125,
	F15: 126,
	F16: 127,
	F17: 128,
	F18: 129,
	F19: 130,
	F20: 131,
	F21: 132,
	F22: 133,
	F23: 134,
	F24: 135,
	NumLock: 144,
	ScrollLock: 145,
	VolumeMute: 181,
	VolumeDown: 182,
	VolumeUp: 183,
	';': 186,
	':': 186,
	'=': 187,
	'+': 187,
	',': 188,
	'<': 188,
	'-': 189,
	_: 189,
	'.': 190,
	'>': 190,
	'/': 191,
	'?': 191,
	'`': 192,
	'~': 192,
	'[': 219,
	'{': 219,
	'\\': 220,
	'|': 220,
	']': 221,
	'}': 221,
	"'": 222,
	'"': 222,
	Meta: 224,
	AltGraph: 225,
	Attn: 246,
	CrSel: 247,
	ExSel: 248,
	EraseEof: 249,
	Play: 250,
	ZoomOut: 251,

26个英文字母是忽略大小写的一个命令目前只能注册一个快捷键。不需要注册快捷键则填空字符串即可。

metakey与Controlkey相同写Control即可。

目前第三个参数只能获得store后续需要修改下。 0.2.0 版本第二个参数可以获得config同时commander不从index中导出需要使用时从config中获取。

最后个参数是展示名。

右键菜单

右键菜单可以进行自定义:

// 自定义右键
const contextMenuState = config.getContextMenuState();
const unmountContextMenu = contextMenuState.unmountContextMenu;
const commander = config.getCommanderRegister();
const ContextMenu = () => {
	const handleclick = () => {
		unmountContextMenu();
	};
	const forceUpdate = useState(0)[1];
	contextMenuState.forceUpdate = () => {
		forceUpdate((pre) => pre + 1);
	};
	return (
		<div
			style={{
				left: contextMenuState.left,
				top: contextMenuState.top,
				position: 'fixed',
				background: 'rgb(24, 23, 23)',
			}}
		>
			<div
				style={{ width: '100%' }}
				onClick={() => {
					commander.exec('redo');
					handleclick();
				}}
			>
				<Button>自定义</Button>
			</div>
		</div>
	);
};
contextMenuState.contextMenu = <ContextMenu></ContextMenu>;

先拿到contextMenuStatecontextMenuState上有个unmountContextMenu是关闭右键菜单方法。

所以在点击后需要调用关闭。

同时上面的left和top是右键的位置。

另外你还需要在组件内增加个强刷赋值给forceUpdate用于在组件移动时进行跟随。

函数开发

函数导入

函数导入做成对象置入initFunctionMap即可

initFunctionMap: functionMap,

函数开发

键名会显示出来所以键名是唯一的。

它的值是2个对象一个是函数内容fn一个是配置项config (0.10.0以上还需要传入函数名称,用于显示)。

config中的数组里每个配置会显示出来让用户去配置name则是展示名字data代表数据去哪里获取可以选择从输入框input数据源dataSource,上下文ctx中获取另外还有个特殊的弹窗modal

options中的receive表示会从args哪个键上获取该值。

multi代表是否允许多个选项配置。

dooringx-lib中写好了2个函数changeUserValue与changeUserValueRecord第一个函数会将得到的结果做成数组如果非multi则取第一个结果就行。而第二个函数会将结果做成对象比如用户在数据源中选了keya那么就会把数据源的键值对作为个对象返回。

fn中第一个ctx参数代表上下文如果有转换函数之类可能需要使用比如要把第一个函数的结果导给后面的函数

第二个参数next是需要运行完毕后执行的否则事件链会一直在该函数中不退出。

第三个参数config就可以拿到整个config对象。

第四个参数args是用户填写的参数会根据options里填写的字段进行返回。

第五个是eventList可以获取整个事件链的参数。

第六个参数iname可以拿到用户的选择项。

 通用GET请求函数: {
    fn: (ctx, next, config, args, _eventList, iname) => {
      console.log(args, '参数x');
      const userSelect = iname.data;
      const urlVal = changeUserValue(
        userSelect['请求url'],
        args,
        '_url',
        config,
        ctx
      ); // input datasource ctx //datasource会去取值 ctx取ctx上字段
      const paramSource = changeUserValueRecord(
        // 设定只能从datasource或者ctx里取
        userSelect['请求参数'],
        args,
        '_origin',
        config,
        ctx
      );
      const ctxVal = changeUserValue(
        userSelect['返回上下文字段'],
        args,
        '_ctx',
        config,
        ctx
      );
      // 检查参数是否存在
      // 都是数组非multi则取第一个。
      const url = urlVal[0];
      if (!url) {
        return next();
      }
      const ctxKey = ctxVal[0];

      axios
        .get(url, {
          params: {
            ...paramSource,
          },
        })
        .then((res) => {
          const data = res.data;
          ctx[ctxKey] = data;
          next();
        })
        .catch((e) => {
          console.log(e);
          next();
        });
    },
    config: [
      {
        name: '请求url',
        data: ['dataSource', 'ctx', 'input'],
        options: {
          receive: '_url',
          multi: false,
        },
      },
      {
        name: '请求参数',
        data: ['dataSource', 'ctx'],
        options: {
          receive: '_origin',
          multi: true,
        },
      },
      {
        name: '返回上下文字段',
        data: ['input'],
        options: {
          receive: '_ctx',
          multi: false,
        },
      },
    ],
    name: '通用GET请求函数'
  },

时机与函数装载

如果有需要,一般使用:

	eventCenter.manualUpdateMap(cur, displayName, arr);

manualUpdateMap第一个是时机名第二个是展示名第三个是用户选择。

更新事件中心后还需要更新store结果以store为准。