JS低代码引擎跨iframe拖拽定位实现详解

作者:键盘侠时间:2026-10-26 10:36:48浏览:221

本文详解JS低代码引擎中跨iframe拖拽定位的实现机制。重点解析设计器与渲染器通过host通信唤起渲染器iframe的方法,以及DragObject、LocationEvent和DropLocation三大核心概念。通过绑定拖放事件、获取坐标、计算最近节点、应用嵌套规则及MobX状态管理,实现实时显示可插入点的功能。

低代码引擎

在低代码分层架构体系中,低代码引擎构成了最为复杂的核心模块。该引擎主要承担四大关键职能:入料、设计、画布渲染以及出码,其具体定义如下:

  • 入料环节负责向引擎内部注入配置好的设置器、插件以及各类组件资源。
  • 设计环节允许用户对组件执行布局调整、属性配置以及增删改等操作,最终生成符合页面搭建规范的JSON Schema数据。
  • 画布渲染环节负责将生成的JSON Schema数据转化为可视化的UI界面。
  • 出码环节则在页面发布时触发,将JSON Schema数据转换为目标手写代码。

本文重点探讨拖拽定位功能,即在拖拽过程中精准探测组件的可插入点。为确保渲染器拥有纯净的渲染环境,渲染器与设计器被隔离在独立的iframe中运行。因此,拖拽组件的操作不仅包含同一iframe内的交互,还涉及跨iframe的复杂场景。渲染器所在的iframe由设计器动态唤起,在深入解析拖拽定位之前,首先介绍如何唤起渲染器iframe。

唤起渲染器 iframe

通常,iframe元素的src属性被设置为固定的网页地址,以嵌入现有网页。然而,由于渲染器没有固定的网页地址,此处采用了一种特殊用法:通过调用document.write方法,直接向iframe所在的文档写入其需要加载的内容。设计器唤起渲染器iframe的具体流程如下图所示:

低代码引擎

设计器环境与渲染器环境通过host机制进行通信。SimulatorRenderer向host暴露了一系列API,协助设计器完成交互操作;同时,设计器也向host提供API,辅助渲染器完成画布渲染任务。

在设计器环境中,与渲染器环境相关的唯一元素是一个iframe对象,具体代码如下:

<iframe
     name="SimulatorRenderer"
     className="vitis-simulator-frame"
     style={frameStyle}
     ref={this.mountContentFrame}
/>

向iframe写入内容的操作发生在host.mountContentFrame方法中,相关代码片段如下:

this.frameDocument!.open()
this.frameDocument!.write(
     `<!doctype html>
      <html class="engine-design-mode">
        <head>
<meta charset="utf-8"/>
// 这里是渲染器环境要加载的css样式脚本
          ${styleTags}
        </head>
        <body>
	     // 这里是渲染器环境要加载的js脚本
            ${scriptTags}
        </body>
     </html>`
)
   this.frameDocument!.close()
// 监听iframe加载成功和加载失败的事件
this.frameWindow!.addEventListener('load', loaded);
this.frameWindow!.addEventListener('error', errored);

在使用低代码引擎设计界面时,为确保渲染器环境能正确显示画布,上述scriptTags中必须至少包含react、react-dom以及vitis-lowcode-simulator-renderer的js脚本。在开发阶段,vitis-lowcode-simulator-renderer的js脚本地址为http://localhost:5555/js/simulator-renderer.js;而在发布后,该js脚本地址则指向其npm包的js地址。

拖拽定位

拖拽定位是指当组件在画布区域被拖动时,界面实时显示该组件最近的可放置位置。这是一个与设计器紧密相关的功能,因此它与设计器处于同一个iframe中。相关的DOM元素被叠放在画布区域的上方,具体布局如下图所示:

低代码引擎

上图中蓝线所示位置即为被拖动组件最近的可放置点。实现此功能需要结合Element.getBoundingClientRect()方法与HTML5的拖放事件。通过给渲染器中的低代码组件设置ref属性,当组件装载到界面上时即可获取其DOM元素,从而计算出拖拽过程中鼠标所经过的低代码组件。

低代码组件的拖拽能力由Dragon实例提供。与拖拽相关的核心概念包含以下三项:

  • DragObject:代表被拖拽的对象,即画布中的低代码组件或组件面板上的低代码组件。
  • LocationEvent:携带了拖拽过程中产生的坐标信息以及被拖拽的对象数据。
  • DropLocation:表示被拖拽对象在画布上最近的可放置点。

DragObject是一个联合类型,针对拖拽不同位置的低代码组件,其类型有所区别,具体接口类型定义如下:

interface DragNodeObject {
    type: DragObjectType.Node; // 被拖拽的是画布中的低代码组件
    node: Node;
  }
  interface DragNodeDataObject {
    type: DragObjectType.NodeData; // 被拖拽的是组件面板上的低代码组件
    data: ComponentSpec;
  }
type DragObject = DragNodeObject | DragNodeDataObject

设计器利用LocationEvent来计算被拖拽对象最近的可放置点,其接口类型定义如下:

interface LocationEvent {
  dragObject: DragObject,
  originalEvent: DragEvent,
  clientX: number,
  clientY: number
}

上述接口中的clientY和clientX属性源自DragEvent对象,它们用于计算画布中距离鼠标最近的Node节点。

DropLocation是拖拽操作需要计算得出的结果,其接口类型定义如下:

interface DropLocation {
  // 被拖拽对象可放置的容器
  containerNode: Node;
  // 被拖拽对象在容器中的插入点
  index: number;
}

以拖拽组件面板中的低代码组件为例,在画布区域显示组件最近的可放置点,总体而言需经历以下6个步骤。

1. 绑定拖放事件

在iframe和组件面板中的低代码组件上绑定拖放事件,从而得到DragObject,相关代码片段如下:

