formily-面向中後臺場景的複雜解決方案

正文

在解決企業級應用的前端問題中,表單是個沒法繞過的大山,正好最近有時間,調研一下 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 的聯動以及生命週期

這裏放在一塊兒講,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 //表明目標字段狀態
}

包括內置的聯動類型:

  • value:visible,由值變化控制指定字段顯示隱藏
  • value:schema,由值變化控制指定字段的 schema
  • value:state,由值變化控制指定字段的狀態

能夠看出這裏的語法是以 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

actions/effects

這裏承接上文的生命週期,提供了除 ref 以外的方式來達到:

  • 外部調用組件內部 api 的問題,主要是使用 actions
  • 組件內部事件通知外部的問題,同時藉助了 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 次渲染,就能夠給其設置默認值

表單狀態

FormState

狀態名 描述 類型 默認值
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

FieldState

狀態名 描述 類型 默認值
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 組件
    提供的擴展方式主要有:

    • SchemaForm 中傳入 components 擴展(要求組件知足 value/onChange API)
    • SchemaForm 中傳入 components 組件擁有 isFieldComponent 靜態屬性,能夠拿到 FieldProps, 獲取更多內容,則能夠經過 useSchemaProps 方法
    • registerFormField 全局註冊擴展組件,要求傳入組件名和具體組件,同時,若是針對知足 value/onChange 的組件,須要用 connect 包裝,不包裝,須要手動同步狀態(藉助 mutators)
    • registerFormFields 全局批量註冊擴展組件,同時,若是針對知足 value/onChange 的組件,須要用 connect 包裝,不包裝,須要手動同步狀態(藉助 mutators)
  • 擴展 VirtualField 組件

  • 擴展校驗模型(規則、文案、模板引擎) registerValidationMTEngine registerValidationRules setValidationLocale

  • 擴展聯動協議

  • 擴展生命週期

  • 擴展 Effect Hook

  • 擴展狀態(FormState/FieldState/VirtualFieldState)擴展狀態的方式主要有如下幾種:

    • 直接調用 actions.setFormState/actions.setFieldState 設置狀態,這種方式主要在 Form 組件外部調用,在 effects 裏消費
    • 使用 useFormState/useFieldState 設置狀態,這種方式主要在自定義組件內部使用,使用這兩個 API,咱們能夠將狀態掛在 FormGraph 裏,這樣就能統一走 FormGraph 對其作時間旅行操做

性能優化

這是官方提供的方法:

大數據場景

推薦使用內置 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 , 它所能起到的做用對於咱們目前來講就是承上啓下的一個很重要的特性.會讓咱們再也不專門針對業務來寫表單,而是經過這種方式達到抽象建模的能力,併爲以後的工程化提供了良好的基礎.

當再也不針對業務去思考問題,而是站在更高的維度去思考前端如何結合業務場景快速提升生產環境的效率,那麼才能走的更遠.

更多關於這方面的延展,能夠參考前端早早聊大會的相關議題 前端搞搭建 的相關內容.

相關文章
相關標籤/搜索