網易雲音樂面向複雜場景的表單解決方案

本文做者:董健華前端

1. 背景

雲音樂 B 端業務場景很是多,B 端業務相對於 C 端業務產品生命週期更長並且更注重場景的梳理。不少時候開發 B 端業務都是拷貝以前的代碼,這樣增長了不少重複並且枯燥的工做量。git

中後臺系統其實能夠拆分紅幾個比較通用的場景:表單、表格、圖表,其中表單涉及到聯動、校驗、佈局等複雜場景,常常是開發者須要耗費精力去解決的點。github

對比傳統的 Ant Design[1] 表單開發開發方式,咱們認爲有如下問題:web

  1. 首先代碼沒法被序列化,並且對於一些非前端的開發者更習慣用 JSON 方式描述表單,由於足夠簡單
  2. 表單的校驗並無和校驗狀態作結合
  3. onChange 實現的聯動方式在複雜的聯動狀況下代碼會變得難以維護,容易產生不少鏈表式的邏輯
  4. 表單有許多互斥的狀態能夠整理,並且咱們也但願用戶能夠很輕易的在這些狀態間進行切換
  5. 對於一些比較經常使用並且通用的場景,例如:表單列表,也能夠抽離出一套可行的方案

因此雖然傳統的表單開發方式已經足夠的靈活,可是我也依然認爲表單還有優化的空間,在靈活與效率上作了些權衡。面試

外界也有比較成熟的表單解決方案,例如:Formliy[2]FormRender[3] 。雖然解決了上面某幾個點的問題,可是依然不夠全面,咱們須要有本身 style 的方案。算法

因此爲了提升中後臺開發效率,讓前端可以把時間投入到更有意義的事情裏,咱們總結了一套面向複雜場景的表單解決方案。json

2. 技術方案

在技術方案上相當重要的一環就是Schema設計,框架架構等工做都是圍繞這一環去實現的,因此我會沿襲這個思路給你們作介紹。api

2.1 Schema設計

表單方案基於 Ant Design 開發,經過 JSON 方式配置 Schema,可是並不是是 JSON Schema,外界不少基於 JSON Schema 的配置方案,其實也有考慮過,不過 JSON Schema 寫起來有點麻煩,因此對 JSON Schema 的轉換隻做爲一項附加的能力。微信

案例以下面代碼所示,最簡單的表單字段只要配置 keytypeui.label 就能夠了:架構

const schema = [
    {
        "key""name",
        "type""Input",
        "ui": {
            "label""姓名"
        }
    },
    {
        "key""age",
        "type""InputNumber",
        "ui": {
            "label""年齡"
        },
        "props": {
            "placeholder""請輸入年齡"
        }
    },
    {
        "key""gender",
        "type""Radio",
        "value""male",
        "ui": {
            "label""性別"    
        },
        "options": [
            {
                "name""男",
                "value""male"
            },
            {
                "name""女",
                "value""female"
            }
        ]
    }
];

