低代码平台开发 - 编辑器拓展

设计器(编辑器)这边内容比较杂,我们这次挑两个讲,一个是自定义出码,一个是新版本引擎中 array-setter 存在的问题
这期和之前的文章关联性不大,可以直接在阿里的低代码引擎初始化的目录下进行,如何搭建阿里低代码引擎平台可以参考我之前的文章

阿里低代码引擎使用 - 项目启动 & 本地物料开发

题外话:无法使用 antd 组件 / antd 组件样式不生效

我们在设计器当中开发时想引入 antd 的组件,会发现 antd 的组件无法使用,或者样式不生效。目前我的解决办法是将 antd 包升到 5.x 版本。我本地使用的是 5.7.1

自定义出码

引擎自带的出码是基于 React 的,组件是类式组件,也就是用的 class。很多场景都无法满足。
不过本文的重点还是专注于熟悉如何使用低代码引擎,不会对出码功能本身做太详细的展开,这次就带着做一个非常简单的 HTML 结构的出码功能,非组件式的

在页面上添加自己的按钮

第一步,我们需要在设计器页面当中添加自己的按钮,也就是入口
官网中的介绍如下

插件拓展 - 面板拓展

我的想法是添加一个按钮,按钮单独打开一个面板,将一部分功能集合全塞在这里,所以我选择新建一个 layout 目录,然后创建我们的按钮

