通用 Form API 協議 - 基礎版
highlight: vs2015
背景
筆者的目標非常明確,就是「提高中後臺系統的開發效率」,目前經歷了 3 個階段(背景有點長,但是很重要):
第一階段,由於業務需要,先著手進行了 開箱即用的工具 - BI 的調研,最終得到的結論是首選 DataEase,次選 Metabase。但是 BI 只是中後臺系統的一部分,這點成果還遠遠不夠,於是進入了第二階段。
第二階段,進行了 低程式碼漫談 系列調研,希望能夠通過現有的低程式碼平臺大幅提升開發效率。但是,整體引入一個低程式碼平臺,對於現有的開發流程來說不太現實,最大的問題就是現有的專案程式碼不好處理。筆者之前有一段低程式碼產品的開發經歷,所以還是有一定認知深度的,於是開始進行更深一步的研究,希望能夠從更抽象的底層尋求更合適的解決方案。
研究發現,百度的 amis 和阿里的 lowcode-engine 文件非常完善,後者甚至出了一本白皮書。在細心研習了一番之後有了比較大的收穫和啟發(amis 核心概念淺析、lowcode-engine 協議淺析),筆者意識到:
低程式碼產品最重要的技術核心是協議。
如何理解這句話呢?表面看起來,低程式碼產品是通過視覺化拖拽操作生成 APP。這個過程中最核心的一步就是:拖拽畫布輸出的產物(資料),輸入到 APP 生成器,最終生成 APP。這個「產物」非常重要,通常需要網路傳輸,所以都會選擇 JSON 格式,而這個 JSON 攜帶並表達了整個 APP 的資訊。
大家知道,一個 APP 是非常複雜的,科學合理的設計好這個 JSON 是非常難的,而這個 JSON 的格式就是所謂的「協議」。為了更直觀,我們舉一個 lowcode-engine 的 Demo 例子: ```jsx import ReactRenderer from '@ali/lowcode-react-renderer'; import ReactDOM from 'react-dom'; import { Button } from '@alifd/next';
/ 符合協議格式的 schema / const schema = { componentName: 'Page', props: {}, children: [ { componentName: 'Button', props: { type: 'primary', style: { color: '#2077ff' }, }, children: '確定', }, ], };
const components = { Button, };
/ 傳入 ReactRenderer 就能渲染出 APP /
ReactDOM.render((
``
在 Demo 中,只需要傳入一個樹狀結構(符合協議)的 schema 和一個元件列表,
ReactRender` 就可以將整個 APP 渲染出來,供人使用。
有了以上認知,一個解決思路就出現了: 1. 首先,協議是語言無關的,所以無論前端專案用的 React、Vue 甚至後端直出的模板,都可以應用; 2. 再者,協議的實現可以用多種方式提效,比如低程式碼產品使用的方式是 可拖拽畫布。說到底,協議就是一個大 JSON,我們還可以用表單、程式碼片段甚至外掛來高效的生成它。 但是!上來就搞這麼大的動作幾乎是不可能成功的。所以理論要想落地,還是要有一定規劃和里程碑的,通常不變形地落地才是工程的最大難點,於是來到了第三階段。
第三階段,也就是現階段。路要一步一步走,飯要一口一口吃。經過慎重思考,筆者決定要走的第一步,就是先提升表單開發的效率。表單和列表佔據了中後臺系統絕大部分的內容,所以如果提升了表單的開發效率,實際上對於整體開發效率的提升還是有一定效果的。於是筆者進行了 表單狀態管理 的調研,分析了現在主流表單狀態管理框架的設計思路後,結合低程式碼協議的思路,整理成了本文。旨在制定一個 框架無關的、未來可以方便的擴充套件成低程式碼協議 的表單協議,來指導表單元件的封裝,從而全範圍提效。
目標
- 統一所有前端技術棧(React、Vue、甚至 RN)下的表單開發方式,只需要編寫符合協議的 JS Object 配置,傳入封裝好的表單元件即可,即配置化;
- 制定出表單元件的 API 以及具體格式,以承接符合協議的配置,指導元件的封裝;
正文
分析
筆者在《Form 元件 API 對比 - AntD vs Element vs Naive》中總結到:
- 元件分 2 級:Form 和 Field(或 Item),二者的 API 分類比較類似;
- API 分 2 類:UI 樣式類和功能類;
- UI 樣式類,基本上處理好 layout、label、validateMessage 三方面就夠了,其它複雜的佈局,就交給自定義元件來兜底;
- 功能類,基本上都可以歸到表單狀態管理的範疇裡。如果用過 React Hook Form、Formik、Final Form 之類的表單狀態管理工具,就會發現幾乎全部的功能類 API 都能對上;
UI 類:因為筆者的目的是配置化,那麼配置資訊的易讀性就更加重要,所以筆者會盡量將同一類 API,集合到一個物件中。比如 label-width、label-align 等,都聚合到 label 物件當中,大概如下:
ts
interface FormLabel {
suffix?: string; // ":"
visible?: boolean; // true
placement?: "left" | "top"; // "left"
requiredMark?: "left" | "right" | "hidden"; // "right"
style?: CSSProperties;
}
功能類:理論上參考表單狀態管理工具的 API 就行,再細一點就是 useForm
、useField
之類的入參。但是,調研的三個工具 API 的設計還是有比較大的區別的,所以筆者會在設計時取交集,作為最重要的 API,其他的會視情況進行取捨和聚合。
Form Props
| 引數 | 說明 | 型別 | 預設值 |
| ---- | ---- | ---- | ------ |
| initialValues | 表單預設值,只有初始化以及重置時生效 | object | - |
| onSubmit | 提交表單的回撥事件 | function(values)
| - |
| validateTriggers | 統一設定欄位觸發驗證的時機 | Array\<"onChange" \| "onBlur"> | [ "onChange" ] |
| formEvents | 除 onSubmit
外的其他回撥事件,詳見下文 | FormEvents
| - |
| labelOptions | label
樣式相關配置,詳見下文 | LabelOptions
| - |
| layoutOptions | 佈局相關配置,詳見下文 | FormLayout
| - |
| validateMessageOptions | 驗證提示相關配置,詳見下文 | ValidateMessageOptions
| - |
FormEvents
| 引數 | 說明 | 型別 | 預設值 | | ---- | ---- | ---- | ------ | | onValuesChange | 欄位值更新時觸發回撥事件 | function(changedValues, allValues) | - | | beforeValidate | 觸發校驗之前的回撥事件,返回 false 則停止後續邏輯 | function(values): boolean | - | | afterValidate | 觸發校驗之後的回撥事件 | function(values) | - | | beforeSubmit | 表單提交之前的回撥事件,返回 false 則停止後續邏輯 | function(values): boolean | - | | afterSubmit | 表單提交之後的回撥事件 | function(values) | - |
LabelOptions
| 引數 | 說明 | 型別 | 預設值 |
| ---- | ---- | ---- | ------ |
| visible | label 標籤是否可見 | boolean | true |
| suffix | label 標籤字尾 | string | ":" |
| placement | label 標籤位置 | "left" \| "top" | "left" |
| requiredMark | 表示「必選」的 * 位置 | "left" \| "right" \| "hidden" | "right" |
| style | label 標籤的樣式 | CSSProperties
| - |
FormLayout
| 引數 | 說明 | 型別 | 預設值 |
| ---- | ---- | ---- | ------ |
| align | 垂直對齊方式 | "top" \| "middle" \| "bottom" | "top" |
| gutter | 柵格間隔,單位 px
| number | 0 |
| justify | 水平排列方式 | "start" \| "end" \| "center" \| "space-around" \| "space-between" \| "space-evenly" | "start" |
| offset | 柵格左側的間隔格數,間隔內不可以有柵格 | number | 0 |
| pull | 柵格向左移動格數 | number | 0 |
| push | 柵格向右移動格數 | number | 0 |
| size | 元件尺寸 | "mini" \| "small" \| "medium" \| "large" | "medium" |
| span | 柵格佔位格數 | number | 24 |
| wrap | 是否自動換行 | boolean | true |
ValidateMessageOptions
| 引數 | 說明 | 型別 | 預設值 |
| ---- | ---- | ---- | ------ |
| visible | 驗證提示是否可見 | boolean | true |
| placement | 驗證提示位置 | "right" \| "bottom" | "bottom" |
| validateFirst | 只顯示第一條驗證提示 | boolean | false |
| style | validate message 的樣式 | CSSProperties
| - |
Field Props
| 引數 | 說明 | 型別 | 預設值 |
| ---- | ---- | ---- | ------ |
| name | 欄位標識,具有唯一性 | string | - |
| rules | 校驗規則 | Array<Rule> | - |
| validateTriggers | 統一設定欄位觸發驗證的時機 | Array<"onChange" \| "onBlur"> | ["onChange"] |
| labelOptions | label
樣式相關配置 | LabelOptions
| - |
| layoutOptions | 佈局相關配置 | Omit<FormLayout, "gutter" \| "wrap">
| - |
| validateMessageOptions | 驗證提示相關配置 | Omit<ValidateMessageOptions, "validateFirst">
| - |
| trigger | 設定收集欄位值變更的時機 | string | "onChange" |
| valueOptions | 對於 value
的預處理,詳見下文 | ValueOptions
| - |
| fieldEvents | 表單域其他回撥事件 | FieldEvents
| - |
| dependences | | string[] | - |
ValueOptions
| 引數 | 說明 | 型別 | 預設值 |
| ---- | ---- | ---- | ------ |
| formatOutput | 對元件輸出後的 value
進行處理 | function(value): any | - |
| parseInput | 傳入元件之前,對 value
的預處理邏輯 | function(value): any | - |
| propName | 子節點的值的屬性,如 Switch 的是 "checked" | string | "value" |
FieldEvents
| 引數 | 說明 | 型別 | 預設值 | | ---- | ---- | ---- | ------ | | before/afterChange | change 之前/後的回撥函式 | function(value, values) | - | | before/afterBlur | blur 之前/後的回撥函式 | function(value, values) | - | | before/afterFocus | focus 之前/後的回撥函式 | function(value, values) | - | | before/afterSubmit | submit 之前/後的回撥函式 | function(value, values) | - |
協議
注意,因為 Form 也有可能作為 Field,即子表單的情況,所以協議必須是一個「完全遞迴」的結構,參考 lowcode-engine 的協議 結構,定義如下:
```ts
interface FieldSchema {
componentName: string;
props: {
fieldProps?: FieldProps,
// ...other private props
};
children?: Array
// example: const schema = { componentName: "FormRender", props: { labelOptions: { suffix: "", placement: "top", }, onSubmit(values) { console.log(values); }, }, children: [ { componentName: "Input", props: { fieldProps: { name: "username", rules: [{ required: true }, { min: 5 }], }, }, }, { componentName: "Input", props: { fieldProps: { name: "password", rules: [{ min: 8 }, { pattern: /[0-9a-zA-Z]{0,8}/ }], }, type: "password", placeholder: "Please input set a password", }, }, ], };
``
所以,只要實現
FormRender` 元件,能夠遞迴動態解析 schema 即可。這似乎不是特別難,只是細節會多一些,有了協議在,只需要按照協議來實現就行了。
但是!請注意!這裡結束還很遠,甚至只是剛剛開始!我為什麼這麼說呢?
完整版協議預告
我們來看一個例子:
初看佈局特別複雜,這個問題其實比較好解決,上述的協議已經可以靠
layoutOptions
的配置來解決了。實際上最難處理的是一些非 Field 的自定義元件,比如 Sub Title,Notice Icon,甚至還有一個 Tabs 元件。這些元件都是不需要 fieldProps
屬性的,相當於表單狀態管理不關心的元件。它們要怎麼用協議表示?
另外,還可能有展開/收起的模組。我們當然可以自定義一個這樣的「容器元件」,但是無疑成本會比較大。而且還有不可窮盡的其他「小互動」需求,這些都涉及到了區域性狀態(比如 state.expand)管理,這個區域性狀態實際上與表單狀態也是無關的。這個功能協議怎麼表示?
我們稍微抽象一下,其實 Form 可以看成一個小型的頁面,理論上裡面有可能出現任何佈局和元素,不僅僅只有 Field 元素。這意味著如果想要實現完美的 Form 配置化,其復難度與實現頁面的配置化差不多了,而實現了頁面配置化也基本等於實現了低程式碼了,看來事情比筆者最初想象的要複雜啊。
不過既然涉及到頁面配置化了,那麼 lowcode-engine 的協議 也就派上用場了,由於內容過於複雜,所以會另起一篇。
總結
表單之所以複雜,主要取決於兩個維度:UI 和狀態管理。前者參考業內目前比較流行的 UI 框架,後者參考業內比較流行的表單狀態管理框架,再考慮到易用性,就產生了本文的協議。
另外,由於暫時不需要跨端或進行網路傳輸,所以協議採用了 JS Object
的語法表示,可以承載表示式、函式甚至元件等資訊。如果用 JSON
實現這些,協議的結構會複雜很多。不過可以保證的是,即使有一天需要把現在的協議轉化成 JSON,也是資訊完備的,用 AST 的工具就可以完成自動轉換。
最後,正如結尾的預告說的。本文實際上只完成了「表單配置化協議」的基礎部分,只適用於非常簡單的表單場景,雖然估計這些簡單場景已經佔到了實際情況的 60% 以上,但終究是不完美。再考慮到後續還要實現「列表配置化協議」,現在協議結構的包容性明顯是不夠的,所以接下來就是協議的升級了,敬請期待。
“謹記,你是在尋找最好的答案,而不是你自己能得出的最好答案。”——Ray Dalio
- 看我用 Linux 帶娃,培養程式設計興趣
- 【微前端】Qiankun Vue3 配置
- 通用 Form API 協議 - 基礎版
- Final Form 設計思路淺析
- 【低程式碼漫談】 lowcode-engine - Vue Renderer 嘗試
- Redash 設計理念淺析
- Metabase 設計理念淺析
- DataEase 設計理念淺析
- 開源 BI 工具調研:Superset、Metabase、Redash、DataEase(一)- 基本資料
- Ubuntu 一行命令裝軟體——VirtualBox
- 程式設計師怎麼給娃起名?當然是寫個指令碼!
- GoGoCode - 像用 Jquery 一樣方便地處理 AST
- 【gRPC】Web 請求的 TS 封裝 - 完美版
- 【gRPC】2 分鐘學會 Protocol Buffer 語法
- 【gRPC】封裝前端網路請求的核心思想 - TS版
- 如何避免 Vue 的漏洞破壞單向資料流
- 用函數語言程式設計寫出“傻瓜”都能看懂的程式碼
- Vue3 最佳實踐之編碼規範