// 当组件面板中的组件开始拖动时
<div draggable={true} onDragStart={() => onDragStart(item.packageName)}>xxx</div>
const onDragStart = (packageName: string) => {
  // 得到DragObject
dragon.onNodeDataDragStart(packageName)
}
// 给 iframe 绑定dragover事件,当拖动操作进入画布区域时触发事件
this.frameDocument?.addEventListener('dragover', (e: DragEvent) => {
      e.preventDefault()
      this.project.designer.dragon.onDragOver(e)
})

2. 获取拖拽过程中的 LocationEvent

LocationEvent将在iframe的dragover事件处理程序中实时获取,具体代码如下:

onDragOver = (e: DragEvent) => {
    // 获取 locateEvent 只是简单的取值
	const locateEvent = this.createLocationEvent(e)
}
createLocationEvent = (e: DragEvent): LocationEvent => {
        return {
            dragObject: this.dragObject,
            originalEvent: e,
            clientX: e.clientX,
            clientY: e.clientY
        }
}

3. 获取离鼠标最近的 Node

Node节点被装载在渲染器环境中,只有SimulatorRenderer实例清楚每个Node的具体位置。因此,这一步需要调用SimulatorRenderer给host提供的getClosestNodeIdByLocation方法,该方法的代码如下:

getClosestNodeIdByLocation = (point: Point): string | undefined => {
    // 第一步:找出包含 point 的全部 dom 节点
    const suitableContainer = new Map<string, DomNode>()
    for (const [id, domNode] of reactInstanceCollector.domNodeMap) {
        const rect = this.getNodeRect(id)
        if (!domNode || !rect) continue
        const { width, height, left, top } = rect
        if (left < point.clientX && top < point.clientY && width + left > point.clientX && height + top > point.clientY) {
            suitableContainer.set(id, domNode)
        }
    }
    // 第二步:找出离 point 最近的 dom 节点
    const minGap: {id: string| undefined; minArea: number} = {
        id: undefined,
        minArea: Infinity
    }
    for (const [id, domNode] of suitableContainer) {
        const { width, height } = domNode.rect
        if (width *  height  < minGap.minArea) {
            minGap.id = id;
            minGap.minArea = width *  height
        }
    }
    return minGap.id
}

上述reactInstanceCollector对象中保存了画布上全部低代码组件的DOM节点。实现这一目的需借助React的ref属性,此处不再展开详细介绍。

4. 获取拖拽对象最近的可放置容器

每个低代码组件均可设置嵌套规则,规定哪些组件可作为其子元素或父元素。不符合规则的组件不可放置,在此步骤中将应用组件的嵌套规则,具体代码如下:

getDropContainer = (locateEvent: LocationEvent) => {
    // 从上一步得来的潜在容器
    let containerNode = this.host.getClosestNodeByLocation({clientX: locateEvent.clientX, clientY: locateEvent.clientY})
    const thisComponentSpec: ComponentSpec = locateEvent.dragObject.data
    while(containerNode) {
        if (containerNode.componentSpec.isCanInclude(thisComponentSpec)) {
            return containerNode
        } else {
            // 继续往上找父级
            containerNode = containerNode.parent
        }
    }
}

5. 计算被拖动的对象在容器中的插入点

容器可能包含多个子元素,在此步骤中将利用鼠标位置计算被拖动对象在容器中的插入点,从而得到最终的DropLocation,具体代码如下:

// 初始值
const dropLocation: DropLocation = { index: 0, containerNode: container}
const { childrenSize, lastChild } = container
const { clientY } = locateEvent
if (lastChild) {
    const lastChildRect = this.designer.getNodeRect(lastChild.id)
    // 判断是否要插到容器的末尾
    if (lastChildRect &amp;&amp; clientY &gt; lastChildRect.bottom) {
        dropLocation.index = childrenSize
    } else {
        let minDistance = Infinity
        // 容器中最近的插入点
        let minIndex = 0
        for (let index = 0 ; index &lt; childrenSize; index ++) {
            const child = container.getChildAtIndex(index)!
            const rect = this.designer.getNodeRect(child.id)
            if (rect &amp;&amp; Math.abs(rect.top - clientY) &lt; minDistance) {
                minDistance = Math.abs(rect.top - clientY)
                minIndex = index
            }
        }
        dropLocation.index = minIndex
    }
}
return dropLocation

6. 在界面上提示最近的插入位置

经过前述步骤已获得插入位置,现在需要在界面上向用户显示相应的提示。此处需使用状态管理库MobX,在此之前需将Dragon实例转换为可观察对象,并在React组件中使用mobx-react导出的observer,具体代码如下:

import { observer } from 'mobx-react'
observer(function InsertionView() {
    const [style, setStyle] = useState<React.CSSProperties>({})
    useEffect(() => {
        const dropLocation = observableProject.designer.dragon.dropLocation
        if (!dropLocation) {
            setStyle({})
        } else {
            const { width, left, top } = dropLocation.containerRect
            setStyle({
                borderTopStyle: 'solid',
                width,
                left,
                top
            })
        }
    }, [observableProject.designer.dragon.dropLocation])
    return (
       // 这个元素被绝对定位到画布区域的上面
        <div className='vitis-insertion-view' style={style}></div>
    )
})

当dragon.dropLocation的值发生变化时,InsertionView组件将重新渲染,实时向用户提示拖拽对象对接的可插入点。

写在后面

低代码的拖拽定位功能远不止本文介绍的这些,至少还包含悬停探测等功能,详情可查看开源项目。该开源项目持续更新中。

以上就是JS分层架构低代码跨iframe拖拽示例详解的详细内容,更多关于JS分层架构跨iframe拖拽的资料请关注本站其它相关文章!

作者最新文章