作業少不了寫“增刪改查”,“增刪改查”中的“增”和“改”都與 Form 有關,可以說:提升了 Form 的開發效率,就提升了整體的開發效率,
本文通過總結 Form 的寫法,形成經驗檔案,用以提升團隊開發效率,
1.布局
不同人開發的表單,細看會發現:表單項的上下間距、左右間距有差別,如果 UE 同學足夠細心,挑出了這些毛病,開發同學也是各改各的,用獨立的 css 控制各自的表單樣式,未來 UE 同學要調整產品風格,開發需要改所有表單樣式,代價極高,
解決這個問題的辦法是:統一布局方式:Form + Space + Row & Col,
以下圖表單為例,進行說明,

const App = () => {
const [form] = Form.useForm();
return (
<Form
form={form}
labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }}
requiredMark={false}
onFinish={console.log}
>
<Form.Item name="name" label="名稱" rules={[Required]}>
<Input />
</Form.Item>
<Form.Item label="源IP" style={{ marginBottom: 0 }}>
<Address namePathRoot="src" />
</Form.Item>
<Form.Item label="目的IP" style={{ marginBottom: 0 }}>
<Address namePathRoot="dst" />
</Form.Item>
<Form.Item label=" " colon={false}>
<Space>
<Button type="primary" htmlType="submit">
確定
</Button>
<Button>取消</Button>
</Space>
</Form.Item>
</Form>
);
};
antd 采用的是 24 柵格系統,即把寬度 24 等分,以下代碼設定了:標簽占 4 個柵格,內容占 20 個柵格,
<Form labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
...
</Form>

確定、取消按鈕中間的間隔,通過 Space 組件來實作,不寫樣式,
<Space>
<button>確定</button>
<button>取消</button>
</Space>
按鈕和上方的輸入框左對齊,靠的是:設定 Form.Item 的 label 為一個空格,并且不顯示冒號,
<Form.Item label=" " colon="{false}">
<Space>
<button>確定</button>
<button>取消</button>
</Space>
</Form.Item>
還有一種做法是用柵格系統的 offset,讓 offset 值等于 Form labelCol 的 span,這種做法形成了依賴關系,以后調整 Form labelCol 的 span,還需要調整 offset,因此不建議這樣使用,
<Form.Item wrapperCol={{ offset: 4 }}>...</Form.Item>
<Form labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
...
</Form>
再來看 Address 組件,

Address 組件被用在兩個地方:
<>
<Form.Item label="源IP" style={{ marginBottom: 0 }}>
<Address namePathRoot="src" />
</Form.Item>
<Form.Item label="目的IP" style={{ marginBottom: 0 }}>
<Address namePathRoot="dst" />
</Form.Item>
</>
const Address = ({ namePathRoot }) => {
return (
<Row gutter={[8, 8]}>
<Col span={24}>
<Form.Item name={[namePathRoot, "type"]} initialValue="https://www.cnblogs.com/apolis/archive/2022/02/10/ip" noStyle>
<Select>
<Select.Option value="https://www.cnblogs.com/apolis/archive/2022/02/10/ip">IP地址</Select.Option>
<Select.Option value="https://www.cnblogs.com/apolis/archive/2022/02/10/iprange">IP地址段</Select.Option>
</Select>
</Form.Item>
</Col>
<Col flex={1}>
<Form.Item name={[namePathRoot, "version"]} initialValue="https://www.cnblogs.com/apolis/archive/2022/02/10/v4">
<Select>
<Select.Option value="https://www.cnblogs.com/apolis/archive/2022/02/10/v4">IPV4</Select.Option>
<Select.Option value="https://www.cnblogs.com/apolis/archive/2022/02/10/v6">IPV6</Select.Option>
</Select>
</Form.Item>
</Col>
<Col flex={2}>
<Form.Item
dependencies={[
[namePathRoot, "type"],
[namePathRoot, "version"],
]}
noStyle
>
{({ getFieldValue }) => {
const type = getFieldValue([namePathRoot, "type"]);
const version = getFieldValue([namePathRoot, "version"]);
if (type === "ip") {
return (
<Form.Item
name={[namePathRoot, "ip"]}
dependencies={[
[namePathRoot, "type"],
[namePathRoot, "version"],
]}
validateFirst
rules={[Required, version === "v4" ? IPv4 : IPv6]}
>
<Input placeholder="請輸入IP地址" />
</Form.Item>
);
} else {
return (
<Row gutter={8} style={{ lineHeight: "32px" }}>
<Col flex={1}>
<Form.Item
name={[namePathRoot, "iprange", "start"]}
dependencies={[
[namePathRoot, "type"],
[namePathRoot, "version"],
]}
validateFirst
rules={[Required, version === "v4" ? IPv4 : IPv6]}
>
<Input placeholder="請輸入起始IP" />
</Form.Item>
</Col>
-<Col flex={1}>
<Form.Item
name={[namePathRoot, "iprange", "end"]}
dependencies={[
[namePathRoot, "type"],
[namePathRoot, "version"],
[namePathRoot, "iprange", "start"],
]}
validateFirst
rules={[
Required,
version === "v4" ? IPv4 : IPv6,
buildMultiFieldsRule(
[
[namePathRoot, "iprange", "start"],
[namePathRoot, "iprange", "end"],
],
(start, end) => ipToInt(end) > ipToInt(start),
"結束IP需要大于起始IP"
),
]}
>
<Input placeholder="請輸入結束IP" />
</Form.Item>
</Col>
</Row>
);
}
}}
</Form.Item>
</Col>
</Row>
);
};
注意 Address 組件中第一個 Form.Item 有屬性 noStyle,noStyle 讓 Form.Item 沒有樣式,這樣 Form.Item 就不會有 margin 了,Form.Item 之間就會更緊湊了,
對比一下有和無 noStyle 的區別:
有 noStyle:

