背景#
因為工作原因近一個月換了完全沒用過的技術棧開發新項目。React、Storybook 這些倒還好,基本一天就上手了,倒是 Formily 這厮陸續摸索了半個多月才算入門,趁一階段工作結束空隙我就把這些經驗總結一下。
因為項目原因我使用 Formily 的方式不一定是最佳實踐甚至不一定是對的,對表單方面的使用也約等於零,主要場景集中於使用@formily/antd-v5和自製組件上。
基礎#
Formily 是阿里開發的一套以 Schema 描述表單的開源框架,不過我們項目中使用它的理由並不是構建表單,而是用於動態構建頁面。
Formily 裡有三個比較重要的概念:Form、Field 和 Schema。
- Form 是整個表單的核心,其實例可用於獲取和設置表單的校驗、值、字段、字段聯動等內容
- Field 是 Form 的組成部件,也就是字段,比如表單裡設置了一個叫 name 為
keyword
的輸入框,那每次修改輸入框的內容時,form.values.keyword
就會跟著變動。 - Schema 是描述 Field 的數據,可以簡單的認為其與 ReactNode 等價,具體一個 Schema 能包含什麼內容可以參考官方文檔。
- 在 Schema 轉組件的過程中 Formily 會默認代理組件的
value
和onChange
,所以要開發 Formily 組件時要考慮到這一點。
- 在 Schema 轉組件的過程中 Formily 會默認代理組件的
從一個簡單的頁面開始#
先從一個簡單的示例開始
上面的代碼創建一個 ==“帶邊框的 div,裡面裝著一個輸入框和另一個帶了兩個輸入框的容器”== 的場景,從代碼中我們可以得到幾個結論:
- Schema 的層級結構相當於 HTML 中的層級結構,其中的
properties
起到了類似children
的作用。 createForm
中可以定義表單的一些初始狀態?x-component
代表當前位置需要渲染的元素,可以支持原生的標籤可自定義的組件,其中的自定義組件需要在createSchemaField
中進行註冊。x-component-props
中定義了組件的 props,x-decorator
定義了包裝這個組件的組件。
簡單操作一下表單,可以發現 Schema 和實際的表單值的映射關係。
如果要管理不同字段的值,schema 的type
非常關鍵。
- 當
type
為void
的時候,表單會忽略這一層的路徑。 - 當
type
為object
的時候,該字段會成為承接子字段的對象。 - 當
type
為基本類型的時候,該字段代表具體的值。
當時我們本來打算封裝一個組合了 Card + Tabs 功能的組件,但是在 Formily 的機制下,字段本身不能既表示自己的值(Tabs 的 activeKey),也成為包含子字段的對象,所以最後打消了這種做法。
函數處理#
如果堅持使用 JSON Schema,那往 Schema 裡塞函數的做法就顯得不那麼合理了。在 Schema 中,Formily 會把{{}}
的字符串處理成函數,所以要轉換一下寫法。
// ...
input: {
type: "string",
"x-component": "Input",
"x-component-props": {
onClick: `{{ (event) => { console.log(event) } }}`
}
}
// ...
同樣的,對需要傳 ReactNode 的 props 也是如此處理,不過需要使用React.createElement
import { createElement } from "react";
import { Input } from '@formily/antd-v5'
import { SearchOutlined } from '@ant-design/icons';
// ...
const SchemaField = createSchemaField({
components: {
Input
},
scope: {
createElement,
SearchOutlined
}
})
// ...
input: {
type: "string",
"x-component": "Input",
"x-component-props": {
suffix: `{{ createElement(SearchOutlined) }}`
}
}
// ...
字段聯動#
這部分內容官網文檔說得就挺好的,就不贅述了
數據傳遞#
我們當時的頁面有一個聯動邏輯,當在頂部導航欄切換時間後,下面的各個圖表都要同步更新數據,而我們的解決方式如下,不一定是最佳實踐。
組件開發流程#
Formily 組件和常規的 React 組件差別還是挺大的,如果把@formily/antd-v5
作為官方的推薦實踐方式的話,那就不能以之前的思路來開發組件。
以 antd 的 Table 為例,無論是columns
還是dataSource
都是作為 props 的一部分傳遞給組件的,但在 Formily 中,數據應只由默認配置的props.value
來處理,所以開發時應該做好渲染數據與props.value
的轉換。對接已有組件時也可以用官方的mapProps方法做映射。
要較真的話數據放在哪都是可以實現功能的,但能簡單的從
form.values
拿到所有值還是比把數據分散到 field 的componentProps
要方便的。
除此之外,善用useForm
、useField
和useFieldSchema
可以方便獲取父子字段的內容。
一些坑#
ReactNode 轉 Schema#
雖然可以用{{createElement}}
的方式傳遞 ReactNode,但對於自定義組件來說還需要提前把組件傳入 scope 中,對於動態的 Schema 來說,這難以做到按需導入。
我當時的做法是組件內部做攔截。用useFieldSchema
拿到 Schema,然後判斷對應的屬性是否也是一個 Schema 對象,如果是則返回一個RecursionField
來渲染 Schema。
攔截寫完後乍一看還沒什麼毛病,但是在 antd 的 Table sortIcon 時就傻眼了,渲染的圖標在點擊後完全沒有狀態變化。
個人猜測這跟 Formily 的渲染機制有關,RecursionField
的渲染結果被緩存了,只是重新運行函數傳遞新的 props 還不足以觸發其重新渲染。
onChange#
還是跟 Table 的過濾有關,原流程中點擊表頭的排序會觸發表格的onChange
事件獲得排序信息。
但我在嘗試換了多種寫法後發現仍不能觸發這個onChange
,看了源碼後才發現為了防止冒泡官方給onChange
覆蓋了個空函數。你們是完全用不到這個功能嗎?
業務層面沒什麼解決方法,這裡最後用了 patch 的方式解決的,頗為麻煩。