如何在React項(xiàng)目中優(yōu)雅的使用對(duì)話框
背景
對(duì)話框在前端開(kāi)發(fā)應(yīng)用中,是一種非常常用的界面模式。對(duì)話框作為一個(gè)獨(dú)立的窗口,常常被用于信息的展示,輸入信息,亦或者更多其他功能。但是項(xiàng)目的使用過(guò)程中,在某些場(chǎng)景下對(duì)話框用起來(lái)會(huì)有一些麻煩。例如:
場(chǎng)景一
如果想要在多個(gè)子組件(A、B)中控制一個(gè)對(duì)話框(C)的顯示影藏,這個(gè)對(duì)話框必須在共有的父組件(MySalesOrders)中進(jìn)行聲明。
場(chǎng)景二
如果需要給對(duì)話框(C)傳遞參數(shù),一般情況我們會(huì)使用 props 傳入,意味著狀態(tài)的管理必須也是子組件(A、B)的父組件或者更高一級(jí)進(jìn)行管理和維護(hù),但是其實(shí)這些狀態(tài)可能只需要在子組件 A 或者 B 中維護(hù)。這種情況下,我們就需要自定義事件,將狀態(tài)進(jìn)行回傳,比較麻煩。
const MySalesOrders: React.FC = () => {
const [visible, setVisible] = React.useState(false);
...
return (
<>
<A modalVisible={setVisible}/>
<B modalVisible={setVisible}/>
{
visible ? (
<C
...
/>
) : null
}
</>
);
}
const A: React.FC = (props) => {
...
return (
<>
<Button
onClick={() => {
props.modalVisible(...)
}}
/>
</>
);
}
const B: React.FC = (props) => {
...
return (
<>
<Button
onClick={() => {
props.modalVisible(...)
}}
/>
</>
);
}場(chǎng)景三
一個(gè)展示的對(duì)話框,對(duì)話框在不同的模塊可能只是提示文案不一樣,需要在不同的地方多次導(dǎo)入定義。例如系統(tǒng)中常用的提示成功、提示失敗的對(duì)話框。


我們通常會(huì)定義一個(gè)通用的組件,在父組件中定義,然后使用時(shí)喚起,但是如果我們需要在不同的頁(yè)面使用,我們就需要在不同的頁(yè)面組件中使用引入定義。
這些場(chǎng)景都是在我在實(shí)際開(kāi)發(fā)中都會(huì)用到的,并且我們開(kāi)發(fā)中也是基本都是這樣做的,雖然可以正常的使用。但是隱藏了幾個(gè)小的問(wèn)題。
問(wèn)題一:難以擴(kuò)展
如果和 MySalesOrders 同級(jí)的組件也要訪問(wèn)這個(gè)對(duì)話框(C)?又或者, MySalesOrders 下面的某個(gè)深層級(jí)的孫子組件也要能對(duì)話框(C)?前者意味著代碼需要重構(gòu),繼續(xù)提升狀態(tài)到 MySalesOrders 組件的父組件;后者意味著業(yè)務(wù)邏輯處理更復(fù)雜,需要通過(guò)層層的自定義事件回調(diào)來(lái)完成。
問(wèn)題二:維護(hù)問(wèn)題
同一個(gè)組件,需要在不同的地方多次的導(dǎo)入定義。在系統(tǒng)中增加了大量重復(fù)的代碼。代碼很快就會(huì)變得臃腫,且難以理解和維護(hù)。
問(wèn)題的本質(zhì)
對(duì)上訴問(wèn)題來(lái)說(shuō),本質(zhì)在于:在我們?nèi)粘5捻?xiàng)目中應(yīng)該哪里定義去對(duì)話框?又該如何和對(duì)話框進(jìn)行數(shù)據(jù)交互?
對(duì)話框的本質(zhì)
換一個(gè)角度再來(lái)看對(duì)話框,其實(shí)對(duì)話框本身是一個(gè)一對(duì)一或者一對(duì)多的 UI 模式。站在對(duì)話框的角度上,對(duì)話框本質(zhì)上是一個(gè)「獨(dú)立于其他界面的一個(gè)窗口,用于完成一個(gè)獨(dú)立的功能」。
如果從視覺(jué)角度出發(fā),你會(huì)發(fā)現(xiàn)在使用對(duì)話框的時(shí)候,你完全不會(huì)關(guān)心它是從哪個(gè)具體的組件中彈出來(lái)的,而只會(huì)關(guān)心對(duì)框本身的內(nèi)容。比如說(shuō),成功和失敗的對(duì)話框,它可能在 A 組件點(diǎn)出來(lái)的,也可能是 B 組件點(diǎn)出來(lái)的,亦或者其他組件點(diǎn)出來(lái)的。對(duì)話框的本質(zhì)就決定了它是獨(dú)立于各個(gè)組件之外的,
雖然很可能在一開(kāi)始這個(gè)對(duì)話框的實(shí)現(xiàn)和某個(gè)組件非常高的相關(guān)度,但是在整個(gè)應(yīng)用的不斷開(kāi)發(fā)和演進(jìn)過(guò)程中,是很可能不斷變化的。所以,在定義一個(gè)對(duì)話框的時(shí)候,其定位基本會(huì)等價(jià)于定義一個(gè)具有唯一 URL 路徑的頁(yè)面。只是前者由彈出層實(shí)現(xiàn),后者是頁(yè)面的切換。對(duì)于頁(yè)面級(jí)別的 UI 切換,我們很容易理解,就是定義全局的路由嘛。那么同樣的,如果我們以同樣的方式去思考對(duì)話框,其實(shí)就是將對(duì)話框全局化,然后通過(guò)一個(gè)全局的機(jī)制來(lái)管理這些對(duì)話框。這個(gè)過(guò)程和頁(yè)面 URL 的切換非常類似,那么我們就可以給每一個(gè)對(duì)話框定義一個(gè)全局唯一的 ID,然后通過(guò)這個(gè) ID 去顯示或者隱藏一個(gè)對(duì)話框,并且給它傳遞參數(shù)。
基于這樣的設(shè)想,我們可以嘗試使用全局的狀態(tài)管理來(lái)設(shè)置我們的對(duì)話框。
全局的狀態(tài)管理的對(duì)話框
整體的架構(gòu)


