--- title: dooringx-lib插件开发 toc: menu order: 3 --- ### 插件导入 dooringx-lib的插件需要一个类型为`Partial`的对象。 对于多个插件,需要使用dooringx-lib导出的`userConfigMerge`来进行合并。 userConfigMerge不是所有属性都会合并,部分属性会进行覆盖。 ``` * 部分无法合并属性如果b传了会以b为准 * initstore不合并 * leftallregistmap合并 * leftRenderListCategory合并 * rightRenderListCategory合并 * rightGlobalCustom 不合并 * initComponentCache合并 * initFunctionMap合并 * initDataCenterMap合并 * initCommandModule合并 * initFormComponents合并 ``` config支持部分配置异步导入,比如左侧分类等,这个是实验性功能,所以不推荐这么做。 ### 左侧面板 左侧面板传入leftRenderListCategory即可。 ```js leftRenderListCategory: [ { type: 'basic', icon: , displayName: '基础组件', }, { type: 'xxc', icon: , custom: true, customRender:
我是自定义渲染
, }, ], ``` type是分类,左侧组件显示在哪个分类由该字段决定。 icon则是左侧分类小图标。 当custom为true时,可以使用customRender自定义渲染。 ### 左侧组件 ### 插件导入 左侧组件要至于对象的LeftRegistMap中。 左侧组件支持同步导入或者异步导入。 ```js const LeftRegistMap: LeftRegistComponentMapItem[] = [ { type: 'basic', component: 'button', img: 'icon-anniu', displayName: '按钮', urlFn: () => import('./registComponents/button'), }, ]; ``` 如果需要异步导入组件,则需要填写urlFn,需要一个返回promise的函数。也可以支持远程载入组件,只要webpack配上就行了。 如果需要同步导入组件,则需要将组件放入配置项的initComponentCache中,这样在载入时便会注册进componentRegister里。 ```js initComponentCache: { modalMask: { component: MmodalMask }, }, ``` ### 组件编写 组件需要导出一个由ComponentItemFactory生成的对象。 ```js const MButton = new ComponentItemFactory( 'button', '按钮', { style: [ createPannelOptions('input', { receive: 'text', label: '文字', }), ], animate: [createPannelOptions('animateControl', {})], actions: [createPannelOptions('actionButton', {})], }, { props: { ... text:'yehuozhili'// input配置项组件接收的初始值 }, }, (data, context, store, config) => { return ; }, 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注册时机: ```js useDynamicAddEventCenter(pr, `${pr.data.id}-init`, '初始渲染时机'); //注册名必须带id 约定! useDynamicAddEventCenter(pr, `${pr.data.id}-click`, '点击执行时机'); ``` useDynamicAddEventCenter第一个参数是render的四个参数组成的对象。第二个参数是注册的时机名,必须跟id相关,这是约定,否则多个组件可能会导致名称冲突,并且方便查找该时机。 注册完时机后,你需要将时机放入对应的触发位置上,比如这个button的点击执行时机就放到onclick中: ```js ``` 其中第一个参数则为注册的时机名,第二个为render函数中最后个参数config #### 函数注册 函数由组件抛出,可以加载到事件链上。比如,注册个改变文本函数,那么我可以在任意组件的时机中去调用该函数,从而触发该组件改变文本。 函数注册需要放入useEffect中,在组件卸载时需要卸载函数!否则会导致函数越来越多。 注意id要带上组件id,因为一个组件可以拖出n个组件生成n个函数。 ```js 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(); }; }, []); ``` 函数中参数与配置见后面函数开发。 ### 右侧面板 右侧面板的配置和左侧面板一样: ```js export interface RightMapRenderListPropsItemCategory { type: string; icon: ReactNode; custom?: boolean; customRender?: (type: string, current: IBlockType) => ReactNode; } ``` type会影响左侧组件在开发时第三个参数的键名。那个键名中即代表该右侧中展示的type。 icon则是可以放文字或者图标用来进行面板切换的。 如果custom为true,该面板下的显示可以通过customRender自定义。 ### 右侧组件 #### 右侧组件导入 导入时,只要将开发的组件配成一个对象放入initFormComponents即可。 ```js initFormComponents: Formmodules, ``` #### 右侧组件开发 首先为了良好的开发体验,需要定义个formMap类型: ```js 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会收到: ```js interface MInputProps { data: CreateOptionsRes; current: IBlockType; config: UserConfig; } ``` 也就是其中data是formMap类型,而current是当前点击的组件,config就不用说了。 还记得在左侧组件开发中的第三个参数吗?这样就都关联起来了: ```js style: [ createPannelOptions('input', { receive: 'text', label: '文字', }), ], ``` createPannelOptions 这个函数的泛型里填入对应的组件,将会给收到的配置项良好的提示。 在配置项组件里所要做的就是接收组件传来的配置项,然后去修改current的属性: ```js function MInput(props: MInputProps) { const option = useMemo(() => { return props.data?.option || {}; }, [props.data]); return ( {(option as any)?.label || '文字'}: { 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] }); }} > ); } ``` 由于可以很轻松的拿到store,所以可以在任意地方进行修改数据。 将组件的value关联current的属性,onChange去修改store,这样就完成了个双向绑定。 注意:如果你的右侧组件需要用到block以外的属性,可能需要去判断是否处于弹窗模式。 ### 命令开发 #### 命令的导入 命令对象导入到插件的initCommandModule里即可 ```js initCommandModule: commandModules, ``` #### 命令的开发 命令需要导出一个CommanderItemFactory生成的对象。 ```js import { CommanderItemFactory } from 'dooringx-lib'; const undo = new CommanderItemFactory( 'redo', 'Control+Shift+z', (store) => { store.redo(); }, '重做' ); export default undo; ``` 第一个参数是注册名。 第二个参数是快捷键名,快捷键映射是键盘事件key值: ```js 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中获取。 最后个参数是展示名。 ### 右键菜单 右键菜单可以进行自定义: ```js // 自定义右键 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 (
{ commander.exec('redo'); handleclick(); }} >
); }; contextMenuState.contextMenu = ; ``` 先拿到contextMenuState,contextMenuState上有个unmountContextMenu是关闭右键菜单方法。 所以在点击后需要调用关闭。 同时上面的left和top是右键的位置。 另外,你还需要在组件内增加个强刷赋值给forceUpdate,用于在组件移动时进行跟随。 ### 函数开发 #### 函数导入 函数导入做成对象置入initFunctionMap即可 ```js 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可以拿到用户的选择项。 ```js 通用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请求函数' }, ``` #### 时机与函数装载 如果有需要,一般使用: ```js eventCenter.manualUpdateMap(cur, displayName, arr); ``` manualUpdateMap第一个是时机名,第二个是展示名,第三个是用户选择。 更新事件中心后还需要更新store,结果以store为准。