DUICUO

オープンソース コンポーネントを再パッケージ化して、インデント ガイドや子ノードの遅延読み込みなどの機能を Antd テーブル コンポーネントに追加するにはどうすればよいですか?

[[384776]]

ビジネスアプリケーションでは、Ant Designのようなコンポーネントライブラリに基づいて多くの機能をカスタマイズする必要がある場合があります。この記事では、私が遭遇したビジネス要件を例に、ツリー構造のテーブルコンポーネントの実装と最適化を段階的に説明します。このコンポーネントは以下をサポートします。

  • 各レベルのインデントインジケータ線
  • 子ノードのリモート遅延読み込み
  • 各レベルでページ区切りがサポートされます。

このシリーズは 2 つの記事で構成されており、この記事ではこれらのビジネス要件を実装する方法のみを説明します。

次の記事では、コードの結合とメンテナンスの難しさの問題を解決するために、コンポーネントのシンプルなプラグイン機構を設計する方法を説明します。

機能性

階層的なインデント行

Ant Design のテーブル コンポーネントは、デフォルトではこの機能を提供しておらず、ツリー構造のみをサポートしています。

  1. const ツリーデータ = [
  2. {
  3. function_name: `React Tree Reconciliation`,
  4. カウント: 100,
  5. 子供たち: [
  6. {
  7. 関数名: `React Tree Reconciliation2`,
  8. カウント: 100
  9. }
  10. ]
  11. }
  12. ]

表示効果は以下のとおりです。

antdテーブル

ご覧の通り、大きな関数スタックをインデントなしで表示するのはかなり不自然です。ビジネスチームからこの要件について事前に説明を受けたのですが、残念ながら以前から忙しすぎて、当面は保留にせざるを得ませんでした。😁

VSCode のインデント効果を参照すると、インデントはノードの階層と密接に関係していることがわかります。

vscode

たとえば、src ディレクトリが最初のレベルに対応する場合、その子ディレクトリである client と node は td の前に 1 本の垂直線を描くだけで済みますが、node の下の 3 つのディレクトリは 2 本の垂直線を描く必要があります。

  1. レベル 1: | テキスト
  2. レベル2: | | テキスト
  3. レベル3: | | | テキスト

セル要素をカスタム レンダリングする場合は、次の 2 つの情報のみを取得する必要があります。

  1. 現在のノードの階層情報。
  2. 現在のノードの親ノードは展開された状態ですか?

したがって、データを再帰的に処理し、ノードの階層構造と親ノードへの参照を書き込みます。そして、テーブルの展開された行データをTableのexpandedRowKeysプロパティに渡すことで、テーブル内の行データを保持します。