具體實(shí)現(xiàn)
代碼實(shí)現(xiàn)以 React 項(xiàng)目為主。
Redux - reducer 存儲(chǔ)
利用 Redux 的 store 去存儲(chǔ)每個(gè)對(duì)話框狀態(tài)和參數(shù)。
export default (state = {
hiding: {}
}, action: AnyAction) => {
switch (action.type) {
case CONSTANTS.modalShow:
return {
...state,
[action.payload.modalId]: action.payload.args || true,
hiding: {
...state.hiding,
[action.payload.modalId]: false,
},
};
case CONSTANTS.modalHide:
return action.payload.force
? {
...state,
[action.payload.modalId]: false,
hiding: { [action.payload.modalId]: false },
}
: { ...state, hiding: { [action.payload.modalId]: true } };
default:
return state;
}
};Redux - action 處理對(duì)話框的顯示隱藏
兩個(gè) action ,分別用來(lái)顯示和隱藏對(duì)話框。
export function showModal(modalId: string, args: any) {
return {
type: CONSTANTS.modalShow,
payload: {
modalId,
args,
},
};
}
export function hideModal(modalId: string, force: any) {
return {
type: CONSTANTS.modalHide,
payload: {
modalId,
force,
},
};
}Hook - useCommonModal
定義一個(gè) Hook,在其內(nèi)部封裝對(duì) Store 的操作,從而實(shí)現(xiàn)對(duì)話框狀態(tài)管理的邏輯重用。
export const useCommonModal = (modalId: string) => {
const dispatch = useDispatch();
const show = React.useCallback(
(args?: any) => new Promise((resolve) => {
commonmModalCallbacks[modalId] = resolve;
dispatch(showModal(modalId, { ...args }));
}),
[dispatch, modalId],
);
const resolve = React.useCallback(
(args?: any) => {
if (commonmModalCallbacks[modalId]) {
commonmModalCallbacks[modalId]({ ...args });
delete commonmModalCallbacks[modalId];
}
},
[modalId],
);
const hide = React.useCallback(
(force?: any) => {
dispatch(hideModal(modalId, force));
delete commonmModalCallbacks[modalId];
},
[dispatch, modalId],
);
const args = useSelector((s: any) => s?.modalReducer?.[modalId]);
const hiding = useSelector((s: any) => s?.modalReducer?.hiding?.[modalId]);
return React.useMemo(
() => ({ args, hiding, visible: !!args, show, hide, resolve }),
[args, hide, show, resolve, hiding],
);
};創(chuàng)建對(duì)話框-容器模塊
創(chuàng)建對(duì)話框時(shí),使用容器模式,它會(huì)在對(duì)話框不可見(jiàn)時(shí)直接返回 null,從而不渲染任何內(nèi)容;并且確保即使頁(yè)面上定義了 100 個(gè)對(duì)話框,也不會(huì)影響頁(yè)面性能。
export const createCommonModal = (modalId: string, Comp: any) => (props: any) => {
const { visible, args } = useCommonModal(modalId);
if (!visible) return null;
return (
<Comp
{...args}
{...props}
/>
);
};對(duì)話框返回值處理
往往在實(shí)際的使用中,可能在打開(kāi)對(duì)話框進(jìn)行操作之后需要將返回值返給調(diào)用者,有兩種方式可以供參考:
- callback:在傳入?yún)?shù)時(shí),傳入一個(gè)回調(diào)函數(shù),在進(jìn)行操作完成之后,進(jìn)行回調(diào)函數(shù)的調(diào)用。
const show = React.useCallback(
(args?: any) => new Promise((resolve) => {
commonmModalCallbacks[modalId] = resolve;
// args 中攜帶上 callback
dispatch(showModal(modalId, { ...args }));
}),
[dispatch, modalId],
);
// 調(diào)用
const modal = useCommonModal('modal-id');
modal.show({
callback() {}
});
// 對(duì)話框解析參數(shù)
const modalReducer = useSelector((state: any) => state.modalReducer);
const { callback } = modalReducer?.['modal-id'];
//對(duì)話框觸發(fā)
callback();- 將 show 和 resolve 兩個(gè)函數(shù)通過(guò) Promise 聯(lián)系起來(lái)。通過(guò)臨時(shí)變量,來(lái)存放 resolve 回調(diào)函數(shù),在對(duì)話框中去調(diào)用 modal.resolve 來(lái)進(jìn)行值的返回。
const resolve = React.useCallback(
(args?: any) => {
if (commonmModalCallbacks[modalId]) {
commonmModalCallbacks[modalId]({ ...args });
delete commonmModalCallbacks[modalId];
}
},
[modalId],
);
// 調(diào)用
const modal = useCommonModal('modal-id');
modal.show(args).then(result => {});
// 對(duì)話框觸發(fā)
const modal = useCommonModal('modal-id');
modal.resolve({ ... });運(yùn)行實(shí)例
總結(jié)
分享了一種使用對(duì)話框的實(shí)踐方式:利用全局狀態(tài)來(lái)管理對(duì)話框。解決上文提到的在使用對(duì)話框遇到的問(wèn)題。其核心思路在于從 UI 模式的角度出發(fā),把對(duì)話框也可當(dāng)做一個(gè)單獨(dú)的頁(yè)面,對(duì)話框的展示可用全局狀態(tài)來(lái)管理,因此,用全局的方式去管理對(duì)話框就是一種非常合理的方式。從而讓組件的語(yǔ)義更加清楚,代碼更容易理解和維護(hù)。
并且對(duì)于對(duì)話框定義位置,其實(shí)可以分場(chǎng)景來(lái)甄別。系統(tǒng)某一個(gè)模塊下的業(yè)務(wù)對(duì)話框,就只需要定義在這個(gè)業(yè)務(wù)模塊的根組件下就可以了。對(duì)于全局都可能使用的公共對(duì)話框,那就可以定義在整個(gè)系統(tǒng)的根組件,系統(tǒng)任何地方都可以使用。定義的位置決定了對(duì)話框組件輻射的廣度。
當(dāng)然這種全局的狀態(tài)管理對(duì)話框的方式,只是對(duì)原有的對(duì)話框操作做了一個(gè)增強(qiáng),解決了一些場(chǎng)景下的問(wèn)題,但是對(duì)于一些簡(jiǎn)單的對(duì)話框我們還是可以用常用的方式去管理和控制。兩者是可以并存的,大家可以根據(jù)場(chǎng)景來(lái)定義使用哪一種方式。
參考
- http://www.dbjr.com.cn/article/247814.htm
- time.geekbang.org/column/arti…
- http://www.dbjr.com.cn/article/247817.htm
- ant.design/components/…
- www.chkui.com/article/rea…
到此這篇關(guān)于如何在React項(xiàng)目中優(yōu)雅的使用對(duì)話框的文章就介紹到這了,更多相關(guān)React使用對(duì)話框內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Mobx實(shí)現(xiàn)React?應(yīng)用的狀態(tài)管理詳解
這篇文章主要為大家介紹了Mobx?實(shí)現(xiàn)?React?應(yīng)用的狀態(tài)管理,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
React onClick/onChange傳參(bind綁定)問(wèn)題
這篇文章主要介紹了React onClick/onChange傳參(bind綁定)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02
react-player實(shí)現(xiàn)視頻播放與自定義進(jìn)度條效果
本篇文章通過(guò)完整的代碼給大家介紹了react-player實(shí)現(xiàn)視頻播放與自定義進(jìn)度條效果,代碼簡(jiǎn)單易懂,對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2022-01-01
React使用context進(jìn)行跨級(jí)組件數(shù)據(jù)傳遞
這篇文章給大家介紹了React使用context進(jìn)行跨級(jí)組件數(shù)據(jù)傳遞的方法步驟,文中通過(guò)代碼示例給大家介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)React context組件數(shù)據(jù)傳遞有一定的幫助,感興趣的小伙伴跟著小編一起來(lái)學(xué)習(xí)吧2024-01-01
create-react-app項(xiàng)目配置全解析
這篇文章主要為大家介紹了create-react-app項(xiàng)目配置全解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06
通過(guò)React-Native實(shí)現(xiàn)自定義橫向滑動(dòng)進(jìn)度條的 ScrollView組件
開(kāi)發(fā)一個(gè)首頁(yè)擺放菜單入口的ScrollView可滑動(dòng)組件,允許自定義橫向滑動(dòng)進(jìn)度條,且內(nèi)部渲染的菜單內(nèi)容支持自定義展示的行數(shù)和列數(shù),在內(nèi)容超出屏幕后,渲染順序?yàn)榭v向由上至下依次排列,對(duì)React Native橫向滑動(dòng)進(jìn)度條相關(guān)知識(shí)感興趣的朋友一起看看吧2024-02-02

