您好,登錄后才能下訂單哦!
本篇內容介紹了“怎么編寫插件機制優化基于Antd Table封裝表格的混亂代碼”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
前言
最近在一個業務需求中,我通過在 Antd Table 提供的回調函數等機制中編寫代碼,實現了這些功能:
每個層級縮進指示線
遠程懶加載子節點
每個層級支持分頁
最后實現的效果大概是這樣的:
最終效果
這篇文章我想聊聊我在這個需求中,對代碼解耦,為組件編寫插件機制的一些思考。
重構思路
隨著編寫功能的增多,邏輯被耦合在 Antd Table 的各個回調函數之中,
指引線的邏輯分散在 rewriteColumns, components中。
分頁的邏輯被分散在 rewriteColumns 和 rewriteTree 中。
加載更多的邏輯被分散在 rewriteTree 和 onExpand 中
至此,組件的代碼行數也已經來到了 300 行,大概看一下代碼的結構,已經是比較混亂了:
export const TreeTable = rawProps => { function rewriteTree() { // ?加載更多邏輯 // ? 分頁邏輯 } function rewriteColumns() { // ? 分頁邏輯 // ? 縮進線邏輯 } const components = { // ? 縮進線邏輯 }; const onExpand = async (expanded, record) => { // ? 加載更多邏輯 }; return <Table />; };
這時候缺點就暴露出來了,當我想要改動或者刪減其中一個功能的時候變得異常痛苦,經常在各個函數之間跳轉查找。
有沒有一種機制,可以讓代碼按照功能點聚合,而不是散落在各個函數中?
// ? 分頁邏輯 const usePaginationPlugin = () => {}; // ? 加載更多邏輯 const useLazyloadPlugin = () => {}; // ? 縮進線邏輯 const useIndentLinePlugin = () => {}; export const TreeTable = rawProps => { usePaginationPlugin(); useLazyloadPlugin(); useIndentLinePlugin(); return <Table />; };
沒錯,就是很像 VueCompositionAPI 和 React Hook 在邏輯解耦方面所做的改進,但是在這個回調函數的寫法形態下,好像不太容易做到?
這時候,我回想到社區中一些開源框架提供的插件機制,好像就可以在不深入源碼的情況下注入各個回調時機的用戶邏輯。
比如 Vite 的插件[1]、Webpack 的插件[2] 甚至大家很熟悉的 Vue.use()[3],它們本質上就是對外暴露出一些內部的時機和屬性,讓用戶去寫一些代碼來介入框架運行的各個時機之中。
那么,我們是否可以考慮把「處理每個節點、column、每次 onExpand」 的時機暴露出去,這樣讓用戶也可以介入這些流程,去改寫一些屬性,調用一些內部方法,以此實現上面的幾個功能呢?
我們設計插件機制,想要實現這兩個目標:
鴻蒙官方戰略合作共建——HarmonyOS技術社區
邏輯解耦,把每個小功能的代碼整合到插件文件中去,不和組件耦合起來,增加可維護性。
用戶共建,內部使用的話方便同事共建,開源后方便社區共建,當然這要求你編寫的插件機制足夠完善,文檔足夠友好。
不過插件也會帶來一些缺點,設計一套完善的插件機制也是非常復雜的,像 Webpack、Rollup、Redux 的插件機制都有設計的非常精良的地方可以參考學習。
接下來,我會試著實現的一個最簡化版的插件系統。
源碼
首先,設計一下插件的接口:
export interface TreeTablePlugin<T = any> { (props: ResolvedProps, context: TreeTablePluginContext): { /** * 可以訪問到每一個 column 并修改 */ onColumn?(column: ColumnProps<T>): void; /** * 可以訪問到每一個節點數據 * 在初始化或者新增子節點以后都會執行 */ onRecord?(record): void; /** * 節點展開的回調函數 */ onExpand?(expanded, record): void; /** * 自定義 Table 組件 */ components?: TableProps<T>['components']; }; } export interface TreeTablePluginContext { forceUpdate: React.DispatchWithoutAction; replaceChildList(record, childList): void; expandedRowKeys: TableProps<any>['expandedRowKeys']; setExpandedRowKeys: (v: string[] | number[] | undefined) => void; }
我把插件設計成一個函數,這樣每次執行都可以拿到最新的 props 和 context。
context 其實就是組件內一些依賴上下文的工具函數等等,比如 forceUpdate, replaceChildList 等函數都可以掛在上面。
接下來,由于插件可能有多個,而且內部可能會有一些解析流程,所以我設計一個運行插件的 hook 函數 usePluginContainer:
export const usePluginContainer = ( props: ResolvedProps, context: TreeTablePluginContext ) => { const { plugins: rawPlugins } = props; const plugins = rawPlugins.map(usePlugin => usePlugin?.(props, context)); const container = { onColumn(column: ColumnProps<any>) { for (const plugin of plugins) { plugin?.onColumn?.(column); } }, onRecord(record, parentRecord, level) { for (const plugin of plugins) { plugin?.onRecord?.(record, parentRecord, level); } }, onExpand(expanded, record) { for (const plugin of plugins) { plugin?.onExpand?.(expanded, record); } }, /** * 暫時只做 components 的 deepmerge * 不處理自定義組件的沖突 后定義的 Cell 會覆蓋前者 */ mergeComponents() { let components: TableProps<any>['components'] = {}; for (const plugin of plugins) { components = deepmerge.all([ components, plugin.components || {}, props.components || {}, ]); } return components; }, }; return container; };
目前的流程很簡單,只是把每個 plugin 函數調用一下,然后提供對外的包裝接口。mergeComponent 使用deepmerge[4] 這個庫來合并用戶傳入的 components 和 插件中的 components,暫時不做沖突處理。
接著就可以在組件中調用這個函數,生成 pluginContainer:
export const TreeTable = React.forwardRef((props, ref) => { const [_, forceUpdate] = useReducer((x) => x + 1, 0) const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]) const pluginContext = { forceUpdate, replaceChildList, expandedRowKeys, setExpandedRowKeys } // 對外暴露工具方法給用戶使用 useImperativeHandle(ref, () => ({ replaceChildList, setNodeLoading, })); // 這里拿到了 pluginContainer const pluginContainer = usePluginContainer( { ...props, plugins: [usePaginationPlugin, useLazyloadPlugin, useIndentLinePlugin], }, pluginContext ); })
之后,在各個流程的相應位置,都通過 pluginContainer 執行相應的鉤子函數即可:
export const TreeTable = React.forwardRef((props, ref) => { // 省略上一部分代碼…… // 這里拿到了 pluginContainer const pluginContainer = usePluginContainer( { ...props, plugins: [usePaginationPlugin, useLazyloadPlugin, useIndentLinePlugin], }, pluginContext ); // 遞歸遍歷整個數據 調用鉤子 const rewriteTree = ({ dataSource, // 在動態追加子樹節點的時候 需要手動傳入 parent 引用 parentNode = null, }) => { pluginContainer.onRecord(parentNode); traverseTree(dataSource, childrenColumnName, (node, parent, level) => { // 這里執行插件的 onRecord 鉤子 pluginContainer.onRecord(node, parent, level); }); } const rewrittenColumns = columns.map(rawColumn => { // 這里把淺拷貝過后的 column 暴露出去 // 防止污染原始值 const column = Object.assign({}, rawColumn); pluginContainer.onColumn(column); return column; }); const onExpand = async (expanded, record) => { // 這里執行插件的 onExpand 鉤子 pluginContainer.onExpand(expanded, record); }; // 這里獲取合并后的 components 傳遞給 Table const components = pluginContainer.mergeComponents() });
之后,我們就可以把之前分頁相關的邏輯直接抽象成 usePaginationPlugin:
export const usePaginationPlugin: TreeTablePlugin = ( props: ResolvedProps, context: TreeTablePluginContext ) => { const { forceUpdate, replaceChildList } = context; const { childrenPagination, childrenColumnName, rowKey, indentLineDataIndex, } = props; const handlePagination = node => { // 先加入渲染分頁器占位節點 }; const rewritePaginationRender = column => { // 改寫 column 的 render // 渲染分頁器 }; return { onRecord: handlePagination, onColumn: rewritePaginationRender, }; };
也許機智的你已經發現,這里的插件是以 use 開頭的,這是自定義 hook的標志。
沒錯,它既是一個插件,同時也是一個 自定義 Hook。所以你可以使用 React Hook 的一切能力,同時也可以在插件中引入各種社區的第三方 Hook 來加強能力。
這是因為我們是在 usePluginContainer 中通過函數調用執行各個 usePlugin,完全符合 React Hook 的調用規則。
而懶加載節點相關的邏輯也可以抽象成 useLazyloadPlugin:
export const useLazyloadPlugin: TreeTablePlugin = ( props: ResolvedProps, context: TreeTablePluginContext ) => { const { childrenColumnName, rowKey, hasNextKey, onLoadMore } = props; const { replaceChildList, expandedRowKeys, setExpandedRowKeys } = context; // 處理懶加載占位節點邏輯 const handleNextLevelLoader = node => {}; const onExpand = async (expanded, record) => { if (expanded && record[hasNextKey] && onLoadMore) { // 處理懶加載邏輯 } }; return { onRecord: handleNextLevelLoader, onExpand: onExpand, }; };
而縮進線相關的邏輯則抽取成 useIndentLinePlugin:
export const useIndentLinePlugin: TreeTablePlugin = ( props: ResolvedProps, context: TreeTablePluginContext ) => { const { expandedRowKeys } = context; const onColumn = column => { column.onCell = record => { return { record, ...column, }; }; }; const components = { body: { cell: cellProps => ( <IndentCell {...props} {...cellProps} expandedRowKeys={expandedRowKeys} /> ), }, }; return { components, onColumn, }; };
至此,主函數被精簡到 150 行左右,新功能相關的函數全部被移到插件目錄中去了,無論是想要新增或者刪減、開關功能都變的非常容易。
此時的目錄結構:
目錄結構
“怎么編寫插件機制優化基于Antd Table封裝表格的混亂代碼”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。