所見即所得低代碼編輯器畫布實(shí)現(xiàn)(所見即所得編程語言)
1
前言
在開發(fā)或使用低代碼產(chǎn)品時(shí),我們總會想到一個(gè)功能,就是預(yù)覽,用來確認(rèn)編輯的內(nèi)容是否符合預(yù)期。
市面上的編輯器在預(yù)覽方面有多種實(shí)現(xiàn)方式:
- 打開一個(gè)新的窗口,進(jìn)行預(yù)覽
- 直接在編輯區(qū)所見即所得
如果是用戶,當(dāng)然是選擇第2種,最直觀,最符合預(yù)期。
如果是開發(fā)者就會知道,之所以有這樣多的可能性,是因?yàn)樗娂此梅浅R蕾噷?shí)際生產(chǎn)的運(yùn)行環(huán)境,第1種單獨(dú)打開窗口的實(shí)現(xiàn)成本比較低。
而有的編輯器雖然聲稱是所見即所得,但實(shí)際操作上,只是有相似度的所見即所得,因?yàn)閷?shí)際運(yùn)行環(huán)境和生產(chǎn)有較大的差距,仍然比較容易產(chǎn)生預(yù)期外的問題。
在我們團(tuán)隊(duì)的珊瑚海項(xiàng)目中,就實(shí)現(xiàn)了一個(gè)可以做到完全所見即所得的低代碼編輯器,如果想了解該項(xiàng)目概況,可以訪問下我們過往的兩篇基本介紹文章。
- 《設(shè)計(jì)無侵入性的低代碼編輯器》
- 《珊瑚??缍私鉀Q方案及在移動端的布局動態(tài)化實(shí)踐》
下面我就來介紹下,我們是如何實(shí)現(xiàn)的所見即所得低代碼編輯畫布。
2
方案設(shè)計(jì)
在描述具體實(shí)現(xiàn)之前,先聲明一些前提:
- 本文描述的編輯器的運(yùn)行容器都是瀏覽器,因此所見即所得的主要是瀏覽器的頁面
- 本文所描述的所見即所得的“得”,主要指和生產(chǎn)環(huán)境最終產(chǎn)物一致的效果,而不是高保真Demo
2.1 通常的畫布實(shí)現(xiàn)
低代碼編輯器中,通常是將組件的實(shí)際內(nèi)容拖拽到編輯器的畫布中,然后展示出一個(gè)頁面的布局情況。
如果想對畫布中的元素進(jìn)行編輯和修改,首先要點(diǎn)擊一下畫布中的元素,會出現(xiàn)對應(yīng)元素的屬性配置,修改這些配置項(xiàng),并將修改后的效果展示在畫布中,就完成了一個(gè)基本的低代碼編輯的操作。
這套基本的操作,在時(shí)下主流的編輯器中都是將拖拽的元素、選擇元素的事件、選框都寫在了一個(gè)文檔流中,而這樣的設(shè)計(jì)就讓畫布的內(nèi)容包含了編輯器的樣式和腳本,而生產(chǎn)環(huán)境并沒有這些內(nèi)容,這就導(dǎo)致在這樣的設(shè)計(jì)下,編輯器中的內(nèi)容,必然無法完全做到所見即所得。
2.2 通常的預(yù)覽實(shí)現(xiàn)
基于對編輯器畫布無法完全做到所見即所得的設(shè)計(jì),也就衍生了通過彈出一個(gè)單獨(dú)的容器(可能是一個(gè)沙盒窗口、也可能是新建一個(gè)頁面)的方式來完成生產(chǎn)環(huán)境頁面的預(yù)覽,這樣是可以達(dá)到預(yù)覽生產(chǎn)環(huán)境頁面的目的。
但是這就讓編輯和對產(chǎn)物的預(yù)期產(chǎn)生了一定的割裂,一邊編輯,還要一邊去打開一個(gè)額外的容器去看效果,顯得比較笨拙。
2.3 讓預(yù)覽作為編輯環(huán)節(jié)的一部分
由上面的鋪墊我們不難想到,如果我的畫布上編輯的內(nèi)容就是預(yù)覽的內(nèi)容,不就能做到直接編輯生產(chǎn)環(huán)境的內(nèi)容了么。
那這個(gè)預(yù)覽的內(nèi)容放在哪里呢?通過畫中畫的方式根據(jù)修改實(shí)時(shí)刷新,這或許不錯(cuò),但是在優(yōu)先的窗口下,預(yù)覽要看的清楚就會有些占地方。
既然畫布描繪的就是期望和生產(chǎn)一樣,那不如把這個(gè)預(yù)覽的窗口墊在畫布下面,甚至作為畫布的一部分是不是就能達(dá)到目的了呢?
像上圖一樣,我們將預(yù)覽的容器放在畫布區(qū)域下面,每當(dāng)編輯器中的頁面產(chǎn)生變化,我們就重新渲染一下容器,這樣就可以達(dá)到我們在頁面中所見即所得的目的了,酷!
但是實(shí)際操作的話我們就會發(fā)現(xiàn),原有畫布和預(yù)覽容器都有內(nèi)容的展示,這會導(dǎo)致上下層級視覺上的沖突,而且因?yàn)閮蓚€(gè)容器的運(yùn)行環(huán)境不同,也導(dǎo)致了內(nèi)容無法一一對齊,很容易就產(chǎn)生了錯(cuò)位,不過既然整體思路有了,下面我們就來解決問題。
3
編輯層和渲染層
我們來整理一下將預(yù)覽容器放在畫布下顯而易見的問題:
- 組件的展示會產(chǎn)生重疊
- 兩層內(nèi)容會產(chǎn)生錯(cuò)位
針對這兩個(gè)問題,我設(shè)計(jì)了一套多層結(jié)構(gòu)畫布,即由編輯層和渲染層組成的畫布。
其中渲染層就是預(yù)覽的頁面,在這一層我們不會做什么工作,因?yàn)橐坏┳隽耸裁淳蜁绊懮a(chǎn)版本的還原度。
也因此,渲染層的容器我選擇了iframe,用iframe可以較完美的隔絕編輯器和渲染層的上下文,不會受到意外的干擾;同時(shí)也可以在操作彈窗這種組件的時(shí)候,讓覆蓋整個(gè)頁面的蒙版更可控。
而編輯層之于原來的畫布,我們也做了一些改變,最大的區(qū)別就是編輯層不再感知當(dāng)前拖拽的組件實(shí)體,而是通過一個(gè)通用的占位組件結(jié)合當(dāng)前要操作的實(shí)際組件信息,組織出一個(gè)觸發(fā)器,以透明的方式遮蓋在渲染層對應(yīng)的組件上方。
上圖是一個(gè)實(shí)際用戶從操作編輯層,到看到編輯結(jié)果的流程。通過這樣的設(shè)計(jì),用戶操作編輯器的時(shí)候,看到的就是原來的預(yù)覽容器,也就是渲染層展示的內(nèi)容。
而用戶去點(diǎn)擊渲染層組件的時(shí)候,其實(shí)點(diǎn)擊到的是覆蓋在渲染層上面編輯層中的觸發(fā)器,然后根據(jù)觸發(fā)器綁定的組件信息,進(jìn)行實(shí)際的編輯。
在編輯過后,通過將編輯器的產(chǎn)物給到渲染層做重新的渲染,就有可能達(dá)到百分百的所見即所得的效果了,且解決了組件間重疊的問題。
用戶在編輯時(shí)想要點(diǎn)擊一個(gè)畫布中的組件,如何能讓系統(tǒng)正確地識別到用戶點(diǎn)擊的這個(gè)組件呢?這就要先解決兩個(gè)層級中組件錯(cuò)位的問題了。
4
編輯層和渲染層組建的布局同步
為了能讓編輯層的觸發(fā)器和渲染層的實(shí)際組件布局同步,我首先嘗試的方案是通過收集渲染層組件渲染后的實(shí)際展示大小,讓編輯層的觸發(fā)器繼承對應(yīng)組件的這個(gè)大小然后根據(jù)所設(shè)置的默認(rèn)信息,自然形成編輯層的布局狀態(tài),來達(dá)到一一對齊。
這里“自然形成”的意思是指如果一個(gè)組件在渲染層用的是flex那編輯層對應(yīng)的觸發(fā)框也用flex,而收集組件大小是因?yàn)?,編輯層沒有實(shí)際的內(nèi)容,如文本組件里面會因?yàn)槲淖謨?nèi)容的不同,大小不同。
而這種“自然形成”的策略在實(shí)踐中很快就出現(xiàn)了問題:
- 受到全局屬性的影響,如box-sizing在編輯層和渲染層的定義不一樣會導(dǎo)致渲染布局的整體邏輯都不同
- 一些組件自定義的樣式編輯層感知不到,對于這種黑盒的場景無法完全覆蓋到
尤其是組件樣式的問題,導(dǎo)致了這個(gè)方案直接被否定,因此我們轉(zhuǎn)向了另一個(gè)思路,獲取組件最外層的所有布局信息傳遞給編輯層。
我在設(shè)計(jì)這套DSL給定每個(gè)節(jié)點(diǎn)一個(gè)id,在這里我們可以通過將節(jié)點(diǎn)id附著在渲染組件的DOM上。
再通過DOM的getBoundingClientRect方法獲取DOMRect對象,再結(jié)合組件上的overflow屬性,我們就可以較精確的定位到組件在渲染層的實(shí)際位置和大小。
因?yàn)殇秩緦佑玫氖莍frame,我們在編輯器是可以獲取到渲染層的上下文的,因此通過前面的方案我可以輕松獲取到所有組件的布局信息,并和編輯層一一對應(yīng)。
5
實(shí)際應(yīng)用
咱們來看下實(shí)際的效果
上圖是編輯器中的實(shí)際效果,我們可以看到頁面就是生產(chǎn)一樣的頁面,點(diǎn)擊內(nèi)容會觸發(fā)選框。
為什么有多個(gè)選框?這是使用了數(shù)據(jù)綁定和根據(jù)數(shù)據(jù)循環(huán)渲染節(jié)點(diǎn)的效果,后面的文章我們會對實(shí)現(xiàn)進(jìn)行展開,本文不做展開。
我們換個(gè)圖來看下這個(gè)頁面。
在chrome的devTools中l(wèi)ayers模式下我們可以更直觀地看出整個(gè)分層的關(guān)系,每一個(gè)觸發(fā)器都清晰可見。
看到這個(gè)圖大家應(yīng)該可以更直觀地理解我們的這個(gè)方案操作方式。
6
好處不止所見即所得
通過編輯層和渲染層配合得到的所見即所得的設(shè)計(jì),我們可以再發(fā)散一下思維,看看在這樣的架構(gòu)下還能做些什么。
低代碼編輯器跨技術(shù)棧的實(shí)現(xiàn):渲染層的隔離,也就是組件運(yùn)行環(huán)境的隔離。稍加操作,我們完全可以讓渲染層以外的內(nèi)容不用感知組件的實(shí)現(xiàn),如通過聲明的方式導(dǎo)入組件。
通過這樣的設(shè)計(jì)就能做到編輯器和渲染層中的組件用不同的技術(shù)棧,就能做到react開發(fā)的編輯器,拖拽vue開發(fā)的組件庫,這能讓編輯器可復(fù)用性更好,也能更加地減少開發(fā)資源。
線上頁面調(diào)試工具:我們也可以換個(gè)思維,做一些工具,如chrome的擴(kuò)展工具,在系統(tǒng)產(chǎn)生的頁面上像收集布局信息那樣,直接植入編輯器或者做一些調(diào)試工作。
在這樣靈活的低代碼編輯器設(shè)計(jì)下,你也可以發(fā)揮想象力,做更多更酷的事情。
7
并不完美
這個(gè)設(shè)計(jì)可以讓我們的低代碼編輯器復(fù)用,且更具擴(kuò)展性,但也有一定的不足:
- 知識成本:雖然能做到完美還原生產(chǎn)環(huán)境的狀態(tài),但是是否能真的還原依賴組件庫開發(fā)者的建設(shè),比如生產(chǎn)環(huán)境所有依賴的腳本和樣式,即使和組件庫并無直接關(guān)系。
- 動態(tài)頁面的解決方案:如果是純靜態(tài)的頁面,這個(gè)設(shè)計(jì)很好理解,但如果是要調(diào)用接口呢,整個(gè)頁面是動態(tài)的呢,要怎么做到所見即所得,這里就需要編輯器綁定數(shù)據(jù)的建設(shè),而這里我也做了設(shè)計(jì)和實(shí)踐,將在后面的文章中說明。
- 對圖文混排支持不夠:還有就是圖文混排的場景,選框不夠精準(zhǔn),因?yàn)樾袃?nèi)片段可能是個(gè)多邊形,這個(gè)因?yàn)閳鼍氨容^少,我還沒有想到好的方案,希望有想法的同學(xué)可以留言給我。
- 渲染次數(shù)過多導(dǎo)致性能問題:再有就是這樣的方案會導(dǎo)致編輯層和渲染層的內(nèi)容重新渲染次數(shù)過多,比較消耗瀏覽器的性能。這里在后面會考慮根據(jù)場景策略加一些變更的監(jiān)聽,來減少一些渲染的回流,不過這就是后話了。
8
總結(jié)
隨著前端技術(shù)的覆蓋面越來越廣,低代碼作為前端頁面搭建的一個(gè)提效選擇,幾乎成為了前端團(tuán)隊(duì)必須的技術(shù)沉淀,為了能讓低代碼系統(tǒng)本身的開發(fā)成本越來越低,大家也在做各種各樣的嘗試,編輯層和渲染層的分離也是在這個(gè)大背景下的產(chǎn)物。
當(dāng)前阿里的低代碼引擎和騰訊的魔方平臺也都使用了類似的方案。在開源的內(nèi)容中,為了讓畫布層更可控,兩個(gè)大廠都不約而同的選擇了定制化的畫布,而我在這里給出的方案是增加了一些開發(fā)的知識點(diǎn),但是更加開放,把掌控權(quán)給到了開發(fā)者。
另外就像前面說的,我還考慮了數(shù)據(jù)綁定還有跨技術(shù)棧相關(guān)的場景,后續(xù)的文章中將會更新相關(guān)的內(nèi)容。
作者:高飛宇
來源:微信公眾號:58技術(shù)
出處:https://mp.weixin.qq.com/s/bfZraIWD7REeCvV27iCKEw