我們是袋鼠云數堆疊 UED 團隊,致力于打造優秀的一站式資料中臺產品,我們始終保持工匠精神,探索前端道路,為社區積累并傳播經驗價值,,
本文作者:修能
以下內容充滿個人觀點,? ヽ(`Д′)? ┻━┻
前言
基于分布表單的需求,在中后臺管理中是一個非常常見的需求,通常具有如下布局:
其中,自定義需求度從高到低為,正文 > 按鈕區 > 步驟條,
雖然布局類似,但是實作的方式卻是天差地別,這里就探究一下究竟怎么樣實作可以兼具代碼的可維護性和可讀性呢?
指出問題
Container
我們這里,以「指標-資料模型」的代碼為例,
首先先來看看資料模型這里的代碼是如何實作的?
export default () => {
...
return (
<>
<header>
<Steps current={current}>
{['tab1', 'tab2', 'tab3', 'tab4', 'tab5'].map(
(title, index) => (
<Step key={index} title={title} />
)
)}
</Steps>
</header>
<Spin>
{stepRender(current, {
childRef,
modelDetail,
globalStep: globalStep.current,
mode,
isModelTypeDisabled,
setModelDetail,
setDisabled,
onModelNameChange: handleModelNameChange,
})}
<Modal>...</Modal>
</Spin>
<footer>
{current === EnumModifyStep.tab1 ? (
<Button
onClick={() => router.push('/url')}
>
取消
</Button>
) : null}
...
</footer>
</>
)
}
這是資料模型編輯頁面 Steps
所在的容器組件的 DOM 部分的代碼,
可以看出來,設計者的思路是比較明確的,通過 header,content,和 footer 進行分層, 增加代碼的可讀性,
在 header 中,通過宣告 title 陣列的方式創建 Steps 的方式簡潔又不失可讀性,
在 content 中,有幾個問題的存在:
- 既然 header 和 footer 都有語意化的標簽強化可讀性,我認為這里其實也可以添加語意化的標簽強化可讀性,譬如
main
或者section
,當然同時還需要考慮會不會造成過深的層級, stepRender
函式的實作把一大堆params
傳到子組件是否合適,- 為何 content 區域內,會存在 Modal?對于沒有設定
getPopupContainer
的 Modal 來說,其會通過createPortal
在 body 上創建,那么在這里不論是寫在 content 還是 header,都不會影響它的渲染,所以我推薦把 Modal 寫到最角落里,不影響可讀性, - 在 footer 中,通過
current === 步驟
的方式去定義按鈕,我認為這種方式會使代碼顯得較為冗余,
Tab1
我們這里以指標相關代碼為例,以簡見深,以小見大
export default (props) => {
...
const { cref, modelDetail, mode, onModelNameChange } = props;
useImperativeHandle(cref, () => {
return {
validate: () => {...},
getValue: () => {...},
}
});
useEffect(() => {
setFieldsValue({
a: modelDetail.a,
b: modelDetail.b,
c: modelDetail.c,
});
}, [modelDetail]);
return (
<Form>
<Row gutter={40}>
<Col span={12}>
...
</Col>
<Col span={12}>
...
</Col>
</Row>
<Row gutter={40}>
<Col span={12}>
...
</Col>
</Row>
</Form>
)
}
這里我想指出的第一個問題是,ref 的使用,由于 ref 無法在 props 中傳遞,需要通過 forwardRef 才能拿到,然而這里通過 cref 這種比較 hack 的方式進行一個操作,我認為這是一個不推薦的做法,如果需要拿 ref 我建議是老老實實通過 forwardRef 拿,
其次是 Row 和 Col 的使用,并不是說 Col 達到 24 之后就需要再寫一個 Row,你可以繼續寫的呀,童鞋!
這里需要提出來的一個論點是,每一個子組件里去寫 Form 的方式好(即上面的這種寫法),還是總體寫一個 Form 的方式更好?個人認為前者存在的問題如下:
- 由于子組件寫 Form,但是提交(或下一步)按鈕在外面,那么必然需要用 ref 拿到子組件的實體,并呼叫相關方法,(上面是 validate 和 getValue 分別對應下一步和上一步呼叫)
- 沒有遵循 single source of truth(單一事實來源)
- 如果多層級結構,例如 RelationTableSelect 的話,每一層都有填寫內容,那么需要大量 Form + ref,降低可維護性,
除此之外,由于基礎資訊比較簡單,所以不存在 props 層層往下傳遞的問題,但是復雜組件就會存在層層往下傳遞的情況,那么就涉及到是否需要 context 的問題了,當然,我推薦是需要 context 的,
Tab2
這里再看一眼第二步關聯表的設計
interface ITab2Props {
cref: IModifyRef;
modelDetail?: Partial<IModelDetail>;
mode: any;
globalStep: number;
updateModelDetail: Function;
setDisabled?: Function;
}
const RelationTableSelect = (props: ITab2Props) => {}
首先,這里需要支持的一個設計思路是,通常情況下,切忌直接把 dispatch 傳遞給子組件,
關聯表這里的設計由于層級嵌套很深,子組件非常多,導致updateModelDetail
不斷往下傳遞,你完全不知道哪層組件在什么情況下會去修改這個值!!! 這對于 SSOT 來說,是毀滅性的打擊,
再加上 modelDetail
是一個很復雜的資料,對于可維護性來說,屬于是力中暴力地打擊了,
解決問題
綜上,我們設計分布表單的時候,需要規避以上的問題,遵循如下原則:
- SSOT
- 可維護性
- 可擴展性
首先實作如下組件:
<StepsForm
current={current}
onChange={setCurrent}
titles={['tab1', 'tab2', 'tab3', 'tab4', 'tab5']}
/>
這一塊代碼比較簡單,無非就是投傳幾個值到對應的組件中去,
接下來考慮底部按鈕的可擴展性,
通過 submitter
屬性支持定制按鈕的互動屬性,
<StepsForm
current={current}
onChange={setCurrent}
submitter={[
{
[StepsForm.PREV]: {
children: '取消',
},
},
null,
{
[PREVIEW]: {
danger: true,
children: '預覽',
},
},
]}
/>
接下來要解決按鈕的事件,這里有兩種方案,一種是將事件掛載在 Container 上(即這里的 StepsForm 組件),通過諸如 onCancel,onSubmit,onPrev
等方式進行反饋,
我認為這種方式不夠好,原因有如下幾點
- 通常我們會把子組件提出來,不會和 Container 組件寫在一起,這就會使得我們需要在不同的組件中寫按鈕的互動邏輯和 UI 邏輯,存在隔離感
- 有時候我們需要把 Select.Option 相關的資料一起放到資料里給到服務端,這種方式互動需要把 Option 的資料提取到 Container 中
- 需要通過 ref 去子組件獲取值
而目前我考慮通過事件訂閱對按鈕事件觸發,通過 useEffect 監聽事件,但是這種方式的缺點如下:
- 不夠直觀,和我們通常來說的組件開發有一定相悖的思路
除了以上兩種方式以外,其實還有一種方式,即通過實作 Children 組件,將 Children 組件作為 StepsForm 的子組件,從而使得將每一步相關的 title 和 onSubmit 等方式都掛載在 Children 組件上,即 ant-design-pro 中的 StepsForm
的實作方式,我認為這種方式的優點在于直觀,不割裂,缺點在于如下:
- 為了獲取 title 不得不先渲染子組件,從而導致 DOM 先渲染出來,然后通過 active 判斷表單是否渲染,
- 導致子組件無法通過
useEffect
獲取資料
其中第二點我認為是無法忍受的,這和開發組件的思路完全相悖,故摒棄這種方式
暫時考慮不清楚是第一種好還是第二種好,
這里先考慮實作第二種方式后組件書寫的效果:
export function () {
...
StepsForm.useFooterEffect(
({ prev }) => {
prev(() => {});
},
[StepsForm.PREV],
);
StepsForm.useFooterEffect(() => {
message.info('預覽')
}, [PREVIEW]);
StepsForm.useFooterEffect(
({ next }) => {
next(() => {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 1000);
});
});
},
[StepsForm.NEXT],
);
return (
...
)
}
hook 的實作方式也比較簡單,基于事件訂閱,結合每一個按鈕都賦予一個唯一值,
實作按鈕互動觸發后,通過事件分發,觸發當前渲染的組件中的監聽 hook,
總結
本文意在探索分步表單的最佳實踐,防止不同的同學在開發該型別的需求會寫出五花八門的代碼,從而導致降低可維護性,
本文提到的解決方案也不認為是最佳實踐,其中不同的方法經過分析都存在優點和缺點,在實際的開發程序中,仍然需要根據具體的需求進行調整,
但是基于分步表單的特性和使用場景,總結出適用大部分情況下的方法論是有必要的,
最后
歡迎關注【袋鼠云數堆疊UED團隊】~
袋鼠云數堆疊UED團隊持續為廣大開發者分享技術成果,相繼參與開源了歡迎star
- 大資料分布式任務調度系統——Taier
- 輕量級的 Web IDE UI 框架——Molecule
- 針對大資料領域的 SQL Parser 專案——dt-sql-parser
- 袋鼠云數堆疊前端團隊代碼評審工程實踐檔案——code-review-practices
- 一個速度更快、配置更靈活、使用更簡單的模塊打包器——ko
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/557051.html
標籤:Html/Css
下一篇:返回列表