積夢前端 Meson Form 的分層抽象設計

概述

這篇文章大體梳理積夢採用的表單方案作的一些嘗試和回顧.
目前從用的方案是 Meson Form, 名字大體來源於 immer json:
https://github.com/jimengio/m...
目前 Meson Form 形態逐漸開始穩定了, 方案上基本仍是可靠的.
過程中的考慮有一些曲折, 大體作一些梳理.html

KForm

早先咱們的方案當中其實沿用了一套書寫較爲簡便的方案, 稱爲 KForm.
看過 Meson Form 例子的話, 跟 KForm 的寫法已經比較類似了,
主要看 items, 每一個元素定義了表單當中的一項, 這個表單有 3 項:前端

let { dataSource, onSubmitData, formRef } = this.props;

let items: IFormItem[] = [
  { id: "name", label: lang.lblName, rules: [{ required: true }] },
  { id: "code", label: lang.lblSerialNumber, rules: [{ required: true }] },
  { id: "description", label: lang.lblDescription },
];

return <KForm ref={formRef} items={items} data={dataSource} layout={layout} onSubmitData={onSubmitData} />;

粗看這個例子, 可能以爲已是比較成熟的表單方案了.
不過深刻使用的話, KForm 存在兩個問題,react

第一個問題是沒有類型系統的良好支持, 或者說 TypeScript 的良好支持.
KForm 的內部是基於 antd 表單的, 而控件通常都有各自的屬性,
KForm 當中添加屬性, 須要用 property 手動寫, 這個地方是丟失類型的,
這個地方的處理, 對於真實的開發調試來講不夠友好, 沒有檢查也沒有提示,git

let items: IFormItem[] = [
  {
    id: "userGroup",
    label: lang.lblUserGroup,
    rules: [{ required: true }],
    controlType: UserGroupSelectDropdown,
    controlPropsMapper: (controlProps) => {
      controlProps.plantId = plantId; // <-- 缺失類型檢查
      return controlProps;
    },
  },
];

必定程度上手動添加類型或許能夠做爲補充的, 可是書寫相對繁瑣.github

另外一個問題是可變數據, antd 的方案是基於可變數據實現的.
React 當中傾向使用不可變數據來輔助性能優化,
同時另外一方面, 不可變數據也能避免表單的對象被隨意修改,
KForm 當中可變對象被傳遞到多處, 就引起了一些狀態改變的 bug.
並且隨着咱們愈來愈多使用 immer, 二者之間的不協調就愈來愈明顯.typescript

另外還有個遇到的問題是 KForm 封裝好之後擴展性不夠.
這個就跟具體的實現有關係了, 致使不能應對一些特殊的場景.
好比自定義組件時要修改額外的字段, 就須要組件可以暴露底層操做.
但整體上感受隨着遇到不一樣的業務, 總以爲不夠用.json

Immer Form

爲了能解決前面說的幾個問題, 我基於 immer 開始尋找方案:數組

  • 整個方案圍繞 immer 設計, 不該該隨意出現可變數據,
  • 大部分的邏輯可以被 TypeScript 類型覆蓋到, 也可以配合自動補全,
  • 可以比較靈活地定製, 用於處理一些特殊的表單.

因爲沒有想到清晰的方案, 早先我先嚐試用簡單的函數來抽離複用的代碼,
好比表單的渲染, 好比錯誤校驗, 我分離出了一些經常使用的函數,
而後整理出大體一套方案, 完成了我當時遇到的幾個表單,
大體的代碼好比:
https://gist.github.com/cheny...性能優化

回頭來看, 這套代碼其實比較零碎, 表單狀態被暴露在外部,
也就意味着在父組件當中須要附加上若干狀態個方法用於維護校驗,
渲染部分至關於只有複用佈局, 可是沒有作封裝, 基本沒有限制.
這個寫法好處就是沒有什麼限制, 各類場景要用基本都是能夠用上的,
壞處就是.. 代碼會比較囉嗦, 錯誤須要本身綁定到對應位置, 其實挺煩.antd

JSON 配置表單