無 noStyle:

下面來看如何用 Row & Col 實作兩行的布局,
第一行包含一個下拉框;第二行分為兩部分:左側部份是下拉框,右側部份根據第一行下拉框的選中條件渲染,

<Row gutter={[8, 8]}>
<Col span={24}>第一行</Col>
<Col flex={1}>第二行左側部分</Col>
<Col flex={2}>第二行右側部分</Col>
</Row>
gutter={[8, 8]} 指定 Col 之間的水平間隔和垂直間隔,
<Col span={24}>第一行</Col>,antd 采用 24 柵格系統,因此該 Col 占滿整行,Row 默認自動換行 wrap={true},所以后面的 Col 會換行,
<>
<Col flex={1}>第二行左側部分</Col>
<Col flex={2}>第二行右側部分</Col>
</>
第二行的實作有個細節,兩個 Col 的寬度用的不是 span,而是 flex,如果用 span={8} 和 span={16},那么這兩個 Col 的寬度會固定為 1:2,
這里的設計是:第二行左側部分【下拉框】的寬度是變化的,當第二行右側部分展示兩個輸入框時候,第二行左側部分寬度變小,

Col 使用 flex 指定寬度可以實作這個效果,對應的 css 樣式如下:
| Col:第二行左側部分 | Col:第二行右側部分 |
|---|---|
flex={1} |
flex={2} |
flex-grow: 1;flex-shrink: 1;flex-basis: auto; |
flex-grow: 2;flex-shrink: 2;flex-basis: auto; |
這樣的效果是:
- 如果
組件默認寬度總和小于行寬,剩余的寬度根據flex-grow的比例來分配; - 如果
組件默認寬度總和大于行寬,超出的寬度根據flex-shrink的比例來縮小,
我們的目標是在專案中統一布局方式,不要把“不寫樣式”作為規則規范,那會讓我們束手束腳,
實際上這個表單也寫了兩處樣式,
源 IP、目的 IP 的 Form.Item 設定了 marginBottom: 0,
<Form.Item label="源IP" style={{ marginBottom: 0 }}>
<Address namePathRoot="src" />
</Form.Item>
這是因為輸入框的錯誤要顯示在輸入框的正下方,這樣 Address 組件內的輸入框就不能寫 noStyle,

如果設定 noStyle, 它的錯誤會向上傳遞:

但不寫 noStyle,它就會有 marginBottom,因此需去除包裹 Address 的 Form.Item 的 marginBottom,
<Form.Item label="源IP" style={{ marginBottom: 0 }}>
<Address namePathRoot="src" />
</Form.Item>
起始、結束 IP 中間的橫杠,為了垂直居中,在 Row 上設定了 line-height,

<Row style={{ lineHeight: "32px" }}>...</Row>
2.name 重名
<>
<Form.Item label="源IP">
<Address namePathRoot="src" />
</Form.Item>
<Form.Item label="目的IP">
<Address namePathRoot="dst" />
</Form.Item>
</>

上圖的 Address 組件在表單中出現兩次,如何保證 Form.Item 的 name 不重名?
有的同學把所有 Form.Item 的 name 作為 props 傳入組件,這種方法固然可行,但比較費事,更好的做法是利用 NamePath,
<Form.Item name={["a", "b", "c"]}>
<Input />
</Form.Item>
Form.Item 的 name 不僅可以是字串,也可以是字串陣列,即 NamePath,這樣表單項生成的 value 會是嵌套結構:
{
a: {
b: {
c: "xxxx";
}
}
}
我們只需要讓兩個 Address 實體 NamePath 的根不同,就可以做到區分,就像指定了不同的命名空間,
<>
<Form.Item label="源IP">
<Address namePathRoot="src" />
</Form.Item>
<Form.Item label="目的IP">
<Address namePathRoot="dst" />
</Form.Item>
</>
const Address = ({ namePathRoot }) => {
return (
<Row gutter={[8, 8]}>
<Col span={24}>
<Form.Item name={[namePathRoot, "type"]}>...</Form.Item>
</Col>
...
</Row>
);
};
有的同學問:實際專案中,后臺資料是扁平結構的怎么辦?
我的建議是:前臺在 action 層做資料轉換,
3.條件渲染

