在解決企業級應用的前端問題中,表單是個沒法繞過的大山,正好最近有時間,調研一下 Formily-來自阿里巴巴的面向中後臺複雜場景的表單解決方案,也是一個表單框架,前身是 UForm.主要解決如何更好的管理表單邏輯,更好的保證表單性能,以及展望將來,讓非技術人員高效開發表單頁面.能夠查看相關連接 https://github.com/alibaba/formily,目前 issues 已關閉了 419 項,還有 21 項待處理,總的來講應該是有潛力的,先 fork,慢慢讀.html
準備寫三篇詳細進行介紹,而本篇是簡單介紹以及實例,先會用再看源碼前端
本文所涉及到的demo均放在github https://github.com/leomYili/formilyDemo 上.react
構造了一個 Observable Form Graphgit
從官網的介紹來看,使用了 RxJS,固然這裏只是簡略介紹,以後會詳細介紹;es6
setFieldState( Subscribe( FormLifeCycle, // 表單的生命週期 Selector(Path) // Path是字段路徑, ), TargetState // 操做具體字段的狀態 )
從表達式上來看, Formily 遵循了 發佈訂閱者 的設計模式,但願用戶再也不經過業務邏輯去組裝表單,而是經過簡單的調用 api,以及路徑映射模式更清晰簡單的來描述聯動的方式,以及在跨終端場景下實現通用表單解決方案.github
這裏能夠看出 Formily 的野心,也說明了其不是一個簡單的相似 rc-form 的項目,而是以框架的方式呈現出來,野心很大.算法
對於面向複雜場景的企業管理應用以及工具類型的應用來講,也確實更須要一整套的框架來解決研發效率以及後續擴展問題.express
並且,採用與 UI 無關的方式來構建核心,作跨終端也比較簡單,仍是期待下通用組件庫吧,目前官方提供的通用庫應該還沒開發完成,但從兼容ant-design
以及fusion-design
等庫來講,基礎框架是有了.從 api 上來看,與 antd v4 的 form 類似程度仍是比較高的.後端
固然,同出一源,Formily 的 API 會更加豐富,學習成本會比 antd 要高.不過 JSON Schema 的使用在必定程度上來講,會比單純的 UI 描述在可維護性,效率,協做上帶來必定的提高,但與之相對的學習成本,反認知都是問題.這裏也只是做爲其中一種的方案提供出來.主要仍是爲了以後的動態渲染吧.設計模式
官方提供了三種開發模式,分別針對
純 JSX 開發表單: 用於純前端 jsx 開發方式,自定義表單項以及複合形態居多.
<Form labelCol={7} wrapperCol={12} onSubmit={console.log}> <div style={{ padding: 20, margin: 20, border: '1px solid red' }}> Form組件內部能夠隨便插入UI元素了 </div> <FormItem label="String" name="string" component={Input} /> <FormButtonGroup offset={7}> <Submit>提交</Submit> </FormButtonGroup> </Form>
JSON Schema 開發表單: 後端動態渲染表單,可視化配置能力.
<SchemaForm components={{ Input }} labelCol={7} wrapperCol={12} onSubmit={console.log} schema={{ type: 'object', properties: { string: { type: 'string', title: 'String', 'x-component': 'Input' } } }} > <FormButtonGroup offset={7}> <Submit>提交</Submit> </FormButtonGroup> </SchemaForm>
JSX Schema 開發表單: 用於後端動態渲染表單的過分形態.(過分形態意味着 schema 與 field 可並存,極大的方便了協做與溝通)
<SchemaForm components={{ Input }} labelCol={7} wrapperCol={12} onSubmit={console.log} > <div style={{ padding: 20, margin: 20, border: '1px solid red' }}> 這是一個非Field類標籤,會被挪到最底部渲染 </div> <Field type="string" title="String" name="string" x-component="Input" /> <FormButtonGroup offset={7}> <Submit>提交</Submit> </FormButtonGroup> </SchemaForm>
這裏放在一塊兒講,Formily 提供的 schema 屬性與表達式細節請參考文檔.
這裏的聯動分爲 schema 協議層面簡單聯動以及 actions/effects 複雜聯動.先說簡單聯動:
使用 x-linkages 屬性,編輯其結構達到聯動配置的效果:
{ "type": "string", "x-linkages": [ { "type": "value:visible", "target": "aa", "condition": "{{ $self.value === 123 }}" } ] }
固然,這裏的上下文就尤其重要了,經過合法的上下文參數,才能更好的控制表單項進行聯動.
目前注入的環境變量:
{ ...FormProps.expressionScope, //表明SchemaForm屬性上經過expressionScope傳遞下來的上下文 $value, //表明當前字段的值 $self, //表明當前字段的狀態 $form, //表明當前表單實例 $target //表明目標字段狀態 }
包括內置的聯動類型:
能夠看出這裏的語法是以 condition
爲核心的,進而控制表單的兩大核心屬性:props 與 state;
進而知足平常需求,一樣提供了可擴展的方法.
經過對於生命週期的理解,就像 react 提供的 component 的生命週期同樣,能夠在相應的生命週期裏完成各類操做.
詳細的內容在生命週期章節細講
通俗來說,就是由於formily
使用了 RxJS
,以後,返回的 Observer
對象所帶來的能力包括 dispatch 與 notify,能夠看出 API 基本保持一致.好處是能夠借用 RxJS
的 method,對錶單的事件作各類操做,相關內容請參考 https://cn.rx.js.org/class/es6/Observable.js~Observable.html
雖然會帶來學習成本的提升,但相對的,針對複雜系統,使用 RxJS
能夠保證清晰的業務邏輯以及良好的性能,因此須要權衡利弊.
formily
已提供了不少內置的事件類型,可分爲全局型生命週期觸發事件類型與字段型生命週期觸發事件類型.
固然,既然使用 RxJS
,那麼相對應的自定義生命週期的語法也就相似了
import SchemaForm, { FormEffectHooks } from '@formily/antd' const { /** * Form LifeCycle **/ onFormWillInit$, // 表單預初始化觸發 onFormInit$, // 表單初始化觸發 onFormChange$, // 表單變化時觸發 onFormInputChange$, // 表單事件觸發時觸發,用於只監控人工操做 onFormInitialValueChange$, // 表單初始值變化時觸發 onFormReset$, // 表單重置時觸發 onFormSubmit$, // 表單提交時觸發 onFormSubmitStart$, // 表單提交開始時觸發 onFormSubmitEnd$, // 表單提交結束時觸發 onFormMount$, // 表單掛載時觸發 onFormUnmount$, // 表單卸載時觸發 onFormValidateStart$, // 表單校驗開始時觸發 onFormValidateEnd$, //表單校驗結束時觸發 onFormValuesChange$, // 表單值變化時觸發 /** * FormGraph LifeCycle **/ onFormGraphChange$, // 表單觀察者樹變化時觸發 /** * Field LifeCycle **/ onFieldWillInit$, // 字段預初始化時觸發 onFieldInit$, // 字段初始化時觸發 onFieldChange$, // 字段變化時觸發 onFieldMount$, // 字段掛載時觸發 onFieldUnmount$, // 字段卸載時觸發 onFieldInputChange$, // 字段事件觸發時觸發,用於只監控人工操做 onFieldValueChange$, // 字段值變化時觸發 onFieldInitialValueChange$ // 字段初始值變化時觸發 } = FormEffectHooks
更詳細的內容能夠看 https://formilyjs.org/#/0yTeT0/aAIRIjiou6
1.在外部環境中,經過全局綁定的 actions 對象觸發(經過 actions.dispatch 發送自定義事件)
actions.dispatch('custom_event',payload)
2.在 effects 中 const {dispatch} = createFormActions();
dispatch('custom_event',payload)
3.在自定義組件中// 在 useFormEffects 函數中
useFormEffects(($, {notify}) => { $("onFieldValueChange",'aa').subscribe(()=>{ notify('custom_event',payload) }) }) // 帶fieldProps的自定義組件中。from可直接從 props中取得。 const { from } = props; form.notify('custom_event',payload) // // 不帶 fieldProps的自定義組件中。須要經過 useField建立from對象 const { form } = useField({}); form.notify('custom_event',payload)
消費自定義事件和消費系統事件同樣。觸發事件時參數 payload 中,即爲 subscribe 中的傳入參數。payload 中若是有 name 屬性,則監聽時可經過 name 來過濾。
// effects中消費 // 自定義組件內useFormEffects中消費 $('custom_event').subscribe(payload=>{}) $('custom_event','aa').subscribe(payload=>{}) //則payload中必須含有name=aa
這裏承接上文的生命週期,提供了除 ref 以外的方式來達到:
RxJS
能夠方便的處理異步事件流競態組合問題,主要是使用 effects這裏能夠分享兩個使用 RxJS
進行處理的案例:
const customEvent$ = createEffectHook('CUSTOM_EVENT') const useMultiDepsEffects = () => { const { setFieldState, dispatch } = createFormActions() onFormMount$().subscribe(() => { setTimeout(() => { dispatch('CUSTOM_EVENT', true) }, 3000) }) onFieldValueChange$('aa') .pipe(combineLatest(customEvent$()))// 使用combineLatest解決生命週期依賴聯動的問題 .subscribe(([{ value, values }, visible]) => { setFieldState('bb', state => { state.visible = visible }) }) }) //藉助 merge 操做符對字段初始化和字段值變化的時機進行合流,這樣聯動發生的時機會在初始化和值變化的時候發生 merge(onFieldValueChange$('bb'), onFieldInit$('bb')).subscribe(fieldState => { if (!fieldState.value) return linkage.hide('cc') linkage.show('cc') linkage.value('cc', fieldState.value) }) }
這是一種相似 react-eva 的分佈式狀態管理解決方案,詳情能夠參考 https://github.com/janrywang/react-eva
路徑系統表明了 Form 與 field 之間的關聯.
這裏能夠看看匹配語法:
全通配: "*"
擴展匹配: "aaa~" or "~" or "aaa~.bbb.cc"
部分通配: "a.b.*.c.*"
分組通配:
"a.b.*(aa.bb.dd,cc,mm)"
嵌套分組通配:
"a.b.*(aa.bb.*(aa.b,c),cc,mm)" or "a.b.*(!aa.bb.*(aa.b,c),cc,mm)"
範圍通配:
"a.b.*[10:100]" or "a.b.*[10:]" or "a.b.*[:100]"
關鍵字通配: "a.b.[[cc.uu()sss*\\[1222\\]]]"
這裏我以爲比較好用的是字段解耦,對 name 用 ES Deconstruction 語法作解構,須要注意的是,不支持...語法:
<Field type="array" name="[startDate,endDate]" title="已解構日期" required x-component="DateRangePicker" />
與自定義的組件配合達到最佳效果
在 Formily 中,不論是 SchemaForm 組件仍是 Form 組件,都支持 3 個傳值屬性
1.value 受控值屬性
主要用於外部屢次渲染同步表單值的場景,可是注意,它不會控制默認值,點擊重置按鈕的時候值會被置空
2.defaultValue 同步初始值屬性
主要用於簡單同步默認值場景,限制性較大,只保證第一次渲染生效,重置不會被置空
3.initialValues 異步初始值屬性
主要用於異步默認值場景,兼容同步默認值,只要在第 N 次渲染,某個字段還沒被設置默認值,第 N+1 次渲染,就能夠給其設置默認值
狀態名 | 描述 | 類型 | 默認值 |
---|---|---|---|
displayName | Form 狀態標識 | string | "FormState" |
modified | 表單 value 是否發生變化 | boolean | false |
valid | 表單是否處於合法態 | boolean | true |
invalid | 表單是否處於非法態,若是校驗失敗則會爲 true | boolean | False |
loading | 表單是否處於加載態 | boolean | false |
validating | 表單是否處於校驗中 | boolean | false |
initialized | 表單是否已經初始化 | boolean | false |
submitting | 表單是否正在提交 | boolean | false |
editable | 表單是否可編輯 | boolean | false |
errors | 表單錯誤信息集合 | Array<{ path: string, messages: string[] }> | [] |
warnings | 表單警告信息集合 | Array<{ path: string, messages: string[] }> | [] |
values | 表單值 | object | {} |
initialValues | 表單初始值 | object | {} |
mounted | 表單是否已掛載 | boolean | false |
unmounted | 表單是否已卸載 | boolean | false |
擴展狀態 | 經過 setFormState 能夠直接設置擴展狀態 | any |
狀態名 | 描述 | 類型 | 默認值 |
---|---|---|---|
displayName | Field 狀態標識 | string | "FieldState" |
dataType | 字段值類型 | "any" | "array" |
name | 字段數據路徑 | string | |
path | 字段節點路徑 | string | |
initialized | 字段是否已經初始化 | boolean | false |
pristine | 字段 value 是否等於 initialValue | boolean | false |
valid | 字段是否合法 | boolean | false |
invalid | 字段是否非法 | boolean | false |
touched | 字段是否被 touch | boolean | false |
visible | 字段是否顯示(若是爲 false,字段值不會被提交) | boolean | true |
display | 字段是否 UI 顯示(若是爲 false,字段值能夠被提交) | boolean | true |
editable | 字段是否可編輯 | boolean | true |
loading | 字段是否處於加載態 | boolean | false |
modified | 字段的 value 是否變化 | boolean | false |
active | 字段是否被激活(onFocus 觸發) | boolean | false |
visited | 字段是否被 visited(onBlur 觸發) | boolean | false |
validating | 字段是否正在校驗 | boolean | false |
values | 字段值集合,value 屬性至關因而 values[0],該集合主要來源於組件的 onChange 事件的回調參數 | any[] | [] |
errors | 字段錯誤消息集合 | string[] | [] |
effectErrors | 人工操做的錯誤消息集合(在 setFieldState 中設置 errors 會被重定向到設置 effectErrors) | string[] | [] |
ruleErrors | 校驗規則的錯誤消息集合 | string[] | [] |
warnings | 字段警告信息集合 | string[] | [] |
effectWarnings | 人工操做的警告信息集合(在 setFieldState 中設置 warnings 會被重定向到設置 effectWarnings) | string[] | [] |
ruleWarnings | 校驗規則的警告信息集合 | string[] | [] |
value | 字段值 | any | |
initialValue | 字段初始值 | any | |
rules | 字段校驗規則 | ValidatePatternRules | [] |
required | 字段是否必填 | boolean | false |
mounted | 字段是否已掛載 | boolean | false |
unmounted | 字段是否已卸載 | boolean | false |
inputed | 字段是否主動輸入過 | true | |
props | 字段擴展 UI 屬性(若是是 Schema 模式,props 表明每一個 SchemaField 屬性,若是是 JSX 模式,則表明 FormItem 屬性) | {} | |
擴展狀態 | 經過 setFieldState 能夠直接設置擴展狀態 | any |
佈局各家公司要求都不相同,就不列出來了,比較定製化
擴展性是衡量一個框架的重要指標, formily
提供了不少的擴展入口:
擴展 Form UI 組件:
registerFormComponent(props => { return <div>全局擴展Form組件{props.children}</div> }) const formComponent = props => { return <div>實例級擴展Form組件{props.children}</div> } <SchemaForm formComponent={formComponent} components={{ Input }} onSubmit={values => { console.log(values) }} />
擴展 FormItem UI 組件
registerFormItemComponent(props => { return <div>全局擴展 FormItem 組件{props.children}</div> }) const formItemComponent = props => { return <div>實例級擴展FormItem組件{props.children}</div> } <SchemaForm formItemComponent={formItemComponent} components={{ Input }} onSubmit={values => { console.log(values) }} />
擴展 Field 組件
提供的擴展方式主要有:
擴展 VirtualField 組件
擴展校驗模型(規則、文案、模板引擎) registerValidationMTEngine registerValidationRules setValidationLocale
擴展聯動協議
擴展生命週期
擴展 Effect Hook
擴展狀態(FormState/FieldState/VirtualFieldState)擴展狀態的方式主要有如下幾種:
這是官方提供的方法:
推薦使用內置 BigData 數據結構進行包裝
import { BigData, SchemaForm } from '@formily/antd' const specialStructure = new BigData({ compare(a, b) { //你能夠定製當前大數據的對比函數,也能夠不傳,不傳則是引用對比 }, clone(value) { //你能夠定製當前大數據的克隆函數,也能夠不傳,若是不傳,拷貝則是引用傳遞 } }) const App = () => ( <SchemaForm initialValues={{ aa: specialStructure.create(BigData) }} //注意要保證create傳入的數據是Immutable的數據 effects={$ => { $('onFieldChange', 'bb').subscribe(() => { actions.setFieldState('aa', state => { state.props.enum = specialStructure.create(BigData) //注意要保證create傳入的數據是Immutable的數據 }) }) }} /> )
主要緣由是 Formily 內部會對狀態作深度拷貝,同時也作了深度遍歷髒檢測,這種方式對於用戶體驗而言是更好了,可是在大數據場景下,就會出現性能問題,藉助 BigData 數據結構,咱們能夠更加定製化的控制髒檢查和拷貝算法,保證特殊場景的性能平滑不受影響.
這種場景主要在聯動場景,好比 A 字段要控制 B/C/D/E 等等字段的狀態更新,若是控制的字段數量不多,那麼相對而言是收益最高的,可是控制的字段數量不少,100+的字段數量,這樣作,若是仍是以精確渲染思路來的話,至關於會執行 100+的渲染次數,同時 Formily 內部其實還會有一些中間狀態,就至關於一次批量更新,會致使 100 * n 的渲染次數,那這樣明顯是起到了副作用,因此,針對這種場景,咱們倒不如直接放開,讓表單整樹渲染,一次更新,這樣對於多字段批量操做場景,性能一會兒就上來了。下面是具體的 API 使用方法
onFieldValueChange$('aa').subscribe(() => { actions.hostUpdate(() => { actions.setFieldState('bb.*', state => { state.visible = false }) }) })
使用 hostUpdate 包裝,能夠在當前操做中阻止精確更新策略,在全部字段狀態更新完畢以後直接走根組件重渲染策略,從而起到合併渲染的目的.
這裏都使用了 ArrayList https://github.com/alibaba/formily/blob/master/packages/react-shared-components/src/ArrayList.tsx 做爲底層庫.
官方提供了兩個案例, ArrayTable 與 ArrayCards 有興趣能夠去看看.
這裏主要仍是想分析一下如何自定義實現一個自增列表組件.
這裏使用 IMutators API來完成
屬性名 | 說明 | 類型 | 默認值 |
---|---|---|---|
change | 改變當前行的值 | change(...values: any[]): any | |
focus | 聚焦 | ||
blur | 失焦 | ||
push | 增長一行數據 | (value?: any): any[] | |
pop | 彈出最後一行 | change(...values: any[]): any | |
insert | 插入一行數據 | (index: number, value: any): any[] | |
remove | 刪除某一行 | (index: number | string): any |
unshift | 插入第一行數據 | (value: any): any[] | |
shift | 刪除第一行是數據 | (): any[] | |
exist | 是否存在某一行 | (index?: number | string): boolean |
move | 將指定行數據移動到某一行 | ($from: number, $to: number): any[] | |
moveDown | 將某一行往下移 | (index: number): any[] | |
moveUp | 將某一行往上移 | (index: number): any[] | |
validate | 執行校驗 | (opts?: IFormExtendedValidateFieldOptions): Promise |
能夠簡單看下代碼:
formily的思想仍是值得借鑑的,不過也正如官網所說,它並非一個簡單的輪子,而是一套解決方案,因此須要權衡利弊,充分考慮到業務場景是否須要這麼複雜的一套方案.
固然,真實用到生產環境時,還須要大量的擴展以及與業務結合,所幸這方面formily提供了完備的擴展方式.
但關鍵仍是 schema
,這實際上是外部的 DSL
, 它所能起到的做用對於咱們目前來講就是承上啓下的一個很重要的特性.會讓咱們再也不專門針對業務來寫表單,而是經過這種方式達到抽象建模的能力,併爲以後的工程化提供了良好的基礎.
當再也不針對業務去思考問題,而是站在更高的維度去思考前端如何結合業務場景快速提升生產環境的效率,那麼才能走的更遠.
更多關於這方面的延展,能夠參考前端早早聊大會的相關議題 前端搞搭建 的相關內容.