export default function ({
    const formRef = useRef(null);

    const onSubmit = () => {
        formRef.current.submit().then((data: any) => {
            console.log(data);
        });
    };

    const onReset = () => {
        formRef.current.reset();
    };

    return (
        <>
            <XForm
                ref={formRef}
                schema={schema}
                labelCol={{ span: 6 }}
                wrapperCol={{ span: 12 }}
            />
            <div>
                <Button type="primary" onClick={onSubmit}>提交</Button>
                <Button onClick={onReset}>重置</Button>
            </div>
        </>
    );
}

由於方案是基於 Ant DesignForm 組件設計的,因此爲了保留 Ant Design 的一些特性,設計了 uiprops 兩個字段分別對應 Form.Itemprops 和組件的 props。即便後續 Ant Design 表單增長了某些功能或者特性,這套表單方案也能作到無縫支持。

2.1.1 校驗方式

既然表單是基於 Ant Design 實現的,那麼校驗也沿用了它的校驗類庫 async-validator[4],這個類庫已經比較成熟並且強大,可以校驗 ArrayObject 等深層級的數據類型,知足複雜校驗的需求,因此咱們直接在這個庫的基礎上作調整。

經過 rules 字段進行配置,除了 async-validator 原本就就有的特性外,還額外增長了 status(校驗狀態)和 trigger(觸發條件)枚舉以下:

  • status:校驗狀態
    • error(默認):錯誤
    • warning:警告
  • trigger:觸發條件
    • submit(默認):提交時候觸發
    • change:值變化時候觸發判斷
    • blur:失去焦點時候觸發判斷

基本使用方式以下:

{
    "key""name",
    "type""Input",
    "ui": {
        "label""姓名"
    },
    "rules": [
        {
            "required"true,
            "message""姓名必填",
            "trigger""blur",
            "status""error"
        }
    ]
}

2.1.2 聯動方式

除了校驗,聯動也是比較經常使用的功能,傳統的聯動經過組件 onChange 方式實現,當聯動邏輯比較複雜的時候,看代碼就像搜索鏈表同樣麻煩,因此這塊設計了一種 反向監聽 的方式,字段的全部變化都維護在字段配置自己,下降後期維護成本。

經過 listeners 字段進行配置,設計了 watch(監聽)、 condition(條件)、set(設置)三個字段組合實現聯動功能。

watch 記錄須要監聽的字段,當監聽字段有任何變化的時候,會觸發 condition 條件的判斷,只有條件判斷經過纔會接着觸發 set 設置。

[
    {
        "key""name",
        "type""Input"
    },
    {
        "key""gender",
        "type""Radio",
        "value""male",
        "options": [
            {
                "name""男",
                "value""male"
            },
            {
                "name""女",
                "value""female"
            }
        ],
        "listeners": [
            {
                "watch": [ "name" ],
                "condition""name.value === 'Marry'",
                "set": {
                    "value""female"
                }
            }
        ]
    }
]

上述例子當名字爲 Marry 的時候,性別默認調整成女。

2.1.3 表單狀態

咱們發現有些聯動場景是爲了對字段作隱藏和顯示的操做,爲了方便用戶切換狀態,將4種互斥表單狀態整理成一個 status 字段:

  • status:狀態
    • edit(默認):編輯
    • disabled:禁用
    • preview:預覽
    • hidden:隱藏

preview 狀態並非組件自己具備的,可是預覽的需求蠻多的,因而咱們作了拓展,爲全部基本的表單組件預置了預覽的狀態。即便自定義組件也會默認展現字段值,若是須要自行處理的話也提供了方案。

使用方式以下:

[
    {
        "key""edit",
        "type""Input",
        "status""edit",
        "value""編輯",
        "ui": {
            "label""編輯"
        }
    },
    {
        "key""disabled",
        "type""Input",
        "status""disabled",
        "value""禁用",
        "ui": {
            "label""禁用"
        }
    },
    {
        "key""preview",
        "type""Input",
        "status""preview",
        "value""預覽",
        "ui": {
            "label""預覽"
        }
    },
 {
  "key""hidden",
        "type""Input",
        "status""hidden",
        "value""隱藏",
        "ui": {
            "label""隱藏"
        }
 }
]

效果圖以下:

2.1.4 Options設置

許多選擇組件使用 options 字段設置選項,選項有時候經過異步接口獲取。考慮到異步接口的狀況,設計了 4 套方案 :

  1. optionsArray 的狀況
{
    "key""type",
    "type""Select",
    "options": [
        {
            "name""蔬菜",
            "value""vegetables"
        },
        {
            "name""水果",
            "value""fruit"
        }
    ]
}
  1. optionsstring 的狀況,即接口連接
{
    "key""type",
    "type""Select",
    "options""//api.test.com/getList"
}
  1. optionsobject 的狀況, action 爲接口連接, nameProperty 配置 name 字段, valueProperty 配置 value 字段, path 爲獲取選項路徑, watch 配置監聽字段
{
    "key""type",
    "type""Select",
    "options": {
        "action""//api.test.com/getList?name=${name.value}",
        "nameProperty""label",
        "valueProperty""value",
        "path""data.list",
        "watch": [ "name" ]
    }
}
  1. actionfunction 的狀況
{
    "key""type",
    "type""Select",
    "options": {
        "action"(field, form) => {
            return fetch('//api.test.com/getList')
                .then(res => res.json());
        },
        "watch": [ "name" ]
    }
}

2.1.5 表單列表

表單列表是一種組合類型的表單,一般有 TableCard 兩種場景,具備增長和刪除功能。

這種類型的表單值是以 Array 的形式返回的,因此設計了 Array 組件,根據 props.typeTableCard 形態進行切換(貌似這種狀況很少),children 配置子表單,使用方式以下:

{
    "key""array",
    "type""Array",
    "ui": {
        "label""表單列表"
    },
    "props": {
        "type""Card"
    },
    "children": [
        {
            "key""name",
            "type""Input",
            "ui": {
                "label""姓名"
            }
        },
        {
            "key""age",
            "type""InputNumber",
            "ui": {
                "label""年齡"
            }
        },
        {
            "key""gender",
            "type""Radio",
            "ui": {
                "label""性別"
            },
            "options": [
                {
                    "name""男",
                    "value""male"
                },
                {
                    "name""女",
                    "value""female"
                }
            ]
        }
    ]
}

效果圖以下:

2.2 框架架構

圍繞Schema設計思路,咱們採用了基於分佈式管理方案,將核心層和渲染層分離,字段信息維護在覈心層,渲染層只負責渲染的工做,作到數據和界面代碼的分離結構。

核心層與渲染層之間經過 Sub/Pub 方式進行通信,渲染層經過監聽核心層定義的一系列 Event 事件對界面做出調整。

這種數據狀態的改變驅動界面的變化已經不是什麼新鮮事了,在大多數框架中被普遍使用,其中優點有:

  1. 方面各個字段之間數據與狀態共享
  2. 經過對事件的控制,可以合理的優化渲染次數,提升性能
  3. 可以適配多框架的狀況,只需複用一套核心層代碼

核心層主要由 FormFieldListenerManagerValidatoroptionManager 幾部分組成以下圖所示:

其中 Form 是表單原型,下面承載了不少 Field 字段原型,由 ListenerManager 統一管理聯動方面的功能,Field 下具備 ValidatorOptionManager 分別管理校驗和 options 選項功能

2.2.1 校驗實現

主要仍是經過 async-validator 類庫實現,可是依然沒法知足多校驗狀態和多觸發條件的狀況,因此在這個基礎上作了些拓展,封裝成一個 Validator 類。

Validator 只有一個 Validator.validate 方法,傳遞一個 trigger 參數,實例化 Validator 時候會去解析 rules 字段,根據 trigger 進行分類並建立對應的 async-validator 實例。

2.2.2 聯動實現

ListenerManager 具備 ListenerManager.add 方法和 ListenerManager.trigger 方法,分別用於解析並添加 listeners 字段以及 Field 字段發生變化時觸發聯動效果。

具體流程是在初始化 Field 時,會將 listeners 字段經過 listenerManager.add 方法解析信息,根據 watch 中的 key 值進行分類並保存在其中,當 Field 信息發生變化的時候會經過 ListenerManager.trigger 觸發聯動,判斷 condition 條件是否知足,若是知足即觸發 set 內容。

2.2.3 表單列表實現

表單列表實際上是由多個 XForm 實例構成,每個自增項都是一個 XForm 實例,因此聯動只能在同一行上進行,不能跨行聯動。

當點擊添加按鈕的時候,會根據 children 提供的 Schema 模板建立一個 XForm 實例:

2.2.4 佈局實現

除了 Ant Design 的 Form 提供的三種佈局方式(horizontal、vertical、inline),還須要提供一種更靈活的佈局方式來知足更加複雜的狀況。

佈局真是一個很頭疼的問題,特別是 Schema 在相似 JSON 的結構下實現複雜的佈局很容易致使 Schema 嵌套層級深,這種是咱們不肯意看到的。

最初方案是經過網格佈局實現,經過設置 Formrow.count 或者 col.count 參數計算出網格的行數和列數再對字段進行分佈,這種方式只適用於每行列數都一致的狀況,可是這種方式難以知足每行列數不一致的狀況:

因此從新設計了一個 ui.groupname 的字段,同一個 groupname 的字段都會被一個 div 包裹住,而且 divclassNamegroupname ,用戶要實現複雜的佈局能夠本身寫樣式去實現,這樣的方案雖然簡陋,可是實用。

3. 細節設計

3.1 忽略特定字段值

有些場景須要忽略 statushidden 的字段的值,因此設計了一個 ignoreValues 字段,字段配置有下面幾種狀況:

  • hidden:忽略狀態爲 hidden 的狀況
  • preview:忽略狀態爲 preview 的狀況
  • disabled:忽略狀態爲 disabled 的狀況
  • null:忽略值爲 null 的狀況
  • undefined:忽略值爲 undefined 的狀況
  • falseLike:忽略值 == false 的狀況

經過配置 ignoreValues 字段,提交後返回的 values 就會忽略相應的字段:

<XForm schema={schema} ignoreValues={['hidden', 'null']}/>

3.2 字段解構與重組

字段解構是指把一個字段的值拆成多個字段,字段重組是指把多個字段組合成一個字段,這塊的具體功能還未實現,可是已經有了初步的想法。

字段解構例子以下,主要是經過 key 對字段進行拆分,最終返回 values 包含 startTimeendTime 兩個字段:

{
    "key""[startTime, endTime]",
    "type""RangePicker",
    "ui": {
        "label""時間選擇"
    }
}

發現許多場景須要由多個字段組合成一個字段,這種狀況大多須要寫自定義組件否則就是後期須要對數據進行處理,爲了簡化這一過程因此設計了字段重組的功能。經過 Combine 組件將多個字段重組成一個字段:

{
    "key""time",
    "type""Combine",
    "ui": {
        "label""時間選擇"
    },
    "props": {
        "shape""{startTime, endTime, type}"
    },
    "children": [
        {
            "key""startTime",
            "type""DatePicker"
        },
        {
            "key""endTime",
            "type""DatePicker"
        },
        {
            "key""type",
            "type""Select",
            "options": [
                {
                    "name""發行時間",
                    "value""publishTime"
                },
                {
                    "name""上線時間",
                    "value""onlineTime"
                }
            ]
        }
    ]
}

4. 結尾

完善表單這款產品的過程也是一個博採衆長的過程,咱們調研了業界競品結合自身業務需求,開發出了這款產品。上面供你們參考,很是遺憾的是咱們產品還未開源,相信會在合適的時候跟你們見面。

5. 相關資料

  • Formily [5]
  • FormRender [6]

參考資料

[1]

Ant Design: https://ant.design/

[2]

Formliy: https://formilyjs.org/

[3]

FormRender: https://alibaba.github.io/form-render

[4]

async-validator: https://github.com/yiminghe/async-validator

[5]

Formily: http://formilyjs.org/

[6]

FormRender: https://alibaba.github.io/form-render/

[7]

網易雲音樂大前端團隊: https://github.com/x-orpheus

- END -

   


推薦閱讀 
網易嚴選跨框架組件開發實踐
1024!前端之露首波福利
React v17.0 正式發佈!能夠升級啦
使用IoC來管理你的Vue應用
面試會遇到的手寫 Pollyfill 都在這裏了
【覆盤系列第二篇】面試遇到算法題怎麼辦

本文分享自微信公衆號 - 前端之露(gh_ef72c6726e70)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索