低代碼平臺的屬性面板該如何設計?
我們先對整個平臺的設計做一下簡單回顧:
這里是我平時自己維護的一個低代碼平臺,技術棧是Vue。后續的分享也是基于該平臺的一些具體實現細節展開
和市面上大部分可視化搭建系統基本類似。左側對應組件區域,中間是畫布區域,右側是屬性區域。
大致操作流程就是拖動左側的組件到中間的畫布,選中組件,右側屬性面板就會展示與該組件關聯的屬性。編輯右側屬性,畫布中對應的組件樣式就會同步更新。頁面拼接完成,可通過預覽按鈕進行頁面預覽。預覽無誤,即可通過發布按鈕進行活動的發布。
當然其中也有撤銷、重做等操作。
今天我們來探討的是選中畫布中指定組件,右側屬性面板展示與該組件關聯的表單,修改右側表單,畫布中的組件樣式會同步更新。
首先來看一下編輯器全局的數據結構:
const editorModule = {
state: {
components: [],
currentElement: "",
},
mutations: {
addComponentToEditor(state, component) {
component.id = uuidv4();
state.components.push(component);
},
setActive(state, id) {
state.currentElement = id;
},
updateComponent(state, { id, key, value, isProps }) {
const updatedComponent = state.components.find(
(component) => component.id === (id || state.currentElement)
);
if (updatedComponent) {
if (isProps) {
updatedComponent.props[key] = value;
} else {
updatedComponent[key] = value;
}
}
},
},
getters: {
getCurrentElement: (state) => {
return state.components.find(
(component) => component.id === state.currentElement
);
},
}
}
editor中存儲了components(所有組件數據)和currentElement(當前選中的組件信息)。
當點擊左側業務組件,會觸發業務組件的點擊事件,進而觸發addComponentToEditor,向editor store的components添加一條組件。我們這里添加一個普通的文本組件,然后看下他的初始props:
{
actionType: "",
backgroundColor: "",
borderColor: "#000",
borderRadius: "0",
borderStyle: "none",
borderWidth: "0",
boxShadow: "0 0 0 #000000",
color: "#000000",
fontFamily: "",
fontSize: "14px",
fontStyle: "normal",
fontWeight: "normal",
height: "36px",
left: "97.5px",
lineHeight: "1",
opacity: 1,
paddingBottom: "0px",
paddingLeft: "0px",
paddingRight: "0px",
paddingTop: "0px",
position: "absolute",
right: "0",
tag: "p",
text: "正文內容",
textAlign: "center",
textDecoration: "none",
top: "232px",
url: "",
width: "125px"
}
當在畫布中選中該文本組件時,就會觸發setActive,更新currentElement。(通過getCurrentElement可以獲取到當前正在被操作的組件)。
這個時候,應該如何添加屬性和表單的基礎對應關系呢?
這個也是本篇文章的主題:低代碼平臺的屬性面板該如何設計?
1.屬性面板應該包含哪些內容?
我們的Choba Lego平臺中有很多業務組件,而每個富交互的頁面都是由這些業務組件堆積拼裝而成,而每個組件都包含了一些通用屬性和組件特有屬性,這些屬性反映了當前組件的各種狀態,非常復雜。
對于單獨的組件來說,屬性面板應該是語義化的,無論是開發還是非開發同學,通過屬性面板的操作區,就可以直觀的知道一個組件的屬性是什么,應該如何使用和編輯。
那么屬性面板應該包含哪些內容呢?
- label:屬性名稱。這個可以顯式的告訴具體的屬性的作用,比如元素的寬高、邊框、背景顏色等。
- description:屬性的描述信息。對于一些特殊屬性,可能第一下通過label并不能直觀的識別屬性的含義,添加描述信息可以進行詳細的闡述。
- content:屬性渲染器。用戶可以基于此實現對屬性的修改。最常見的有 textarea、input、select 等。
- error:屬性校驗信息。當用戶輸入了不合法的或者類型不匹配時,可給予適當的錯誤提示信息。
通過以上描述,我們會發現,這其實就是我們常用的表單。
2.屬性和組件的映射關系
其實上面的四塊內容,內容渲染器應該是最復雜的。采用合適的渲染器來渲染對應的屬性才是最重要的。
但存在一些場景,一些屬性可以被多種渲染器來渲染,像字體大小-fontSize,既可以用input-number,又可以用slider。那么這種場景應該如何選用最合適的渲染器呢?其實這種我覺得完全可以看開發者和使用者的綜合意愿,沒有絕對的對錯之分。
對應上面組件的props信息,我們可以對這些屬性做一些歸類,那歸類的標準又是什么呢?我認為應該把屬性與js中的數據類型做一下映射,然后在具體的分類下選用合適的渲染器。
我們知道在JavaScript中,一共有七種數據類型,字符串(String)、數字(Number)、布爾(Boolean)、空(Null)、未定義(Undefined)、Symbol和對象(Object)。其中對象類型包括:數組(Array)、函數(Function)、還有兩個特殊的對象:正則(RegExp)和日期(Date)。
這里面的空(Null)、未定義(Undefined)、Symbol和正則(RegExp)在渲染器中基本用不到。
我們先來看一下字符串(String)、數字(Number)、布爾(Boolean)和日期(Date)可能渲染的方式:
字符串(String)
渲染器類型 | 組件 |
input | |
textarea |
數字(Number)
渲染器類型 | 組件 |
input-number | |
slider |
布爾(Boolean)
渲染器類型 | 組件 |
switch |
日期(Date)
渲染器類型 | 組件 |
date |
除了這幾種,還有對象(Object)、數組(Array)、函數(Function)。
對象和數組屬于較復雜的類型,不過我們可以把它抽象為多層級(可以理解為嵌套)的基礎數據類型:
渲染器類型 | 組件 |
array |
像數組一般是用下拉框的形式來展現。
至于函數(Function),可以采用預定義的形式:
渲染器類型 | 組件 |
function |
到這里,不難想到,我們要維護一個屬性和表單組件的對應關系。屬性對應上面的key,像borderColor、text、width、fontFamily這些,那組件呢?組件其實就是對屬性的具體呈現,像width可以用數字輸入框、text可以用普通輸入框,但是對于一些比較復雜的特性,我們自己去實現這些組件,就顯得捉襟見肘了,這個時候我們就可以考慮和現有的組件庫做一下結合了(這里我采用的是Ant Design Vue)。
那么這樣,屬性prop和component基礎的對應關系就有了:
const mapPropsToComponents = {
text: {
component: "a-input",
},
width: {
component: "a-input-number",
},
borderWidth: {
component: "a-slider",
},
// ...
}
但這只是滿足了常規的基礎組件設計,像一些獨有的屬性或者基礎組件不能滿足的情況,我們需要對其做一定擴展:
渲染器類型 | 組件 |
upload | |
color-picker |
上面提到的上傳組件和顏色選擇組件是需要我們單獨去實現的。
3.屬性分類
僅僅有屬性和組件的對應關系還不夠,每個組件都會對應大量的表單屬性,對他們按功能做一下歸類還是很有必要的。
基本屬性也就是每個組件獨有的一些屬性,除基礎屬性以外,剩余的就是所有組件的通用屬性了。
屬性分類雖然是一個比較簡單的實現,但是能對使用者帶來很大的收益,可以清晰的知道每種屬性更改對組件帶來的不同影響。
4.更新表單將數據更新到屬性
有了上面的準備,最重要的一步來了,那就是選中組件,屬性面板展示該組件關聯的表單屬性,修改屬性,組件數據會同步更新。
以我以往的經驗來看:表單組件在設計時,有兩點是必須的:
- 表單初始值(默認value),供初始展示使用
- 表單屬性更改的事件(默認為 change)
對于不同的表單,初始值和屬性更改后,參數的處理是不一樣的:
- 像高度、寬度這種數字類型的,傳入表單時應保證是number(24)類型,屬性更改后,事件參數應該是string(24px)類型的
- 字體加粗與否、傾斜與否、加下劃線與否,傳入表單時應保證是boolean(true/false)類型,屬性更改后,事件參數應該是string(bold/normal)類型的
所以給每一個屬性在傳入表單和事件更改后都要加一個額外的轉化函數去處理值:
- initialValueConvert
- eventChangeValueConvert
還有對屬性進行賦值時,不是所有的表單控件接收的都是value,像checkbox就是checked,這種單獨抽一個屬性valueProp去控制即可。
其次,像上面提到的父子層級的渲染,除了component還要多加一個subComponent。
上面配置完成后,屬性和組件的對應關系就有了:
const mapPropsToComponents = {
width: {
component: "a-input-number",
eventName: "change",
valueProp: "value",
initialValueConvert: (v) => (v ? parseInt(v) : ""),
eventChangeValueConvert: (e) => (e ? `${e}px` : ""),
text: "寬度",
},
textAlign: {
component: "a-radio-group",
subComponent: "a-radio-button",
eventName: "change",
valueProp: "value",
eventChangeValueConvert: (e) => e.target.value,
text: "對齊",
options: [
{ value: "left", text: "左" },
{ value: "center", text: "中" },
{ value: "right", text: "右" },
],
},
// ...
}
我們的數據始終保持自上而下的順序,也就是說表單更新最終要反射回到總體的 store 當中去。這個時候我們在對應的組件當中發射出一個事件(change),當 change 發生的時候,我們能夠知道是哪個元素的哪個屬性,以及新的值是什么,我們就用這些信息更新這個值,這樣 store完成更新,元素的 props 發生更新,那么整個數據流動就完成了。
5.參考鏈接
https://mp.weixin.qq.com/s/u2AkeXiL0pi4799ccjR_Tg