Immer Form 的寫法原本是打算逐步簡化的, 可是結果用了挺久的,
一方面是沒有找到好的入口, 另外一方面確實業務也消耗着主要的經歷,
我跟同事都是有點想念以前老代碼當中用的 antd 的, 以及前面這個寫法.

我以爲用 JSON 結構配置表單是正確的方向, 由於這樣描述比較少冗餘.
並且以前的 KForm 其實也證實對於簡單的業務, JSON 形態徹底夠用的.
因此很天然會想到作一個組件, 將 JSON 渲染到 Form, 以及生成簡單的邏輯,
以及對於特殊的場景, 提供自定義渲染或者其餘配置, 用來特殊處理.

可是中間有個問題, 即使是 JSON 我依然須要保證自動補全能用,
不過, 一個巨大的 JSON 整個在 VS Code 當中錯誤提示, 很是感人.

好比這樣一個結構,

let formItems = [
  {
    type: EMesonFieldType.Input,
    name: "name",
    label: "名字",
  },
  {
    type: EMesonFieldType.Input,
    name: "name",
    label: "名字禁用",
    disabled: true,
  },
]

我須要在 name 或者 label 位置填寫錯誤時可以被自動提示,
同事, 對於 disabled, 我輸入 dis 能看到對應的補全.
我大體知道 VS Code 有相似的功能的, 在我描述了 type 的前提下, 相似於,
https://basarat.gitbooks.io/t...

實際使用當中反而預測坑了, 我試了一下, 發現錯誤提示老是在整個 JSON 數組上.
後來在朋友的幫助下, 終於明確了在變量上直接加類型約束, 能夠規避問題, 也就是,

let formItems: IMesonFieldItem[] = [
  {
    type: EMesonFieldType.Input,
    name: "name",
    label: "名字",
  },
  {
    type: EMesonFieldType.Input,
    name: "name",
    label: "名字禁用",
    disabled: true,
  },
];

其中 IMesonFieldItem 是藉助 Union 關聯在一塊兒的多個 interface.
這樣寫以後, 錯誤提示和自動補全, 都顯得相對正常了.

Meson Form

基於上面這種 JSON 的格式, 以及一些字段, 我編寫了一個簡單的組件,
這樣, 就是一個簡單的 Meson Form 的結構了.
這是一個例子 http://fe.jimu.io/meson-form/

let formItems: IMesonFieldItem[] = [
  {
    type: EMesonFieldType.Input,
    name: "name",
    label: "名字",
  },
  {
    type: EMesonFieldType.Input,
    name: "name",
    label: "名字禁用",
    disabled: true,
  },
];

return (
  <div className={cx(row, styleContainer)}>
    <MesonForm
      initialValue={form}
      items={formItems}
      onSubmit={(form) => {
        setForm(form);
      }}
    />
    <div>
      <SourceLink fileName={"basic.tsx"} />
      <DataPreview data={form} />
    </div>
  </div>
);

相似地, 對於自定義渲染的需求, 直接用上一個 render 函數插入代碼,
Demo http://fe.jimu.io/meson-form/...

let formItems: IMesonFieldItem[] = [
  {
    type: EMesonFieldType.Custom,
    name: "x",
    label: "自定義",
    render: (value, onChange, form, onCheck) => {
      return (
        <div className={row}>
          <div>
            Custome input
            <Input
              onChange={(event) => {
                onChange(event.target.value);
              }}
              placeholder={"Custom field"}
              onBlur={() => {
                onCheck(value);
              }}
            />
          </div>
        </div>
      );
    },
  },
];

基於這套寫法, 後面又加上了 Select Switch 等組件和樣式,
目前支持的類型比較少, 常常依賴自定義渲染, 後續還要跟隨業務擴展.
實際使用當中也提出了須要更多鉤子用於狀態修改, 慢慢也加上了.
只能說大體知足了經常使用的需求, 加上自定義. 在原來的基礎上減小了代碼量.

另外一方面早期 KForm 大量的場景是跟 Modal 用在一塊兒的,
因此 Meson Form 也加上了 Modal 的封裝, 嘗試覆蓋一些經常使用的需求:
http://fe.jimu.io/meson-form/...
http://fe.jimu.io/meson-form/...

Meson Core

