網站首頁 編程語言 正文
1. 前話
提問:ToB中臺類系統的后端開發主要做啥?
???♂?:CRUD
再次提問:那前端開發呢?
???♂?:增刪查改
開個玩笑哈啊哈哈哈??????
雖然沒有具體數據統計,但作者仍主觀地認為中臺類的系統的前端內容至少一半都是增刪查改??????,其對應的前端頁面類型就是列表頁面和表單頁面。
對于列表頁面的通用實現,如果讀者有看過《React通用解決方案——組件數據請求》一文應該會根據自身實際業務場景得出較好的解決方案,提升實際業務的列表頁面的開發效率。
而對于表單頁面,又該如何實現以作為通用開發模版進行提效?大致有兩種:
- 一種「配置表單」,也就是定義表單DSL,通過JSON配置生成表單頁面,這也是業界低代碼平臺的表單實現。優點是顯而易見的,配置簡單,快速實現。缺點是靈活性受限于DSL的完整性,對于特殊場景需進行表單組件底層實現的定制化。
- 另一種是「原生表單」,也就是直接使用表單組件。其優缺陷大致與「配置表單」相反。
本篇由于主題定義就不講解表單的通用實現只分享表單的通用呈現哈???♂????♂????♂?下面開始正文。
2. 正文
常見的表單的呈現有兩種模式,分別是頁面和浮層
。
首先是「頁面表單」,也就是以頁面的形式呈現表單。示例代碼如下:
const FormPage: React.FC = () => { const [form] = useForm(); const handleSubmit = useCallback((value) => { // TODO 表單提交邏輯 console.log(value); }, []); return ( <div className="test-page"> <h2>新建用戶</h2> <Form form={form} onSubmit={handleSubmit} layout="inline"> <Form.Item field="name" label="名稱" rules={[ { required: true, message: "請輸入名稱", }, ]} > <Input placeholder="請輸入" /> </Form.Item> <Form.Item> <Button htmlType="submit">提交</Button> </Form.Item> </Form> </div> ); };
瀏覽器展現如下:
某一天,產品為了優化交互體驗改成「以彈窗呈現表單」,這時便會用到表單的另一種呈現——「浮層表單」。在原「頁面表單」的實現中進行修改,修改后的示例代碼如下:
const FormPage: React.FC = () => { const [form] = useForm(); const visible = useBoolean(false); const handleSubmit = useCallback(() => { form.validate((error, value) => { if (error) { return; } // TODO 表單提交邏輯 console.log(value); visible.setFalse(); }); }, []); return ( <div className="test-page"> <h2>新建用戶</h2> <Button onClick={visible.setTrue}>點擊新建</Button> <Modal visible={visible.state} title="新建用戶" okText="提交" onOk={handleSubmit} onCancel={visible.setFalse} > <Form form={form} layout="inline"> <Form.Item field="name" label="名稱" rules={[ { required: true, message: "請輸入名稱", }, ]} > <Input placeholder="請輸入" /> </Form.Item> </Form> </Modal> </div> ); };
瀏覽器展現如下:
某一天,產品提了個新需求,另一個「用戶新建頁面表單」。某一天,產品提了個新需求,另一個「用戶新建彈窗表單」。某一天,產品提了個新需求,另一個「用戶新建抽屜表單」。某一天。。。
這時RD糾結了,為了快速完成需求直接是拷貝一個新的「FormPage」組件完成交付最終的結局肯定就是「禿頭」,亟需總結個通用的解決方案應對表單不同呈現的場景的實現。
切入點是對表單和呈現進行拆分,避免表單和呈現的耦合
。
那該如何拆分?我們先明確下表單和呈現各自的關注點,表單主要關注表單值和表單動作,而呈現主要關注自身的樣式
。如果表單的動作需要呈現進行觸發,例如彈窗的確定按鈕觸發表單的提交動作呢?這就需要表單與呈現之間需存在連接的橋梁
。
作者根據這個思路最終拆分的結果是,實現了個「表單容器」。「表單」+「表單容器」,讓表單的實現不關注呈現,從而實現表單的復用,提升了開發效率。
2.1 表單容器定義
表單容器的定義基于浮層容器拓展,定義如下:
- 表單容器支持各種呈現(彈窗和抽屜等);
- 表單容器只關注浮層的標題、顯隱狀態和顯隱狀態變更處理邏輯,不關注浮層內容;
- 表單容器組件提供接口控制浮層容器的標題和顯隱狀態;
- 任何內容被表單容器包裹即可獲得浮層的能力;
- 表單容器提供向浮層內容透傳屬性的能力,內置透傳Form實例、表單模式和只讀狀態的屬性;
- 表單容器的浮層確認邏輯自動觸發Form實例的提交邏輯
基于上面的定義實現的TS類型定義如下:
import React from "react"; import { ModalProps, DrawerProps, FormInstance } from "@arco-design/web-react"; import { EFormMode, IBaseFormProps } from "@/hooks/use-common-form"; export type IFormWrapperBaseProps = { /** 標題 */ title?: React.ReactNode; }; export type IFormWrapperOpenProps<T = any, P = {}> = IFormWrapperBaseProps & { /** 表單模式 */ mode?: EFormMode; /** 表單值 */ value?: T; /** 內容屬性 */ props?: P; }; export type IFormWrapperProps<T = any, P = {}> = IFormWrapperBaseProps & { /** 表單彈窗提交回調函數 */ onSubmit?: ( /** 提交表單值 */ formValue: T, /** 當前表單值 */ currentValue: T, /** 表單模式 */ formMode: EFormMode, /** 內容屬性 */ componentProps?: P ) => Promise<void>; /** 表單彈窗提交回調函數 */ onOk?: (result: any, componentProps?: P) => void | Promise<void>; /** 表單彈窗提交回調函數 */ onCancel?: () => void; /** 內容屬性 */ componentProps?: P; }; export type IFormWrappedModalProps<T = any, P = {}> = Omit< ModalProps, "onOk" | "onCancel" > & IFormWrapperProps<T, P>; export type IFormWrappedDrawerProps<T = any, P = {}> = Omit< DrawerProps, "onOk" | "onCancel" > & IFormWrapperProps<T, P> & { operation?: React.ReactNode; }; export type IFormWrapperRef<T = any, P = {}> = { /** 表單彈窗打開接口 */ open: (openProps?: IFormWrapperOpenProps<T, P>) => void; /** 表單彈窗關閉接口 */ close: () => void; }; export type IWithFormWrapperOptions<T = any, P = {}> = { /** 默認值 */ defaultValue: T; /** 默認屬性 */ defaultProps?: Partial<IFormWrapperProps<T, P>>; }; export type IWithFormWrapperProps<T = any, P = {}> = IBaseFormProps & { /** 表單實例 */ form: FormInstance<T>; } & P;
2.2 表單容器定義實現
基于上面的表單容器定義,我們這里實現一個Hook,實現代碼如下:
/** * 表單容器Hook * @param ref 浮層實例 * @param wrapperProps 浮層屬性 * @param defaultValue 默認值 * @returns */ export function useFormWrapper<T = any, P = {}>( ref: ForwardedRef<IFormWrapperRef<T, P>>, wrapperProps: IFormWrapperProps<T, P>, defaultValue: T, ) { const [form] = Form.useForm(); const visible = useBoolean(false); const loading = useBoolean(false); const [title, setTitle] = useState<React.ReactNode>(); const [componentProps, setComponentProps] = useState<P>(); const [value, setValue] = useState(defaultValue); const [mode, setMode] = useState(EFormMode.view); // 計算是否只讀 const readOnly = useReadOnly(mode); // 提交處理邏輯 const onOk = async () => { loading.setTrue(); const targetComponentProps = wrapperProps.componentProps ?? componentProps; try { // 校驗表單 const formValue = await form.validate(); // 提交表單 const result = await wrapperProps?.onSubmit?.( formValue, value, mode, targetComponentProps, ); await wrapperProps.onOk?.(result, targetComponentProps); visible.setFalse(); } catch (err) { console.error(err); } finally { loading.setFalse(); } }; // 取消處理邏輯 const onCancel = () => { wrapperProps.onCancel?.(); visible.setFalse(); }; // 實例掛載表單操作接口 useImperativeHandle( ref, (): IFormWrapperRef<T, P> => ({ open: openProps => { const { title: newTitle, mode: newMode = EFormMode.view, value: newValue = defaultValue, } = openProps ?? {}; setMode(newMode); setTitle(newTitle); setValue(newValue); form.resetFields(); form.setFieldsValue(newValue); visible.setTrue(); }, close: onCancel, }), ); // 初始化表單默認值 useEffect(() => { form.setFieldsValue(defaultValue); }, []); const ret = [ { visible, loading, title, componentProps, form, value, mode, readOnly, }, { onOk, onCancel, setTitle, setComponentProps, setValue, setMode, }, ] as const; return ret; }
2.3 表單容器呈現實現
表單容器的呈現有多種,常見的為彈窗和抽屜。下面我使用Arco對應組件進行呈現實現 ?? 。
2.3.1 彈窗表單容器
/** * 表單彈窗容器 * @param options 表單配置 * @returns */ function withModal<T = any, P = {}>(options: IWithFormWrapperOptions<T, P>) { const { defaultValue, defaultProps } = options; return function (Component: any) { const WrappedComponent = ( props: IFormWrappedModalProps<T, P>, ref: ForwardedRef<IFormWrapperRef<T, P>>, ) => { const wrapperProps = { ...defaultProps, ...props, }; const { componentProps, title, visible, okButtonProps, cancelButtonProps, okText = 'Submit', cancelText = 'Cancel', maskClosable = false, unmountOnExit = true, ...restProps } = wrapperProps; const [ { form, mode, readOnly, visible: currentVisible, title: currentTitle, componentProps: currentComponentProps, }, { onOk, onCancel }, ] = useFormWrapper<T, P>(ref, wrapperProps, defaultValue); return ( <Modal {...restProps} maskClosable={maskClosable} visible={visible ?? currentVisible.state} onOk={onOk} okText={okText} okButtonProps={{ hidden: readOnly, ...okButtonProps, }} onCancel={onCancel} cancelText={cancelText} cancelButtonProps={{ hidden: readOnly, ...cancelButtonProps, }} title={title ?? currentTitle} unmountOnExit={unmountOnExit}> {React.createElement(Component, { form, mode, readOnly, ...(componentProps ?? currentComponentProps), })} </Modal> ); }; WrappedComponent.displayName = `FormWrapper.withModal(${getDisplayName( Component, )})`; const ForwardedComponent = forwardRef< IFormWrapperRef<T, P>, IFormWrappedModalProps<T, P> >(WrappedComponent); return ForwardedComponent; }; }
2.3.1 抽屜表單容器
/** * 表單抽屜容器 * @param options 表單配置 * @returns */ function withDrawer<T = any, P = {}>(options: IWithFormWrapperOptions<T, P>) { const { defaultValue, defaultProps } = options; return function (Component: any) { const WrappedComponent = ( props: IFormWrappedDrawerProps<T, P>, ref: ForwardedRef<IFormWrapperRef<T, P>>, ) => { const wrapperProps = { ...defaultProps, ...props, }; const { title, visible, componentProps, okText = 'Submit', okButtonProps, cancelText = 'Cancel', cancelButtonProps, maskClosable = false, unmountOnExit = true, operation, ...restProps } = wrapperProps; const [ { form, mode, readOnly, loading, visible: currentVisible, title: currentTitle, componentProps: currentComponentProps, }, { onOk, onCancel }, ] = useFormWrapper<T, P>(ref, wrapperProps, defaultValue); const footerNode = useMemo( () => ( <div style={{ textAlign: 'right' }}> {operation} {!readOnly && ( <> <Button type="default" onClick={onCancel} {...cancelButtonProps}> {cancelText} </Button> <Button type="primary" loading={loading.state} onClick={onOk} style={{ marginLeft: '8px' }} {...okButtonProps}> {okText} </Button> </> )} </div> ), [ loading.state, onOk, onCancel, okText, cancelText, readOnly, okButtonProps, cancelButtonProps, ], ); const showFooter = useMemo( () => !(readOnly && !operation), [readOnly, operation], ); return ( <Drawer {...restProps} maskClosable={maskClosable} visible={visible ?? currentVisible.state} title={title ?? currentTitle} footer={showFooter ? footerNode : null} unmountOnExit={unmountOnExit} onCancel={onCancel}> {React.createElement(Component, { form, mode, readOnly, ...(componentProps ?? currentComponentProps), })} </Drawer> ); }; WrappedComponent.displayName = `FormWrapper.withDrawer(${getDisplayName( Component, )})`; const ForwardedComponent = forwardRef< IFormWrapperRef<T, P>, IFormWrappedDrawerProps<T, P> >(WrappedComponent); return ForwardedComponent; }; }
2.4 表單容器用例
對于上面的代碼示例我們進行以下改造,將頁面的表單抽離成單獨的表單組件,代碼如下:
type IUserFormValue = { name?: string; }; const UserForm: React.FC<IWithFormWrapperProps<IUserFormValue>> = ({ form, }) => { return ( <Form form={form} layout="inline"> <Form.Item field="name" label="名稱" rules={[ { required: true, message: "請輸入名稱", }, ]} > <Input placeholder="請輸入" /> </Form.Item> </Form> ); };
下面我們就可以使用上面實現的表單容器進行包裹生成彈窗表單組件,代碼如下:
const submitForm = async (formValue: IUserFormValue) => { // TODO 表單提交邏輯 console.log(formValue); }; const UserFormModal = FormWrapper.withModal<IUserFormValue>({ defaultValue: { name: "", }, defaultProps: { onSubmit: submitForm, }, })(UserForm);
在實際業務場景中,彈窗表單和頁面表單都能復用一個表單組件,代碼如下:
const FormPage: React.FC = () => { const [form] = useForm<IUserFormValue>(); const userFormRef = useRef<IFormWrapperRef<IUserFormValue>>(null); const handleSubmit = useCallback(() => { form.validate((error, formValue) => { if (error || !formValue) { return; } submitForm(formValue); }); }, []); return ( <div className="test-page"> <h2>新建用戶</h2> {/* 頁面表單 */} <UserForm form={form} /> <Button onClick={handleSubmit}>頁面新建</Button> {/* 彈窗表單 */} <UserFormModal ref={userFormRef} /> <Button onClick={() => { userFormRef.current?.open({ title: "新建用戶", mode: EFormMode.add, value: { name: "", }, }); }} > 彈窗新建 </Button> </div> ); };
3. 最后
表單容器的基于浮層容器進行實現,作者在實際業務開發過程中也廣泛應用到了這兩類容器,本篇也只是對簡單表單場景進行實現,更為復雜的表單場景可以在評論區交流哈。
原文鏈接:https://juejin.cn/post/7089708929466236936
相關推薦
- 2023-01-13 Android?Parcleable接口的調用源碼層分析_Android
- 2022-12-09 C++實現雙向起泡排序算法_C 語言
- 2022-07-23 .Net創建型設計模式之工廠方法模式(Factory?Method)_基礎應用
- 2022-11-15 Flutter有無狀態類與State及生命周期詳細介紹_Android
- 2023-06-21 C#高級靜態語言效率利器之泛型詳解_C#教程
- 2023-01-26 Python上下文管理器深入講解_python
- 2022-04-20 深入理解go緩存庫freecache的使用_Golang
- 2022-04-19 idea如何解決jar包沖突
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支