首頁 > 軟體

JS分層架構低程式碼跨iframe拖拽範例詳解

2023-02-20 06:01:05

低程式碼引擎

低程式碼引擎是低程式碼分層架構中最複雜的部分,引擎的核心功能包含入料、設計、畫布渲染和出碼等,它們的含義如下:

  • 入料:向引擎注入設定器、外掛和元件。
  • 設計:對元件進行佈局設定、屬性設定以及增刪改操作後,形成符合頁面搭建協定的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,等釋出之後vitis-lowcode-simulator-renderer 的 js 指令碼地址是其 npm 包的 js 地址。

拖拽定位

拖拽定位指的是當元件在畫布區域拖動時,介面實時的顯示元件最近的可放置位置,這是一個與設計器強相關的功能,所以與設計器處於同一個 iframe,相關的 DOM 元素被疊放在畫布區域的上面,如下圖所示:

上圖藍線所在的位置就是被拖動元件最近可放置的位置,實現該功能需用到 Element.getBoundingClientRect() 方法和 HTML5 的拖放事件。給渲染器中的低程式碼元件設定 ref 屬性,當其裝載到介面上即可得到元件的 DOM 元素,從而計算出拖拽過程中滑鼠經過的低程式碼元件。

低程式碼元件的拖拽能力由 Dragon 範例提供,與拖拽相關的概念有如下3個:

  • 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 的程式碼如下:

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拖拽的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com