IoT Studio視覺化搭建平臺編輯歷史功能的思考與探索
簡介:在前端視覺化搭建領域中“重做”和“撤銷”這兩個功能已經是標配中的標配,畢竟只要有使用者行為的地方就可能會有出錯,這兩個功能無疑就是為使用者提供了“後悔藥”。目前有各種各樣的視覺化搭建平臺,本文介紹IoT Studio視覺化搭建平臺在編輯歷史功能上的設計與思考。
來源 | 阿里技術公眾號
一 背景
在前端視覺化搭建領域中“重做”和“撤銷”這兩個功能已經是標配中的標配,畢竟只要有使用者行為的地方就可能會有出錯,這兩個功能無疑就是為使用者提供了“後悔藥”。目前有各種各樣的視覺化搭建平臺,本文介紹IoT Studio視覺化搭建平臺在編輯歷史功能上的設計與思考。
二 實現思路
1 頁面DSL的維護
在IoT Studio視覺化搭建平臺中,我們通過頁面的抽象語法樹來維護頁面狀態,頁面資訊和元件資訊都記錄在對應節點上:
PageNode: { componentName: 'page1', id: 'page1', props: {}, children: [ ComponentNode: { componentName: 'component1', id: 'component1', props: { width: 800, height: 1000, color: '#ffffff' }, children: [] }, ComponentNode: { componentName: 'component2', id: 'component2', props: {}, children: [] }, ComponentNode: { componentName: 'component3', id: 'component3', props: {}, children: [] }, ] }
2 重做與撤銷
快照法
在每次編輯頁面時,將頁面的資訊進行深拷貝存入歷史記錄中。在進行重做和撤銷時從歷史記錄中取出對應的快照,用快照代替當前頁面狀態,即可完成一次歷史記錄的操作。
在這種方法下,通常使用一個指標來指向當前的頁面狀態。如下圖:
- 實現比較簡單,頁面資訊全量進行深拷貝即可。
- 歷史記錄之間的切換靈活。
- 當頁面資訊很大時,十分佔用儲存空間。
指令法
IoT Studio使用的是這種方法。
我們為每一次操作定義兩個方法:execute與undo,以及將“操作”抽象為Operation。
在execute中執行這次操作的正向操作,在undo中實現逆向操作。
export abstract class Operation<T = void> {
/**
* 逆向操作
*/
protected abstract undo(): T;
/**
* 正向操作
*/
protected abstract execute(): T;
}
每進行一次編輯操作,其實就是建立一次Operation並執行其execute方法,隨後如果需要撤銷就執行其undo方法。
指令法的特點:
- 相對快照法,在頁面配置複雜時,能節省不少儲存空間。
- 不同的Operation其execute和undo邏輯很可能會不一樣,有一定的邏輯開發成本。
- 跨多個歷史記錄的重做或撤銷,需要執行他們之前所有的execute或undo。例如,上圖中如果從O3到O1需要執行2次undo。這一點沒有快照法便利。
3 實現細節
在上文裡提到了IoT Studio使用的是指令法。
Transation
在實際業務開發中,很多場景會涉及到一次性編輯多個元件,即涉及多個Operation例項。於是在Operation基礎上有了Transaction——事務的概念,Transaction下維護了一份Operation例項List,每當有execute或者undo執行時,會遍歷Operation List中的Operation例項執行其execute或undo方法。
雙向連結串列
IoT Studio中的操作歷史是基於雙向連結串列實現的,每個連結串列節點維護一個Transaction例項。連結串列節點末端的execute結果既是最新的操作歷史。
連結串列之前通過forwardCurrent和backforwardCurrent方法進行節點狀態的切換。
Class Manager {
backwardCurrent(): boolean {
if (this._current?.prev) {
this._current.value.operation.undo();
this._current = this._current.prev;
this._validLength -= 1;
return true;
}
return false;
}
forwardCurrent(): boolean {
if (this._current?.next) {
this._current.next.value.operation.execute();
this._current = this._current.next;
this._validLength += 1;
return true;
}
return false;
}
addAfterCurrent(item: OperationResult<any>) {
if (nextNode) {
nextNode.prev = undefined;
this._length = this._validLength;
}
this._current.next = { value, prev: this._current };
this._current = this._current.next;
}
}
每當有新的編輯操作時,會通過addAfterCurrent插入新的節點。
4 總結
Operation是實現重做和撤銷的最小指令例項,通過Operation不同子類實現不同的execute和undo方法,從而實現重做和撤銷的具體邏輯。
Transaction中維護了Operation例項陣列,我們在進行業務邏輯開發中對元件進行屬性設定時是以Transaction例項為單位進行業務邏輯開發。
維護了一個雙向連結串列來對Transaction例項進行管理,從而實現視覺化搭建的操作歷史功能。
三 探索
在實現思路中我們提到了“快照法”和“指令法”,對比兩者的優缺點,不難發現主要矛盾是在體積與維護成本上。那麼有沒有一種辦法能兼顧二者的優點呢?下面兩個工具可以提供一些思路:
immutable.js + 快照法
在JS中物件是引用賦值,在儲存物件時往往會使用深拷貝規避這個問題,但是這樣會造成CPU和記憶體的浪費,這也是快照法的缺點所在。
immutable使用持久化資料結構,在使用舊資料建立新資料的時候,會保證舊資料同時可用且不變,同時為了避免深度複製複製所有節點的帶來的效能損耗,immutable使用了結構共享,即如果物件樹種的一個節點發生變化,只修改這個節點和受他影響的父節點,其他節點則共享。
在實現操作歷史功能時,使用immutable儲存資料,能解決資料複用的問題。immutable.js + 快照法可以組合使用。據我所知公司的@ali/visualengine使用的就是這個方案。
Git
每次我們執行 git add 和 git commit 命令時,Git 所做的工作實質就是將被改寫的檔案儲存為資料物件, 更新暫存區,記錄樹物件。
Git 是如何做到這點的?Git 打包物件時,會查詢命名及大小相近的檔案,並只儲存檔案不同版本之間的差異內容。 你可以檢視包檔案,觀察它是如何節省空間的。
同樣有趣的地方在於,第二個版本完整儲存了檔案內容,而原始的版本反而是以差異方式儲存的——這是因為大部分情況下需要快速訪問檔案的最新版本。最妙之處是你可以隨時重新打包。Git 時常會自動對倉庫進行重新打包以節省空間。當然你也可以隨時手動執行 git gc 命令來這麼做。
本文為阿里雲原創內容,未經允許不得轉載。