不過整體上說業務每每是多變的, 一個 Form 組件的形態總歸是不夠的.
好比說我會遇到場景, 沒有文字標籤, 標錯的樣式也有區別,
這種場景, 好比就是登陸框了, 一般就不是用 Form 的樣式去作的.
可是又比較明確, 它仍是 Form, 有校驗, 只是界面和結構有區別.

基於這一點, 咱們再進一步想, 前面的 Form 的封裝實際上是有點倉促的,
渲染部分的組件, 實際當中時會有多種可能的, 而不僅僅是一種渲染,
對於 Form 來講, 更加穩定真實的實際上是數據和校驗的部分,
這部分能夠超脫 UI 的形態, 可是表單本身基本都會有在表單項還有校驗,

那麼, 我就想起來用 Hooks 能夠分離出表單的狀態部分,
這部分包含表單的狀態, 校驗結構, 還有一些操做,
這部分代碼能夠超越表單組件自己, 被用到特殊的表單的場景, 核心的 API 好比:

let { formAny, errors, onCheckSubmit, checkItem, updateItem, forcelyResetForm } = useMesonCore({
  initialValue: submittedForm,
  items: formItems,
  onSubmit: onSubmit,
});

這裏能獲取 form errors, 這是渲染表單必備的數據,
而後也暴露出來其餘一些用於校驗和更新表單數據的函數, 甚至於重置表單的數據,
這樣就獲得一個例子, 能夠沿用 Meson Core, 然而本身定義界面如何渲染,
http://fe.jimu.io/meson-form/...

<div className={styleFormArea}>
  {formItems.map((item) => {
    switch (item.type) {
      case EMesonFieldType.Input:
        return (
          <div className={column} key={item.name}>
            <input
              value={form[item.name] || ""}
              type={item.inputType}
              placeholder={item.placeholder}
              onChange={(event) => {
                let text = event.target.value;
                updateItem(text, item);
              }}
              onBlur={() => {
                checkItem(item);
              }}
            />
            {errors[item.name] != null ? <div className={styleError}>{errors[item.name]}</div> : null}
          </div>
        );
    }
  })}
  <div>
    <button onClick={onCheckSubmit}>Submit</button>
  </div>
</div>

基於這個思路, 當咱們須要一個橫向佈局的表單的時候, 就能夠複用了.
核心的規則和校驗邏輯是能夠複用的, 渲染部分徹底用不一樣的實現,
http://fe.jimu.io/meson-form/...

因此 Meson Form 提供的 API實際上提供了兩個不一樣的層次,
直接用 Meson Form 能夠快速生成簡單的 Form, 或者 Core 用於定製.

其餘

固然對於業務來講, 場景多是無窮無盡的, 前面的方案依然未必足夠.

同事在使用 Meson Form 時候, 須要用到自有定義的 Footer,
這必定上跟 Meson Form 最初設定的數據流有衝突了,
因而他用 useImperativeHandle 又加上了一層封裝,
目的就是爲了能把一些事件拋出, 在外部找到地方去觸發, 而不收到設計的限制.

另外使用當中發現校驗規則不斷增多, 逐漸開始有一些明顯的重複,
這些規則按理說經過高階函數仍是能夠進一步進行抽象,
或者不用高階函數, 單純用 JSON 定義規則的話, 也可以表達.
因此這部分的抽象和簡化後面依然須要再補充.

按照 Meson Form 最初設想的, JSON 的格式本來極爲通用,
社區有別人的例子, 用 JSON 定義表單的格式, 而後前端直接渲染,
這樣若是還能在中臺把表單抽象稱爲服務的話, 還能分擔前端的工做量.
即使不能替代前端開發表單, 若是說能在必定程度上生成代碼, 也是有效果的.
因爲 toB 的屬性自己就具有大量的表單, 這方面會有不小的需求.

總之 Meson Form 仍是須要繼續擴充很完善, 用來應對更多業務場景.


其餘關於積夢前端的模塊和工具能夠查看咱們的 GitHub 主頁 https://github.com/jimengio .
目前團隊正在擴充, 招聘文檔見 GitHub 倉庫 https://github.com/jimengio/h... .

相關文章
相關標籤/搜索