下拉框選擇不同,后面的表單項也會不同,遇到這種需求,有的同學使用 state 來實作:
const Address = () => {
const [option, setOption] = useState("ip");
return (
<>
<Form.Item name="type" onChange={setOption}>
<Select>
<Select.Option value="https://www.cnblogs.com/apolis/archive/2022/02/10/ip">IP地址</Select.Option>
<Select.Option value="https://www.cnblogs.com/apolis/archive/2022/02/10/iprange">IP地址段</Select.Option>
</Select>
</Form.Item>
{option === ip ? "IP地址表單項" : "IP地址段表單項"}
</>
);
};
實作條件渲染,這種做法需要在 3 處寫代碼:宣告 state、設定 state、根據 state 條件渲染,邏輯是割裂的,會給閱讀和維護代碼造成麻煩,更好的方式是采用 renderProp,
Form.Item 的 children 傳一個函式:
<Form.Item>
{form => {
const type = form.getFieldValue("type");
if (type === "ip") {
return "ip地址表單項";
} else {
return "ip地址段表單項";
}
}}
</Form.Item>
除此以外,還需要在 Form.Item 上說明,在什么情況下,需要執行 children 函式,
<Form.Item shouldUpdate>
{form => {...}}
</Form.Item>
以上代碼相當于設定 shouldUpdate={true},即每次 render,都重新渲染 children,顯然這樣性能不好,
<Form.Item
shouldUpdate={(preValue, curValue) => preValue.type !== curValue.type}
>
{form => {...}}
</Form.Item>
當表單值發生變化時,檢查 type 值是否改變,改變了才重新渲染 children,這種做法消除了性能問題,但還不是最好的做法,
<Form.Item dependencies={["type"]}>
{form => {...}}
</Form.Item>
上述 dependencies 表示:該表單項依賴 type 欄位,當 type 發生改變時,需要重新渲染 children,這種宣告式的寫法更清晰高效,
4.校驗
從經驗來看,能在各個專案中復用的校驗邏輯是 isXyz:
declare function isXyz(str: string): boolean;
如:
isIPv4isIPv4NetMaskIPisIPv4NetMaskIntisIPv6
這些原子校驗函式寫好后,我們利用函式式的寫法,通過 and、or、not 組合出更強大的校驗函式,如一個輸入框可以輸入 IPv4 也可以輸入 IPv6,那校驗函式就是:
or(isIPv4, isIPv6);
在校驗函式之上,我們再提供 buildRule 方法,將校驗函式轉成 antd 的 Rule,
const buildRule = (validate, errorMsg) => ({
validator: (_, value) =>
validate(value) ? Promise.resolve() : Promise.reject(errorMsg),
});
還有一種比較復雜的情況,是多個表單項的關聯校驗,如起始 IP 和結束 IP,結束 IP 的要大于起始 IP,
這個需求核心的校驗邏輯是判斷 IP 的大小:
(start, end) => ipToInt(end) > ipToInt(start);
這個函式能正常執行的前提是:起始 IP 和結束 IP 輸入框都輸入了合法的 IP,
<>
<Form.Item name="start" validateFirst rules={[Required, IPv4]}>
<Input placeholder="請輸入起始IP" />
</Form.Item>
<Form.Item
name="end"
dependencies={["start"]}
validateFirst
rules={[
Required,
IPv4,
buildMultiFieldsRule(
["start", "end"],
(start, end) => ipToInt(end) > ipToInt(start),
"結束IP需要大于起始IP"
),
]}
>
<Input placeholder="請輸入結束IP" />
</Form.Item>
</>
我們讓 Rule 有層層遞進的關系:
[
Required,
IPv4,
buildMultiFieldsRule(
["start", "end"],
(start, end) => ipToInt(end) > ipToInt(start),
"結束IP需要大于起始IP"
),
];
先校驗填了,再校驗是 IPv4,最后校驗大小合適,
同時,我們設定了 Form.Item 的 validateFirst,順序執行 Rule,有一個出錯了,后續的就不執行了,
在 buildMultiFieldsRule 方法中,封裝判斷各個 field 都填寫正常的邏輯:
const buildMultiFieldsRule =
(fields, validate, errorMsg) =>
({ getFieldValue, isFieldTouched, getFieldError }) => ({
validator: () => {
if (fields.some(f => !isFieldTouched(f) || getFieldError(f).length > 0)) {
return Promise.resolve();
} else {
return validate(...fields.map(getFieldValue))
? Promise.resolve()
: Promise.reject(errorMsg);
}
},
});
5.結束語
以上總結了專案中開發 Form 的好的實踐,這類總結經驗的文章,需要是活的,能隨著專案經驗積累不斷進化,而不是一寫下來就死了,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/423668.html
標籤:其他
上一篇:【JavaScript】筆記(6)--- BOM(open 與 close;彈出訊息框和確認框;將視窗設定為頂級視窗;history 物件;設定瀏覽器地址欄上的URL)
下一篇:【JavaScript】筆記(6)--- BOM(open 與 close;彈出訊息框和確認框;將視窗設定為頂級視窗;history 物件;設定瀏覽器地址欄上的URL)