ここでは元のデータを直接書き換えました。元のデータがクリーンであることを保証する必要がある場合は、React Fiberの考え方を参考に、元のツリーノードへの参照を維持する限り、データ書き込み用の代替ツリーを構築することもできます。

  1. /**
  2. * 再帰木の一般的な関数
  3. /
  4. const トラバースツリー = (
  5. ツリーリスト、
  6. childrenColumnName、
  7. 折り返し電話
  8. ) => {
  9. const traverse = (リスト、親 = null レベル= 1) => {
  10. リスト.forEach(ツリーノード => {
  11. コールバック(treeNode、親、レベル);
  12. const { [childrenColumnName]: next } = treeNode;
  13. if (Array.isArray(次の)) {
  14. トラバース( next , treeNode,レベル+ 1);
  15. }
  16. });
  17. };
  18. トラバース(ツリーリスト);
  19. };
  20.  
  21. 関数rewriteTree({ データソース }) {
  22. traverseTree(データソース、childrenColumnName、(ノード、親、レベル) => {
  23. // ノードの階層を記録する
  24. ノード[INTERNAL_LEVEL] =レベル 
  25. // ノードの親ノードを記録する
  26. ノード[INTERNAL_PARENT] = 親
  27. })
  28. }

次に、Table コンポーネントによって提供される components プロパティを使用して、td 要素である Cell コンポーネントのレンダリングをカスタマイズできます。

  1. constコンポーネント = {
  2. 体: {
  3. セル: (セルプロパティ) => (
  4. <ツリーテーブルセル
  5. {...小道具}
  6. {...セルプロパティ}
  7. 展開された行キー = {展開された行キー}
  8. />
  9. }
  10. }

その後、カスタム レンダリングされたセルでは、描画する垂直線の数が階層と親ノードの展開状態によって決まるという 2 つの情報のみが必要になります。

  1. const isParentExpanded = 拡張RowKeys.includes(
  2. レコード?.[INTERNAL_PARENT]?.[rowKey]
  3. // インデントされたガイドラインは、現在の列が展開可能な親ノードを持つ列である場合にのみ表示されます。
  4. if (dataIndex !== indentLineDataIndex || !isParentExpanded) {
  5. <td className={className}>{children}を返す<​​/td>
  6. }
  7.  
  8. // 階層構造を理解することで、td要素に何本の垂直ガイドラインを描くべきかが分かります。例えば:
  9. // レベル 2: | | テキスト
  10. // レベル 3: | | | テキスト
  11. constレベル= レコード[INTERNAL_LEVEL]
  12.  
  13. const indentLines = renderIndentLines(レベル)

実装の詳細はここでは省略します。絶対位置指定を使用して垂直線をいくつか描画し、レベルをループする際にインデックスを使用して左のオフセット値を決定します。

結果は画像に示されています:

インデント

子ノードのリモート遅延読み込み

この要件を実装するには、かなりハック的なアプローチが必要です。まず、Tableコンポーネントのロジックを観察し、「さらに展開」アイコンは子ノードを持つ子ノードにのみ表示されることを発見しました。

したがって、アイデアとしては、has_next などのフィールドをバックエンドと合意し、データを前処理するときに、最初にこれらのノードを走査して、偽のプレースホルダーの子を追加するというものです。

次に、ノードが展開されると、偽の子ノードが削除され、ノード上の特別な is_loading フィールドを変更することで、カスタム アイコン レンダリング コードに読み込みアイコンが表示されます。

再帰ツリーのロジックに戻って、次のコードを追加します。

  1. 関数rewriteTree({ データソース }) {
  2. traverseTree(データソース、childrenColumnName、(ノード、親、レベル) => {
  3. if (ノード[次のキーを持つ]) {
  4. // ツリー テーブル コンポーネントでは、「展開ボタン」をレンダリングするためにnext が空でない配列である必要があります。
  5. // したがって、ここでプレースホルダー ノード配列を手動で追加します。
  6. // さらにノードがロードされ、この配列は後で onExpand で置き換えられます。
  7. ノード[childrenColumnName] = [generateInternalLoadingNode(rowKey)]
  8. }
  9. })
  10. }

次に、コンポーネントを強制的にレンダリングするための `forceUpdate` 関数を実装する必要があります。

  1. const [_, forceUpdate] = useReducer((x) => x + 1, 0)

次に、onExpand のロジックを見てみましょう。

  1. const onExpand = async (展開、レコード) => {
  2. if (expanded && record[hasNextKey] && onLoadMore) {
  3. // 識別子ノードの読み込み
  4. レコード[INTERNAL_IS_LOADING] = true  
  5. // 展開矢印を表示するために使用された偽の子要素を削除します
  6. レコード[childrenColumnName] = null  
  7. 強制更新()
  8. const childList = onLoadMore(レコード) を待機します
  9. レコード[hasNextKey] = false  
  10. addChildList(レコード、childList)
  11. }
  12. onExpandProp?.(展開、記録)
  13. }
  14.  
  15. 関数addChildList(レコード, childList) {
  16. レコード[childrenColumnName] = 子リスト
  17. レコード[INTERNAL_IS_LOADING] = false  
  18. ツリーの書き換え({
  19. データソース: childList,
  20. 親ノード: レコード
  21. })
  22. 強制更新()
  23. }

ここで、onLoadMore は、より多くの子ノードを取得するためにユーザーによって渡されるメソッドです。

プロセスは次のとおりです。

  1. ノードが展開される際、まずノードに読み込みフラグを書き込み、次に子ノードのデータを空にリセットします。これによりノードは展開されますが、子ノードはレンダリングされず、強制的にレンダリングされます。
  2. 新しい子ノード `record[childrenColumnName] = childList` にロード後に新しい値が割り当てられたら、`forceUpdate` を使用してコンポーネントを強制的に再レン​​ダリングし、新しい子ノードを表示します。

再帰ツリーにノードを追加するためのロジックはすべて `rewriteTree` 関数内に含まれていることに注意することが重要です。したがって、新しい子ノードが追加されるたびに、この関数を再帰的に実行して、レベルや親などの情報を追加する必要があります。

新しく追加されたノードのレベルは、親ノードのレベルを加算して計算する必要があり、1から始めることはできません。そうしないと、レンダリング時のインデント線が乱れてしまいます。そのため、この関数をparentNodeパラメータを含むように書き換え、トラバーサル中に書き込まれるレベルを親ノードに既に存在するレベル分だけ増加させる必要があります。

  1. 関数rewriteTree({
  2. データソース、
  3. // サブツリー ノードを動的に追加する場合は、親参照を手動で渡す必要があります。
  4. 親ノード = null  
  5. }) {
  6. // サブツリー ノードを動的に追加する場合は、親ノードのレベルを手動で渡す必要があります。そうしないと、レベルは1 から計算されます。
  7. const startLevel = parentNode?.[INTERNAL_LEVEL] || 0
  8.  
  9. traverseTree(データソース、childrenColumnName、(ノード、親、レベル) => {
  10. 親 = 親 || 親ノード;
  11. // ノードの階層を記録する
  12. ノード[INTERNAL_LEVEL] =レベル+ 開始レベル;
  13. // ノードの親ノードを記録する
  14. ノード[INTERNAL_PARENT] = 親;
  15.  
  16. if (ノード[次のキーを持つ]) {
  17. // ツリー テーブル コンポーネントでは、「展開ボタン」をレンダリングするためにnext が空でない配列である必要があります。
  18. // したがって、ここでプレースホルダー ノード配列を手動で追加します。
  19. // さらにノードがロードされ、この配列は後で onExpand で置き換えられます。
  20. ノード[childrenColumnName] = [generateInternalLoadingNode(rowKey)]
  21. }
  22. })
  23. }

レンダリング読み込みアイコンのカスタマイズは非常に簡単です。

  1. // expandIcon プロパティをTableコンポーネントに渡すだけです。
  2. エクスポート const TreeTableExpandIcon = ({
  3. 拡大、
  4. 拡張可能、
  5. 展開時、
  6. 記録
  7. }) => {
  8. if (レコード[内部読み込み中]) {
  9. <IconLoading style={iconStyle} />を返します
  10. }
  11. }

関数は完成しました。結果を見てみましょう。

リモート遅延読み込み

各レベルでページ区切りがサポートされます。

この関数は前の関数と似ています。ツリーを書き換える際に、ページネーションが有効かどうかを示す外部から渡されたフィールドに基づいて条件が満たされた場合、子ノード配列の末尾にプレースホルダーの Pagination ノードを追加する必要があります。

次に、このノードのレンダリング ロジックが列のレンダリング関数で書き換えられます。

記録を書き換える:

  1. 関数rewriteTree({
  2. データソース、
  3. // サブツリー ノードを動的に追加する場合は、親参照を手動で渡す必要があります。
  4. 親ノード = null  
  5. }) {
  6. // サブツリー ノードを動的に追加する場合は、親ノードのレベルを手動で渡す必要があります。そうしないと、レベルは1 から計算されます。
  7. const startLevel = parentNode?.[INTERNAL_LEVEL] || 0
  8.  
  9. traverseTree(データソース、childrenColumnName、(ノード、親、レベル) => {
  10. // さらにロジックをロードする
  11. if (ノード[次のキーを持つ]) {
  12. // ツリー テーブル コンポーネントでは、「展開ボタン」をレンダリングするためにnext が空でない配列である必要があります。
  13. // したがって、ここでプレースホルダー ノード配列を手動で追加します。
  14. // さらにノードがロードされ、この配列は後で onExpand で置き換えられます。
  15. ノード[childrenColumnName] = [generateInternalLoadingNode(rowKey)]
  16. }
  17.  
  18. // ページネーションロジック
  19. if (childrenPagination) {
  20. const { totalKey } = childrenPagination;
  21. const nodeChildren = node[childrenColumnName] || [];
  22. const [lastChildNode] = nodeChildren.slice?.(-1);
  23. // ページネーションツールをレンダリングし、まずプレースホルダーノードを追加します
  24. もし (
  25. ノード[合計キー] > ノードチャイルド?.長さ &&
  26. // ページ区切りプレースホルダの重複を防ぐ
  27. !isInternalPaginationNode(最後の子ノード、行キー)
  28. ){
  29. nodeChildren?.push?.(generateInternalPaginationNode(rowKey));
  30. }
  31. }
  32. })
  33. }

列を書き換える:

  1. 関数rewriteColumns() {
  2. /**
  3. * プレースホルダーに基づいてページ区切りコンポーネントをレンダリングします。
  4. /
  5. const rewritePaginationRender = () => {
  6. .render =関数ColumnRender(テキスト, レコード) {
  7. もし (
  8. isInternalPaginationNode(レコード、行キー) &&
  9. dataIndex === indentLineDataIndex
  10. ){
  11. <ページネーション />を返す
  12. }
  13. レンダリングを返しますか?(テキスト、レコード) ?? テキスト
  14. }
  15. }
  16.  
  17. columns.forEach(() => {
  18. rewritePaginationRender()
  19. })
  20. }

実装されたページネーション効果を見てみましょう。

リファクタリングと最適化

より多くの機能が記述されるにつれて、ロジックは Antd テーブルのさまざまなコールバック関数に結合されるようになります。

  • ガイドラインのロジックは、rewriteColumns とコンポーネント全体に分散されます。
  • ページネーション ロジックは rewriteColumns と rewriteTree に分散されます。
  • より多くのデータをロードするためのロジックは、rewriteTree と onExpand に分散されます。

この時点で、コンポーネントのコードは 300 行に達しており、コード構造をざっと見てみると、かなり乱雑になっていることがわかります。

  1. エクスポート const TreeTable = (rawProps) => {
  2. 関数rewriteTree() {
  3. // 🎈さらにロジックを読み込む
  4. // 🔖 ページネーションロジック
  5. }
  6.  
  7. 関数rewriteColumns() {
  8. // 🔖 ページネーションロジック
  9. // 🏁 インデントロジック
  10. }
  11.  
  12. constコンポーネント = {
  13. // 🏁 インデントロジック
  14. }
  15.  
  16. const onExpand = async (展開、レコード) => {
  17. // 🎈 さらにロジックを読み込む
  18. }
  19.  
  20. 戻り値<テーブル/>
  21. }

コードをさまざまな機能に分散させるのではなく、機能ごとにグループ化できるメカニズムはありますか?

  1. // 🔖 ページネーションロジック
  2. const usePaginationPlugin = () => {}
  3. // 🎈 さらにロジックを読み込む
  4. const useLazyloadPlugin = () => {}
  5. // 🏁 インデントロジック
  6. const useIndentLinePlugin = () => {}
  7.  
  8. エクスポート const TreeTable = (rawProps) => {
  9. ページネーションプラグイン()を使用する
  10.  
  11. レイジーロードプラグイン()
  12.  
  13. インデントラインプラグイン()
  14.  
  15. 戻り値<テーブル/>
  16. }

そうです、ロジックの分離という点では VueCompositionAPI と React Hook による改善と非常に似ていますが、このコールバック関数構文で実現するのは少し難しいように思えます。

次の記事では、独自に設計したプラグイン メカニズムを使用して、このコンポーネントの結合コードを最適化する方法について説明します。

フォローと友達追加をお忘れなく。フロントエンドの知識や業界情報を随時共有していきます。2021年を一緒に乗り越えましょう。

この記事は、WeChat公式アカウント「上級者からプロまで、フロントエンド開発」から転載したものです。以下のQRコードからアクセスできます。転載の許可については、「上級者からプロまで、フロントエンド開発」公式アカウントまでお問い合わせください。