JS分層架構(gòu)低代碼跨iframe拖拽示例詳解
低代碼引擎
低代碼引擎是低代碼分層架構(gòu)中最復(fù)雜的部分,引擎的核心功能包含入料、設(shè)計(jì)、畫(huà)布渲染和出碼等,它們的含義如下:
- 入料:向引擎注入設(shè)置器、插件和組件。
- 設(shè)計(jì):對(duì)組件進(jìn)行布局設(shè)置、屬性設(shè)置以及增刪改操作后,形成符合頁(yè)面搭建協(xié)議的JSON Schema。
- 畫(huà)布渲染:將 JSON Schema 渲染成 UI 界面。
- 出碼:將 JSON Schema 轉(zhuǎn)化成手寫(xiě)代碼,這通常發(fā)生在頁(yè)面發(fā)布的時(shí)候。
本文主要介紹拖拽定位,即:拖拽過(guò)程中探測(cè)組件的可插入點(diǎn)。為了給渲染器提供一個(gè)純凈的渲染環(huán)境,渲染器和設(shè)計(jì)器處于不同的 iframe 中,因此拖拽組件,不僅涉及在同一個(gè) iframe 中拖拽組件,還涉及跨 iframe 拖拽組件。渲染器所在的 iframe 由設(shè)計(jì)器喚起,在正式介紹拖拽定位之前,先介紹如何喚起渲染器 iframe。
喚起渲染器 iframe
iframe 元素的常見(jiàn)用法是將它的 src 屬性設(shè)置成一個(gè)固定的網(wǎng)頁(yè)地址,讓它在當(dāng)前網(wǎng)頁(yè)嵌入另一個(gè)已經(jīng)存在的網(wǎng)頁(yè),但渲染器沒(méi)有固定的網(wǎng)頁(yè)地址,所以在這里要使用一種不常見(jiàn)的用法,即調(diào)用 document.write 方法給 iframe 所在的文檔寫(xiě)入它要加載的內(nèi)容。設(shè)計(jì)器喚起渲染器 iframe 的流程如下圖所示:
設(shè)計(jì)器環(huán)境和渲染器環(huán)境通過(guò) host 相互通信,SimulatorRenderer 給 host 提供了一些 API 幫助設(shè)計(jì)器完成交互,設(shè)計(jì)器給 host 提供了一些 API 幫助渲染器完成畫(huà)布渲染。
在設(shè)計(jì)器環(huán)境中與渲染器環(huán)境相關(guān)的只是一個(gè) iframe 元素,如下:
<iframe name="SimulatorRenderer" className="vitis-simulator-frame" style={frameStyle} ref={this.mountContentFrame} />
往 iframe 寫(xiě)入內(nèi)容發(fā)生在 host.mountContentFrame 方法中,代碼片段如下:
this.frameDocument!.open() this.frameDocument!.write( `<!doctype html> <html class="engine-design-mode"> <head> <meta charset="utf-8"/> // 這里是渲染器環(huán)境要加載的css樣式腳本 ${styleTags} </head> <body> // 這里是渲染器環(huán)境要加載的js腳本 ${scriptTags} </body> </html>` ) this.frameDocument!.close() // 監(jiān)聽(tīng)iframe加載成功和加載失敗的事件 this.frameWindow!.addEventListener('load', loaded); this.frameWindow!.addEventListener('error', errored);
用低代碼引擎設(shè)計(jì)界面時(shí),為了讓渲染器環(huán)境能成功的顯示畫(huà)布,上述 scriptTags 中至少包含 react、react-dom 和 vitis-lowcode-simulator-renderer 的js 腳本,在開(kāi)發(fā)階段 vitis-lowcode-simulator-renderer 的 js 腳本地址是 http://localhost:5555/js/simulator-renderer.js,等發(fā)布之后vitis-lowcode-simulator-renderer 的 js 腳本地址是其 npm 包的 js 地址。
拖拽定位
拖拽定位指的是當(dāng)組件在畫(huà)布區(qū)域拖動(dòng)時(shí),界面實(shí)時(shí)的顯示組件最近的可放置位置,這是一個(gè)與設(shè)計(jì)器強(qiáng)相關(guān)的功能,所以與設(shè)計(jì)器處于同一個(gè) iframe,相關(guān)的 DOM 元素被疊放在畫(huà)布區(qū)域的上面,如下圖所示:
上圖藍(lán)線所在的位置就是被拖動(dòng)組件最近可放置的位置,實(shí)現(xiàn)該功能需用到 Element.getBoundingClientRect() 方法和 HTML5 的拖放事件。給渲染器中的低代碼組件設(shè)置 ref 屬性,當(dāng)其裝載到界面上即可得到組件的 DOM 元素,從而計(jì)算出拖拽過(guò)程中鼠標(biāo)經(jīng)過(guò)的低代碼組件。
低代碼組件的拖拽能力由 Dragon 實(shí)例提供,與拖拽相關(guān)的概念有如下3個(gè):
- DragObject:被拖拽的對(duì)象,它是畫(huà)布中的低代碼組件或組件面板上的低代碼組件。
- LocationEvent:攜帶了拖拽過(guò)程中產(chǎn)生的坐標(biāo)信息和被拖拽的對(duì)象。
- DropLocation:被拖拽對(duì)象在畫(huà)布上最近的可放置點(diǎn)。
DragObject 是一個(gè)聯(lián)合類型,拖拽不同位置的低代碼組件,它的類型有所不同,其接口類型定義如下:
interface DragNodeObject { type: DragObjectType.Node; // 被拖拽的是畫(huà)布中的低代碼組件 node: Node; } interface DragNodeDataObject { type: DragObjectType.NodeData; // 被拖拽的是組件面板上的低代碼組件 data: ComponentSpec; } type DragObject = DragNodeObject | DragNodeDataObject
設(shè)計(jì)器用 LocationEvent 來(lái)計(jì)算被拖拽對(duì)象最近的可放置點(diǎn),其接口類型定義如下:
interface LocationEvent { dragObject: DragObject, originalEvent: DragEvent, clientX: number, clientY: number }
上述接口中 clientY 和 clientX 來(lái)自于 DragEvent 對(duì)象,它們用來(lái)計(jì)算畫(huà)布中離鼠標(biāo)最近的 Node。
DropLocation是拖拽操作要計(jì)算的結(jié)果,接口類型定義如下:
interface DropLocation { // 被拖拽對(duì)象可放置的容器 containerNode: Node; // 被拖拽對(duì)象在容器中的插入點(diǎn) index: number; }
以拖拽組件面板中的低代碼組件為例,在畫(huà)布區(qū)域顯示組件最近的可放置點(diǎn),總體而言,需經(jīng)歷6個(gè)步驟。
1. 綁定拖放事件
iframe 和組件面板中的低代碼組件綁定拖放事件,得到 DragObject,代碼片段如下:
// 當(dāng)組件面板中的組件開(kāi)始拖動(dòng)時(shí) <div draggable={true} onDragStart={() => onDragStart(item.packageName)}>xxx</div> const onDragStart = (packageName: string) => { // 得到DragObject dragon.onNodeDataDragStart(packageName) } // 給 iframe 綁定dragover事件,當(dāng)拖動(dòng)操作進(jìn)入畫(huà)布區(qū)域時(shí)觸發(fā)事件 this.frameDocument?.addEventListener('dragover', (e: DragEvent) => { e.preventDefault() this.project.designer.dragon.onDragOver(e) })
2. 獲取拖拽過(guò)程中的 LocationEvent
LocationEvent 將在 iframe 的 dragover 事件處理程序中實(shí)時(shí)獲取,代碼如下:
onDragOver = (e: DragEvent) => { // 獲取 locateEvent 只是簡(jiǎn)單的取值 const locateEvent = this.createLocationEvent(e) } createLocationEvent = (e: DragEvent): LocationEvent => { return { dragObject: this.dragObject, originalEvent: e, clientX: e.clientX, clientY: e.clientY } }
3. 獲取離鼠標(biāo)最近的 Node
Node 被裝載在渲染器環(huán)境中,只有 SimulatorRenderer 實(shí)例才知道每個(gè) Node 的位置,因此這一步需要調(diào)用 SimulatorRenderer 給 host 提供的getClosestNodeIdByLocation 方法,getClosestNodeIdByLocation 的代碼如下:
getClosestNodeIdByLocation = (point: Point): string | undefined => { // 第一步:找出包含 point 的全部 dom 節(jié)點(diǎn) 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 節(jié)點(diǎn) 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 對(duì)象中保存了畫(huà)布上全部低代碼組件的 DOM 節(jié)點(diǎn),實(shí)現(xiàn)這個(gè)目的需借助 React 的 ref 屬性,在這里不展開(kāi)介紹。
4. 獲取拖拽對(duì)象最近的可放置容器
每個(gè)低代碼組件都能設(shè)置嵌套規(guī)則,規(guī)定哪些組件能做為它的子元素和父元素,不符合規(guī)則的組件則不可放置,在這一步將使用組件的嵌套規(guī)則,代碼如下:
getDropContainer = (locateEvent: LocationEvent) => { // 從上一步得來(lái)的潛在容器 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 { // 繼續(xù)往上找父級(jí) containerNode = containerNode.parent } } }
5. 計(jì)算被拖動(dòng)的對(duì)象在容器中的插入點(diǎn)
容器可能包含多個(gè)子元素,在這一步將利用鼠標(biāo)位置計(jì)算被拖動(dòng)的對(duì)象在容器中的插入點(diǎn),得到最終的 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 && clientY > lastChildRect.bottom) { dropLocation.index = childrenSize } else { let minDistance = Infinity // 容器中最近的插入點(diǎn) let minIndex = 0 for (let index = 0 ; index < childrenSize; index ++) { const child = container.getChildAtIndex(index)! const rect = this.designer.getNodeRect(child.id) if (rect && Math.abs(rect.top - clientY) < minDistance) { minDistance = Math.abs(rect.top - clientY) minIndex = index } } dropLocation.index = minIndex } } return dropLocation
6. 在界面上提示最近的插入位置
經(jīng)過(guò)前面的步驟已經(jīng)得到了插入位置,現(xiàn)在需要在界面上給用戶顯示相應(yīng)的提示,這里要用到狀態(tài)管理庫(kù) MobX,在此之前需將 Dragon 實(shí)例變成一個(gè)可觀察對(duì)象,再在React組件中使用 mobx-react 導(dǎo)出的 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 ( // 這個(gè)元素被絕對(duì)定位到畫(huà)布區(qū)域的上面 <div className='vitis-insertion-view' style={style}></div> ) })
當(dāng) dragon.dropLocation 的值發(fā)生變化時(shí),InsertionView 組件將重新渲染,實(shí)時(shí)的給用戶提示拖拽對(duì)象對(duì)接的可插入點(diǎn)。
寫(xiě)在后面
低代碼的拖拽定位遠(yuǎn)不止本文介紹的這些功能,至少還包含懸停探測(cè),詳情可查看開(kāi)源項(xiàng)目。 該開(kāi)源項(xiàng)目持續(xù)更新。
以上就是JS分層架構(gòu)低代碼跨iframe拖拽示例詳解的詳細(xì)內(nèi)容,更多關(guān)于JS分層架構(gòu)跨iframe拖拽的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Electron?網(wǎng)絡(luò)攔截實(shí)戰(zhàn)示例詳解
這篇文章主要為大家介紹了Electron?網(wǎng)絡(luò)攔截實(shí)戰(zhàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03PerformanceObserver自動(dòng)獲取首屏?xí)r間實(shí)現(xiàn)示例
今天給大家介紹一個(gè)非常好用的瀏覽器api:?PerformanceObserver?,?我們可以用它來(lái)獲取首屏、白屏的時(shí)間,就不用再麻煩地手動(dòng)去計(jì)算了2022-07-07微信小程序 藍(lán)牙的實(shí)現(xiàn)實(shí)例代碼
這篇文章主要介紹了微信小程序 藍(lán)牙的實(shí)現(xiàn)實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2017-06-06微信小程序 wx.uploadFile在安卓手機(jī)上面the same task is working問(wèn)題解決
這篇文章主要介紹了微信小程序 wx.uploadFile在安卓手機(jī)上面the same task is working問(wèn)題解決的相關(guān)資料,需要的朋友可以參考下2016-12-12javascript進(jìn)階篇深拷貝實(shí)現(xiàn)的四種方式
這篇文章主要為大家介紹了javascript進(jìn)階篇深拷貝實(shí)現(xiàn)的四種方式詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07微信小程序中頂部導(dǎo)航欄的實(shí)現(xiàn)代碼
這篇文章主要介紹了微信小程序中頂部導(dǎo)航欄的實(shí)現(xiàn)代碼的相關(guān)資料,需要的朋友可以參考下2017-03-03微信小程序 Windows2008 R2服務(wù)器配置TLS1.2方法
微信小程序免費(fèi)SSL證書(shū)https、TLS版本問(wèn)題的解決方案《二十四》request:fail錯(cuò)誤(含https解決方案)(真機(jī)預(yù)覽問(wèn)題把下面的代碼復(fù)制到PowerShell里運(yùn)行一下,然后重啟服務(wù)器。# Enables TLS 1.2 on ...,需要的朋友可以參考下2016-12-12Qiankun Sentry 監(jiān)控異常上報(bào)無(wú)法自動(dòng)區(qū)分項(xiàng)目解決
這篇文章主要為大家介紹了Qiankun Sentry 監(jiān)控異常上報(bào)無(wú)法自動(dòng)區(qū)分項(xiàng)目解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11