import { IPublicModelPluginContext } from '@alilc/lowcode-types';
import CustomerPanel from './customerPanel';	//	这是一个自定义的组件// 保存功能示例
const CustomerLayout = (ctx: IPublicModelPluginContext) => {return {async init() {const { skeleton } = ctx;skeleton.add({name: 'CustomerLayout',area: 'leftArea',type: 'PanelDock',props: {align: 'right',icon: "wenjian",description: "自定义功能集合",},content: <CustomerPanel ctx={ctx} />,panelProps: {floatable: true, // 是否可浮动height: 300,hideTitleBar: false,maxHeight: 800,maxWidth: 1200,title: "自定义功能集合",width: 600,},});},};
}
CustomerLayout.pluginName = 'CustomerLayout';
CustomerLayout.meta = {dependencies: ['EditorInitPlugin'],
};
export default CustomerLayout;

这是我的目录结构
image.png
并不一定非要取名 layout,这个是完全取决于自己的,引擎并没有约定的目录结构。customerPanel 是自定义的组件,大家自由发挥,这个和写普通组件是完全一样的。
找到 src 目录下的 index,导入我们的按钮并且注册

···	// 省略部分代码
import CustomerLayout from './layout';
···	// 省略部分代码
await plugins.register(CustomerLayout);
···	// 省略部分代码

刷新页面,我们就能够看到我们添加的按钮,在左侧的导航栏中
image.png
点击会弹出一个面板,而面板当中的内容,就取决于你刚刚自定义的组件内容,这里我添加了一个按钮,并设置点击时会再弹出一个面板,而我们自定义显示的出码代码就会在这个面板当中,这个部分完全自由发挥,请大家自行完成。
image.png

确认出码逻辑

这一步我们确认出码逻辑,先了解一下官方自带的出码逻辑

出码模块设计

简单来讲,官方的出码是基于 schema 树的,这也是正确的做法,后续我们也会做相关的解析。这次暂时不做这么复杂的,我们做最简单的出码功能,不基于 schema,我们只把页面的 dom 树扒下来
我们随意拖几个组件进设计器,然后打开控制台,观察页面元素
image.png
image.png
可以看到,引擎把内容都放进了一个 iframe 中,在一个 id 为 app 的 dom 中
我们可以通过代码直接拿到 dom 树

const iframe = document.getElementsByClassName('lc-simulator-content-frame')[0]
// @ts-ignore
const domTree = iframe.contentWindow.document.getElementById('app');
const pageDom = domTree?.children[0];
const contentDom = (pageDom.cloneNode(true).children as Element[]);
const container = document.createElement('div');
container.append(...contentDom);
const domTreeHtml = container.innerHTML;

最后的 domTreeHtml 就是 dom 树了,要注意如果是容器组件的话,在容器组件中没有其他组件时,引擎会在容器组件中插入一个提示信息,就是下面这样
image.png
它的 dom 节点是这样的
image.png
因此我们直接拿 dom 树的话需要处理这个问题,直接遍历 dom 树清除就好了,可以参考下面的代码

const clearContainerDom = (elems: Element[]) => {Array.from(elems)for (const elem of Array.from(elems)) {if (elem.classList.contains('lc-container-placeholder')) {elem.parentElement?.removeChild(elem);continue;}if (elem.children.length > 0) {clearContainerDom((elem.children as unknown as Element[]));}}
}

然后在刚刚拿 dom 树的地方调用这个函数就行了

···	// 其他代码
clearContainerDom(contentDom);
const container = document.createElement('div');
container.append(...contentDom);

渲染 dom 代码

最后,展示我们的代码,这个网上的方案有很多,大家可以自行完成。我这边也从网上看到一个方案,大家可以参考
首先格式化 dom 代码的内容就不展示了,大家自行在网上查找解决方案
推荐大家使用 highlight.js 作为染色方案

import hljs from 'highlight.js/lib/common';const target = hljs.highlightAuto(格式化后的dom代码, ['html']).value;

在 React 中,我们可以通过下面的方式将我们的 dom 放进元素当中

import React, { useRef, useState } from 'react';···
const contentDom = useRef<HTMLPreElement>(null!);
···const target = hljs.highlightAuto(text, ['html']).value;	// 这是刚才的代码
contentDom.current.innerHTML = target;···
<pre className='customer-code-pre'><code className='customer-code-block hljs language-html' ref={contentDom}></code>
</pre>
···

我们可以直接在样式中指定对应 class 的颜色

.hljs-tag,
.hljs-keyword,
.hljs-selector-tag,
.hljs-attr,
.hljs-literal,
.hljs-strong,
.hljs-name {color: #f92672;
}.hljs-string,
.hljs-bullet,
.hljs-subst,
.hljs-title,
.hljs-section,
.hljs-emphasis,
.hljs-type,
.hljs-built_in,
.hljs-builtin-name,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-addition,
.hljs-variable,
.hljs-template-tag,
.hljs-template-variable {color: #a6e22e;
}

最后简单展示下效果
image.png
渲染代码和引擎基本没啥关系了,所以过的很快,如果有疑问可以留言

解决 array-setter 的问题

再补充一点目前引擎存在的问题,要讲这个需要结合物料库的内容(什么是物料库可以参考官方文档,或者我之前的文章)

问题描述

当我们设置一个组件的某个属性是数组类型时,我们可以在设计器当中添加这个数组的元素
比如下面这个组件

export interface AntdSelectProps {/*** 选项* @componentName NewArraySetter*/options: {label: string, value: string}[]
}const AntdSelect: React.FC<AntdSelectProps> = ({options = []
}) => {return (<select><option value="">请选择</option>{options?.length > 0 &&options.map((item, index) => {return <option key={index} value={item.value}>{item.label}</option>})}</select>)
}

这是一个下拉框组件,它的下拉选项由传入的 options 属性决定
它在设计器当中是这样的
image.png
注意右侧的添加属性
我们点击后可以添加下拉选项
image.png
这是正常的效果,但是在最近几个版本当中,会出现问题
image.png
当我们添加一个选项之后再次点击,就会报错并且无法再次添加内容
我使用的引擎版本

"@alilc/lowcode-engine": "1.2.3",
"@alilc/lowcode-engine-ext": "1.0.6-beta.19",

这个问题查官方的 git 下面提了不少

解决办法

如果你本地跑起物料库的服务时,会发现在物料库自带的设计器当中并不存在这个问题,那么我的解决办法也非常简单,添加一个新的设置器替代现版本的 array-setter 设置器,而代码就来自物料库当中的 array-setter 代码
有点绕,我们看具体怎么做就行,回到我们设计器的目录下,新建一个 setters 目录,添加一个 new-array-setter.tsx
我们把物料库的服务跑起来,找到 array-setter 的代码位置:

webpack://AliLowCodeEngineExt/src/setter/array-setter/index.tsx

我们拷贝所有代码复制当我们刚刚的 new-array-setter.tsx,解决掉部分依赖和路径问题,最后修改成下面的代码

import * as React from 'react';
import { Component, Fragment } from 'react';
import { common } from '@alilc/lowcode-engine';
import { Button, Message } from '@alifd/next';
import { IPublicModelSettingField, IPublicTypeSetterType, IPublicTypeFieldConfig, IPublicTypeSetterConfig } from '@alilc/lowcode-types';
import CustomIcon from '@alilc/lowcode-engine-ext/es/components/custom-icon';
import Sortable from '@alilc/lowcode-engine-ext/es/setter/array-setter/sortable';
// import './style.less';
const { editorCabin, skeletonCabin } = common;
const { Title } = editorCabin;
const { createSettingFieldView, PopupContext } = skeletonCabin;interface ArraySetterState {items: IPublicModelSettingField[];
}/*** onItemChange 用于 ArraySetter 的单个 index 下的数据发生变化,* 因此 target.path 的数据格式必定为 [propName1, propName2, arrayIndex, key?]。** @param target* @param value*/
function onItemChange (target: IPublicModelSettingField, items: IPublicModelSettingField[], props: ArraySetterProps) {const targetPath: Array<string | number> = target?.path;if (!targetPath || targetPath.length < 2) {console.warn(`[ArraySetter] onItemChange 接收的 target.path <${targetPath || 'undefined'}> 格式非法需为 [propName, arrayIndex, key?]`,);return;}const { field, value: fieldValue } = props;// const { items } = this.state;const { path } = field;if (path[0] !== targetPath[0]) {console.warn(`[ArraySetter] field.path[0] !== target.path[0] <${path[0]} !== ${targetPath[0]}>`,);return;}try {const index = +targetPath[targetPath.length - 2];if (typeof index === 'number' && !isNaN(index)) {fieldValue[index] = items[index].getValue();field?.extraProps?.setValue?.call(field, field, fieldValue);}} catch (e) {console.warn('[ArraySetter] extraProps.setValue failed :', e);}
};interface ArraySetterProps {value: any[];field: IPublicModelSettingField;itemSetter?: IPublicTypeSetterType;columns?: IPublicTypeFieldConfig[];multiValue?: boolean;hideDescription?: boolean;onChange?: Function;extraProps: {renderFooter?: (options: ArraySetterProps & {onAdd: (val?: {}) => any}) => any}
}export class ListSetter extends Component<ArraySetterProps, ArraySetterState> {state: ArraySetterState = {items: [],};private scrollToLast = false;constructor(props: ArraySetterProps) {super(props);}static getDerivedStateFromProps(props: ArraySetterProps, state: ArraySetterState) {const items: IPublicModelSettingField[] = [];const { value, field } = props;const valueLength = value && Array.isArray(value) ? value.length : 0;for (let i = 0; i < valueLength; i++) {let item = state.items[i];if (!item) {item = field.createField({name: i.toString(),setter: props.itemSetter,forceInline: 1,type: 'field',extraProps: {defaultValue: value[i],setValue: (target: IPublicModelSettingField) => {onItemChange(target, items, props);},},});}items.push(item);}return {items,};}onSort(sortedIds: Array<string | number>) {const { onChange, value: oldValues } = this.props;const { items } = this.state;const values: any[] = [];const newItems: IPublicModelSettingField[] = [];sortedIds.map((id, index) => {const item = items[+id];item.setKey(index);values[index] = oldValues[id as number];newItems[index] = item;return id;});this.setState({items: newItems,});onChange?.(values);}onAdd(newValue?: {[key: string]: any}) {const { itemSetter, field, onChange, value = [] } = this.props;const values = value || [];const initialValue = (itemSetter as any)?.initialValue;const defaultValue = newValue ? newValue : (typeof initialValue === 'function' ? initialValue(field) : initialValue);values.push(defaultValue);this.scrollToLast = true;onChange?.(values);}onRemove(removed: IPublicModelSettingField) {const { onChange, value } = this.props;const { items } = this.state;const values = value || [];let i = items.indexOf(removed);items.splice(i, 1);values.splice(i, 1);const l = items.length;while (i < l) {items[i].setKey(i);i++;}removed.remove();const pureValues = values.map((item: any) => typeof(item) === 'object' ? Object.assign({}, item):item);onChange?.(pureValues);}componentWillUnmount() {this.state.items.forEach((field) => {field.purge();});}render() {const { hideDescription, extraProps = {} } = this.props;const { renderFooter } = extraProps;let columns: any = null;const { items } = this.state;const { scrollToLast } = this;this.scrollToLast = false;if (this.props.columns) {columns = this.props.columns.map((column) => (<Title key={column.name} title={column.title || (column.name as string)} />));}const lastIndex = items.length - 1;const content =items.length > 0 ? (<div className="lc-setter-list-scroll-body"><Sortable itemClassName="lc-setter-list-card" onSort={this.onSort.bind(this)}>{items.map((field, index) => (<ArrayItemkey={index}scrollIntoView={scrollToLast && index === lastIndex}field={field}onRemove={this.onRemove.bind(this, field)}/>))}</Sortable></div>) : (<div className="lc-setter-list-notice">{this.props.multiValue ? (<Message type="warning">当前选择了多个节点,且值不一致,修改会覆盖所有值</Message>) : (<Message type="notice" size="medium" shape="inline">暂时还没有添加内容</Message>)}</div>);return (<div className="lc-setter-list lc-block-setter">{!hideDescription && columns && items.length > 0 ? (<div className="lc-setter-list-columns">{columns}</div>) : null}{content}<div className="lc-setter-list-add">{!renderFooter ? (<Button text type="primary" onClick={() => {this.onAdd()}}><span>添加一项 +</span></Button>) : renderFooter({...this.props, onAdd: this.onAdd.bind(this),})}</div></div>);}
}
class ArrayItem extends Component<{field: IPublicModelSettingField;onRemove: () => void;scrollIntoView: boolean;
}> {private shell?: HTMLDivElement | null;componentDidMount() {if (this.props.scrollIntoView && this.shell) {this.shell.parentElement!.scrollIntoView({ behavior: 'smooth', block: 'nearest' });}}render() {const { onRemove, field } = this.props;return (<divclassName="lc-listitem"ref={(ref) => {this.shell = ref;}}><div className="lc-listitem-body">{createSettingFieldView(field, field.parent)}</div><div className="lc-listitem-actions"><Button size="small" ghost="light" onClick={onRemove} className="lc-listitem-action"><CustomIcon type="icon-ic_delete" /></Button><Button draggable size="small" ghost="light" className="lc-listitem-handler"><CustomIcon type="icon-ic_drag" /></Button></div></div>);}
}class TableSetter extends ListSetter {// todo:// forceInline = 1// has more actions
}export default class ArraySetter extends Component<{value: any[];field: IPublicModelSettingField;itemSetter?: IPublicTypeSetterType;mode?: 'popup' | 'list';forceInline?: boolean;multiValue?: boolean;
}> {static contextType = PopupContext;private pipe: any;render() {const { mode, forceInline, ...props } = this.props;const { field, itemSetter } = props;let columns: IPublicTypeFieldConfig[] | undefined;if ((itemSetter as IPublicTypeSetterConfig)?.componentName === 'ObjectSetter') {const items: IPublicTypeFieldConfig[] = (itemSetter as any).props?.config?.items;if (items && Array.isArray(items)) {columns = items.filter((item) => item.isRequired || item.important || (item.setter as any)?.isRequired,);if (columns.length > 4) {columns = columns.slice(0, 4);}}}if (mode === 'popup' || forceInline) {const title = (<Fragment>编辑:<Title title={field.title} /></Fragment>);if (!this.pipe) {let width = 360;if (columns) {if (columns.length === 3) {width = 480;} else if (columns.length > 3) {width = 600;}}this.pipe = this.context.create({ width });}this.pipe.send(<TableSetter key={field.id} {...props} columns={columns} />, title);return (<Buttontype={forceInline ? 'normal' : 'primary'}onClick={(e) => {this.pipe.show((e as any).target, field.id);}}><CustomIcon type="icon-bianji" size="small" />{forceInline ? title : '编辑数组'}</Button>);} else {return <ListSetter {...props} columns={columns?.slice(0, 4)} />;}}
}

然后就是注册这个设置器,我们找到 src\plugins\plugin-custom-setter-sample\index.tsx

import { IPublicModelPluginContext } from '@alilc/lowcode-types';
import NewArraySetter from 'src/setters/new-array-setter';// 保存功能示例
const CustomSetterSamplePlugin = (ctx: IPublicModelPluginContext) => {return {async init() {const { setters } = ctx;setters.registerSetter('NewArraySetter', NewArraySetter);},};
}
CustomSetterSamplePlugin.pluginName = 'CustomSetterSamplePlugin';
export default CustomSetterSamplePlugin;

这里清除了没用的代码,如果拿不准不删除也是完全没问题的
最后就是怎样使用我们新建的设置器,回到物料库,找到对应组件的描述文件:lowcode/xxx/meta.ts
image.png
在此处指名要使用到的设置器即可

如果是跟着我物料库的文章做下来的,那更简单,直接在对应组件属性的注释当中声明即可
image.png
需要注意,之前的逻辑下必须要在 inject.config.js 配置了对应的组件才行,可以简单配置 group 或者 category 就能看到效果

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/319882.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

当AI遇见大脑:电脑与人脑协同“进化”

编者按&#xff1a;2023年是微软亚洲研究院建院25周年。借此机会&#xff0c;我们特别策划了“智启未来”系列文章&#xff0c;邀请到微软亚洲研究院不同研究领域的领军人物&#xff0c;以署名文章的形式分享他们对人工智能、计算机及其交叉学科领域的观点洞察及前沿展望。希望…

Spring常用注解及模拟用户登录流程示例

注解 Resource注解实现自动注入 (反射)代码块xml配置文件 Autowired注解实现自动化注入代码块xml配置文件 扫描器-四个注解Dao层-RepositoryService层-ServiceController层-Controller测试任意类-Component 常用注解示例-模拟用户登录配置自动扫描的xml文件实体类Userdao层消息…

椭球面系列---大地坐标和笛卡尔坐标的相互转换

目录 大地坐标笛卡尔坐标大地坐标 ( λ , φ , h ) (\lambda,\varphi,h) (λ,φ,h)转换为笛卡尔坐标 ( x , y , z ) (x,y,z) (x,y,z)笛卡尔坐标 ( x , y , z ) (x,y,z) (x,y,z)转换为大地坐标 ( λ , φ , h ) (\lambda,\varphi,h) (λ,φ,h) 椭球体下&#xff0c;尤其是地球的…

高端大气的在线文档

背景 产品介绍&#xff0c;帮助手册&#xff0c;操作手册&#xff0c;开发说明&#xff0c;个人的简单网站等等&#xff0c;都需要一个在线的文档&#xff0c;特别是开源社区的在线文档也非常需要&#xff0c;开源社区也为此提供了大量的工具。如何找到一款高端大气的&#xf…

支付宝小程序源码系统:自由DIY+完整的安装部署教程

随着移动互联网的快速发展&#xff0c;支付宝小程序已成为众多企业及开发者关注的焦点。然而&#xff0c;市面上的支付宝小程序源码系统往往难以满足个性化需求&#xff0c;且安装部署过程复杂。今天来给大家分享一款支付宝小程序源码系统。这款自由DIY的支付宝小程序源码系统&…

析构和友元函数

1. 类的析构函数 析构函数的作用&#xff0c;用于释放该类所占用的资源&#xff08;或者说释放对象&#xff09;在类的对象使用完时&#xff08;当类的对象超出了作用域&#xff09;&#xff0c;会自动调用析构函数&#xff1b;如果是在堆区实例化的对象&#xff0c;那么当手动…

Redis 持久化—RDB

文章目录 1. 为什么需要Redis持久化&#xff1f;2. Redis持久化的几种方式3. RDB简介4. 持久化触发4.1 手动触发4.1.1 save命令4.1.2 bgsave 命令 4.2 自动触发4.2.1 save m n4.2.2 flushall4.2.3 主从同步触发 5. 配置说明6. 配置配置7. 配置配置8. RDB 文件恢复9. RDB 优缺点…

如何计算非线性负载的功率需求?

非线性负载的功率需求计算是一个相对复杂的过程&#xff0c;因为非线性负载的电流和电压之间的关系不是简单的正比关系。在计算非线性负载的功率需求时&#xff0c;需要考虑负载的特性、工作状态以及电源电压等因素。 确定负载的类型&#xff1a;首先需要了解负载的具体类型&am…

基于Kettle开发的web版数据集成开源工具(data-integration)-部署篇

目录 &#x1f4da;第一章 前言&#x1f4d7;背景&#x1f4d7;目的&#x1f4d7;总体方向 &#x1f4da;第二章 下载编译&#x1f4d7;下载&#x1f4d7;编译 &#x1f4da;第三章 部署&#x1f4d7;准备工作&#x1f4d5; 安装数据库&redis&consul&#x1f4d5; 修改…

四则运算 C语言xdoj20

问题描述&#xff1a; 输入两个整数和一个四则运算符&#xff0c;根据运算符计算并输出其运算结果&#xff08;和、差、积、商、余之一&#xff09;。注意做整除及求余运算时&#xff0c;除数不能为零。 输入说明&#xff1a; 使用scanf()函数输入两个整数和一个运算符&#xf…

Application layer

title: 应用层 date: 2023-12-20 21:03:48 tags: 知识总结 categories: 计算机网络 应用层&#xff1a;负责最直观的应用请求的封装、发起 一、域名系统DNS 连接在互联网上的主机不仅有IP地址&#xff0c;还有便于用户记忆的主机名字。域名系统DNS能够把互联网上的主机的名字…

Wireshark本地回环网络抓包

背景 因为发往本机的数据包是通过回环地址的&#xff0c;即&#xff1a;数据包不会通过真实的网络接口发送&#xff0c;因此我们需要通过设置路由规则来让本来发到虚拟网络接口的数据包发送到真实网络接口即可。 场景描述&#xff1a;在网络程序开发的过程中&#xff0c;有时…