序文みなさん、久しぶりですね。最近新しい会社に入社したばかりで、スケジュールがとても詰まっています。普段は記事を書く時間が取れないので、更新頻度は遅くなります。 週末に家で退屈していたところ、突然弟が緊急の助けを求めてやって来ました。彼は、テンセントで面接を受けたとき、相手からVueの再帰メニューを渡されて実装するように言われたので、レビューのために私のところに来たそうです。 今週は短い週なので、外へ出て遊ぶ気はないので、家にいてコードを書くことにします。要件を確認したところ、確かに非常に複雑で、再帰コンポーネントの使用が必要であることがわかりました。 この機会に、Vue3 + TS を使用した再帰コンポーネントの実装に関する記事を要約したいと思います。 必要まずは Github Pages で効果をプレビューできます。 要件は、バックエンドが次の形式で潜在的に無限のレベルを持つメニューを返すことです。 [ { id: 1, 父親ID: 0, ステータス: 1, 名称: 「ライフサイエンスコンテスト」 _子供: [ { id: 2, 父親ID: 1, ステータス: 1, 名前: 「フィールドプラクティス」 _child: [{ id: 3, father_id: 2, status: 1, name: 'Botany' }], }, { id: 7, 父親ID: 1, ステータス: 1, 名称:「科学研究」 _子供: [ { id: 8、father_id: 7、status: 1、name: '植物学と植物生理学' }、 { id: 9、father_id: 7、status: 1、name: '動物学と動物生理学' }、 { id: 10、father_id: 7、status: 1、name: '微生物学' }、 { id: 11、father_id: 7、status: 1、name: 'エコロジー' }, ]、 }, { id: 71、father_id: 1、ステータス: 1、名前: '追加' }, ]、 }, { id: 56, 父親ID: 0, ステータス: 1, 名称:「大学院入試関連」 _子供: [ { id: 57、father_id: 56、ステータス: 1、名前: '政治' }, { id: 58、father_id: 56、ステータス: 1、名前: '外国語' }、 ]、 }, ] 1. 各レイヤーのメニュー要素に _child 属性がある場合、このメニューを選択した後も、この項目のすべてのサブメニューが引き続き表示されます。アニメーション画像をプレビューします。 2. 任意のレベルをクリックするとき、親コンポーネントからデータを要求するために、メニューの完全な ID リンクを最外部のレイヤーに渡す必要があります。たとえば、科学研究のカテゴリをクリックします。外部に発信する際には、最初のサブメニュー「植物学と植物生理学」のIDと、その親メニュー「生命科学コンペティション」のID、つまり[1、7、8]も送信する必要があります。 3. 各レイヤーのスタイルをカスタマイズできます。 成し遂げるこれは明らかに再帰コンポーネントの要件です。再帰コンポーネントを設計するときは、まずデータからビューへのマッピングについて明確に考える必要があります。 バックエンドから返されるデータでは、配列の各レイヤーがメニュー項目に対応できるため、配列のレイヤーはビュー内の行に対応します。現在のレイヤーのメニューでは、クリックして選択されたメニューの子がサブメニュー データとして使用され、特定のレイヤーの強調表示されたメニューに子がなくなれば再帰が終了するまで、再帰 NestMenu コンポーネントに渡されます。 要件では各レイヤーのスタイルが異なる可能性があることが求められるため、再帰コンポーネントを呼び出すたびに、親コンポーネントのプロパティからレベルを表す深度を取得し、この深度 + 1 を再帰 NestMenu コンポーネントに渡す必要があります。 これらが主なポイントであり、その後にコーディング実装が実行されます。 まず、NestMenu コンポーネントのテンプレート部分の一般的な構造を見てみましょう。 <テンプレート> <div class="wrap"> <div class="menu-wrap"> <div クラス="メニュー項目" v-for="データ内のメニュー項目" >{{メニュー項目名}}</div> </div> <ネストメニュー :key="アクティブID" :data="サブメニュー" :depth="深さ + 1" </ネストメニュー> </div> </テンプレート> 予想どおり、menu-wrap は現在のメニュー レイヤーを表し、nest-menu はサブコンポーネントを再帰的にレンダリングするコンポーネントそのものです。 最初のレンダリング最初にメニュー全体のデータを取得するときに、各メニュー レイヤーの選択項目をデフォルトで最初のサブメニューに設定する必要があります。非同期で取得される可能性が高いため、この操作を行うには、このデータを監視するのが最適です。 // メニューデータソースが変更されると、現在のレベルの最初の項目がデフォルトで選択されます。const activeId = ref<number | null>(null) 時計( () => props.data、 (新しいデータ) => { アクティブIDの値がある場合 (新しいデータ && 新しいデータの長さ) の場合 { アクティブID.値 = newData[0].id } } }, { 即時: true、 } ) では、上から始めましょう。最初のレイヤーの activeId は、ライフ サイエンス コンペティションの ID に設定されています。再帰的な子コンポーネント、つまりライフ サイエンス コンペティションの子に渡すデータは、計算プロパティである subMenu を通じて取得されることに注意してください。 const getActiveSubMenu = () => { data.find(({ id }) => id === activeId.value)._child を返します。 } const サブメニュー = 計算された(getActiveSubMenu) このようにして、ライフサイエンス コンペティションの子が取得され、サブコンポーネントのデータとして渡されます。 メニュー項目をクリックします以前の要求設計に戻ると、メニュー項目をクリックした後、どのレイヤーをクリックしたかに関係なく、完全な ID リンクをエミットを通じて最外層に渡す必要があるため、ここでさらに処理を実行する必要があります。 /** * サブメニューの最初の項目のIDを再帰的に収集する */ const getSubIds = (子) => { 定数サブID = [] const traverse = (データ) => { if (データ && データ長さ) { 定数first = データ[0] サブIDをプッシュ(first.id) トラバース(first._child) } } トラバース(子) サブIDを返す } const onMenuItemClick = (メニュー項目) => { const newActiveId = メニュー項目.id (newActiveId !== activeId.value) の場合 { アクティブID.値 = 新しいアクティブID 定数子 = getActiveSubMenu() const サブIds = getSubIds(子) // サブメニューのデフォルトの最初の項目 ID を連結し、親コンポーネントに出力します。 context.emit('change', [newActiveId, ...subIds]) } } 前に設定したルールでは、新しいメニューをクリックするとサブメニューの最初の項目がデフォルトで選択されるため、サブメニュー データの最初の項目も再帰的に検索し、最下層まで subIds に配置します。 ここで、イベントを上向きに発行する context.emit("change", [newId, ...subIds]); に注意してください。このメニューが中間レベルのメニューである場合、その親コンポーネントも NestMenu です。NestMenu コンポーネントが親レベルで再帰的に呼び出されたときに、change イベントをリッスンする必要があります。 <ネストメニュー :key="アクティブID" v-if="アクティブID !== null" :data="getActiveSubMenu()" :depth="深さ + 1" @change="onSubActiveIdChange" </ネストメニュー> 親メニューが子メニューの変更イベントを受け取った後、何をすべきでしょうか?はい、さらに上へ渡す必要があります。 定数 onSubActiveIdChange = (ids) => { context.emit('change', [activeId.value].concat(ids)) } ここでは、現在の activeId を配列の先頭に単純に結合し、それを上方に渡し続けるだけです。 このように、任意のレベルのコンポーネントがメニューをクリックすると、最初に独自の activeId を使用してすべてのサブレベルのデフォルトの activeId を結合し、次にそれをレイヤーごとに上方に送信します。そして、上の各親メニューは、リレーと同じように、独自の activeId を先頭に配置します。 最後に、アプリケーション レベルのコンポーネントで完全な ID リンクを簡単に取得できます。 <テンプレート> <nest-menu :data="メニュー" @change="activeIdsChange" /> </テンプレート> エクスポートデフォルト{ メソッド: { アクティブIDの変更(ID) { this.ids = ids; console.log("現在選択されているIDパス", ids); }, }, スタイルの区別再帰コンポーネントを呼び出すたびに depth + 1 を追加するので、この数字をクラス名の後に連結することでスタイルの差別化を実現できます。 <テンプレート> <div class="wrap"> <div class="menu-wrap" :class="`menu-wrap-${depth}`"> <div class="menu-item">{{メニュー項目名}}</div> </div> <ネストメニュー /> </div> </テンプレート> <スタイル> .menu-wrap-0 { 背景: #ffccc7; } .メニューラップ1 { 背景: #fff7e6; } .menu-wrap-2 { 背景: #fcffe6; } </スタイル> デフォルトのハイライト上記のコードを記述すると、デフォルト値がない場合のニーズを満たすのに十分です。このとき、面接官は、製品では、任意のレベルの id を渡すことで、このコンポーネントをデフォルトで強調表示する必要があると述べました。 実際、これは私たちにとって難しいことではありません。コードを少し変更するだけで済みます。親コンポーネントでは、URL パラメータまたはその他の方法で activeId を取得し、最初に深さ優先トラバーサルによってこの ID のすべての親を検索すると仮定します。 定数アクティブID = 7 const findPath = (メニュー、ターゲットID) => { idを入力 const traverse = (subMenus, prev) => { if (ids) { 戻る } if (!サブメニュー) { 戻る } サブメニュー.forEach((サブメニュー) => { (サブメニューID === アクティブID)の場合{ ids = [...前, アクティブID] 戻る } トラバース(サブメニュー._child、[...前、サブメニュー.id]) }) } トラバース(メニュー、[]) IDを返す } 定数 ids = findPath(データ、アクティブ ID) ここでは、再帰時に前のレイヤーの ID を取得することを選択しています。これにより、ターゲット ID を見つけた後、完全な親子 ID 配列を簡単に結合できます。 次に、構築した ID を activeIds として NestMenu に渡します。この時点で、NestMenu はデザインを変更して「制御されたコンポーネント」になる必要があります。そのレンダリング状態は、外側のレイヤーから渡されたデータによって制御されます。 そのため、パラメータを初期化する際の値のロジックを変更し、activeIds[depth]を優先し、メニュー項目をクリックした際に、最も外側のページコンポーネントで変更イベントを受信したときに、activeIdsのデータを同期的に変更する必要があります。こうすることで、NestMenu が受信したデータが混乱することがなくなります。 <テンプレート> <nest-menu :data="データ" :defaultActiveIds="ids" @change="activeIdsChange" /> </テンプレート> NestMenu は初期化時にデフォルト値が存在する状況に対応し、配列から取得した id 値を優先して使用します。 セットアップ(props: IProps, コンテキスト) { const { 深さ = 0、アクティブIds } = props; /** * ここで activeIds は非同期的に取得される可能性もあるため、初期化を確実にするために watch を使用します */ const activeId = ref<number | null | undefined>(null); 時計( () => アクティブID、 (新しいアクティブID) => { if (新しいアクティブID) { 定数 newActiveId = newActiveIds[深さ]; if (新しいアクティブID) { アクティブID値 = 新しいアクティブID; } } }, { 即時: true、 } ); } この方法では、activeIds 配列を取得できない場合、デフォルトは null のままです。メニュー データの変更を監視するロジックでは、activeId が null の場合、最初のサブメニューの ID に初期化されます。 時計( () => props.data、 (新しいデータ) => { アクティブIDの値がある場合 if (newData && newData.length) { アクティブID.値 = newData[0].id } } }, { 即時: true、 } ) 最も外側のページ コンテナーが変更イベントをリッスンする場合、データ ソースを同期する必要があります。 <テンプレート> <nest-menu :data="データ" :activeIds="ids" @change="activeIdsChange" /> </テンプレート> <スクリプト> "vue" から { ref } をインポートします。 エクスポートデフォルト{ 名前:「アプリ」、 設定() { 定数activeIdsChange = (新しいIds) => { ids.value = 新しいIds; }; 戻る { ID、 アクティブIDの変更、 }; }, }; </スクリプト> このように、activeIds が外部から渡されると、NestMenu 全体のハイライト選択ロジックを制御することができます。 データソースの変更によって発生するバグこのとき、面接官はアプリ ファイルに若干の変更を加え、次のようなバグを指摘しました。 App.vue のセットアップ関数に次のロジックが追加されます。 マウント時(() => { タイムアウトを設定する(() => { menu.value = [data[0]].slice() }, 1000) }) つまり、コンポーネントがレンダリングされてから 1 秒後には、メニューの最も外側のレイヤーに残っている項目は 1 つだけです。この時点で、インタビュアーは 1 秒以内に最も外側のレイヤーの 2 番目の項目をクリックします。データ ソースが変更されると、このコンポーネントはエラーを報告します。 これは、データ ソースが変更されたが、コンポーネント内の activeId 状態が、もはや存在しない ID のままになっているためです。 これにより、subMenu の計算プロパティが計算時に失敗します。 ウォッチデータ観測データソースのロジックを少し変更します。 時計( () => props.data、 (新しいデータ) => { アクティブIDの値がある場合 (新しいデータ && 新しいデータの長さ) の場合 { アクティブID.値 = newData[0].id } } // 現在のレベルのデータに `activeId` の値が見つからない場合は、この値が無効であることを意味します。 // データ ソースの最初のサブメニュー項目の ID に調整します props.data.find(({ id }) => id === activeId.value) の場合 { アクティブID.値 = props.data?.[0].id } }, { 即時: true、 // レンダリングエラーを防ぐためにデータの変更を監視した後、同期的に実行します。flush: 'sync', } ) ここで、flush: "sync" が重要であることに注意してください。Vue3 は、データ ソースの変更を監視した後、コールバックをトリガーします。デフォルトでは、ポスト レンダリング後に実行されます。ただし、現在の要件では、レンダリングに間違った activeId を使用すると、直接エラーが発生するため、この監視を手動で同期動作に変更する必要があります。 これで、データ ソースの変更によって発生するレンダリング エラーを心配する必要がなくなりました。 完全なコードアプリ.vue<テンプレート> <nest-menu :data="データ" :activeIds="ids" @change="activeIdsChange" /> </テンプレート> <スクリプト> "vue" から { ref } をインポートします。 「./components/NestMenu.vue」からNestMenuをインポートします。 「./menu.js」からデータをインポートします。 「./util」から { getSubIds } をインポートします。 エクスポートデフォルト{ 名前:「アプリ」、 設定() { // デフォルトの選択IDは7であると仮定します 定数アクティブID = 7; const findPath = (メニュー、ターゲットID) => { id を入力します。 const traverse = (subMenus, prev) => { if (ids) { 戻る; } if (!サブメニュー) { 戻る; } サブメニュー.forEach((サブメニュー) => { (サブメニューID === アクティブID)の場合{ ids = [...前、アクティブID]; 戻る; } トラバース(subMenu._child、[...prev, subMenu.id]); }); }; トラバース(メニュー、[]); ID を返します。 }; const ids = ref(findPath(data, activeId)); 定数activeIdsChange = (新しいIds) => { ids.value = 新しいIds; console.log("現在選択されているIDパス", newIds); }; 戻る { ID、 アクティブIDの変更、 データ、 }; }, コンポーネント: ネストメニュー、 }, }; </スクリプト> ネストメニュー.vue <テンプレート> <div class="wrap"> <div class="menu-wrap" :class="`menu-wrap-${depth}`"> <div クラス="メニュー項目" v-for="データ内のメニュー項目" :class="getActiveClass(メニュー項目ID)" @click="onMenuItemClick(メニュー項目)" :key="メニュー項目id" >{{メニュー項目名}}</div> </div> <ネストメニュー :key="アクティブID" v-if="サブメニュー && サブメニュー.length" :data="サブメニュー" :depth="深さ + 1" :activeIds="アクティブID" @change="onSubActiveIdChange" </ネストメニュー> </div> </テンプレート> <script lang="ts"> 「vue」から {watch、ref、onMounted、computed} をインポートします。 「../menu」からデータをインポートします。 インターフェース IProps { データ: データの型; 深さ: 数値; アクティブID?: 数値[]; } エクスポートデフォルト{ 名前: "NestMenu", プロパティ: ["データ", "深さ", "アクティブIds"], セットアップ(props: IProps, コンテキスト) { const { 深さ = 0、アクティブIds、データ } = props; /** * ここで activeIds は非同期的に取得される可能性もあるため、初期化を確実にするために watch を使用します */ const activeId = ref<number | null | undefined>(null); 時計( () => アクティブID、 (新しいアクティブID) => { if (新しいアクティブID) { 定数 newActiveId = newActiveIds[深さ]; if (新しいアクティブID) { アクティブID値 = 新しいアクティブID; } } }, { 即時: true、 フラッシュ: 'sync' } ); /** * メニューデータソースが変更されると、現在のレベルの最初の項目がデフォルトで選択されます*/ 時計( () => props.data、 (新しいデータ) => { アクティブIDの値がある場合 (新しいデータ && 新しいデータの長さ) の場合 { アクティブID.値 = newData[0].id; } } // 現在のレベルのデータに `activeId` の値が見つからない場合は、この値が無効であることを意味します。 // データ ソースの最初のサブメニュー項目の ID に調整します props.data.find(({ id }) => id === activeId.value) の場合 { アクティブIDの値 = props.data?.[0].id; } }, { 即時: true、 // レンダリングエラーを防ぐためにデータの変更を監視した後、同期的に実行します。flush: "sync", } ); const onMenuItemClick = (メニュー項目) => { const newActiveId = メニュー項目.id; (newActiveId !== activeId.value) の場合 { アクティブID値 = 新しいアクティブID; const 子 = getActiveSubMenu(); const サブ ID = getSubIds(子); // サブメニューのデフォルトの最初の項目 ID を連結し、親コンポーネントに出力します。 context.emit("change", [newActiveId, ...subIds]); } }; /** * 子コンポーネントの更新された activeId を受信すると、 * 親コンポーネントに activeId が更新されたことを通知する仲介役として機能する必要があります。*/ 定数 onSubActiveIdChange = (ids) => { context.emit("change", [activeId.value].concat(ids)); }; const getActiveSubMenu = () => { props.data?.find(({ id }) => id === activeId.value)._child を返します。 }; const サブメニュー = computed(getActiveSubMenu); /** * スタイル関連 */ const getActiveClass = (id) => { id === activeId.value の場合 { 「menu-active」を返します。 } 戻る ""; }; /** * サブメニューの最初の項目のIDを再帰的に収集する */ const getSubIds = (子) => { 定数subIds = []; const traverse = (データ) => { if (データ && データ長さ) { 定数first = データ[0]; サブIDをプッシュします。 トラバース(first._child); } }; トラバース(子); サブIDを返します。 }; 戻る { 深さ、 アクティブID、 サブメニュー、 メニュー項目クリック時、 サブアクティブIDの変更、 アクティブクラスを取得、 }; }, }; </スクリプト> <スタイル> 。包む { パディング: 12px 0; } .メニューラップ{ ディスプレイ: フレックス; flex-wrap: ラップ; } .menu-wrap-0 { 背景: #ffccc7; } .メニューラップ1 { 背景: #fff7e6; } .メニューラップ2 { 背景: #fcffe6; } .メニュー項目{ 左マージン: 16px; カーソル: ポインタ; 空白: ラップなし; } .menu-active { 色: #f5222d; } </スタイル> ソースコードアドレス github.com/sl1673495/v… 要約する再帰メニュー コンポーネントはシンプルですが、難しいものでもあります。 Vue の非同期レンダリングと監視戦略を理解していないと、途中で発生するバグが長い間私たちを悩ませる可能性があります。そのため、原理を適切に学ぶ必要があります。 一般的なコンポーネントを開発する場合、データソースの入力タイミング(同期、非同期)に注意する必要があります。非同期で入力されたデータについては、監視 API をうまく活用して変更を監視し、対応する操作を実行する必要があります。また、データ ソースの変更がコンポーネントに保存されている元の状態と競合するかどうかを考慮し、適切なタイミングでクリーンアップ操作を実行します。 もう 1 つ小さな疑問があります。NestMenu コンポーネントのデータ ソースを確認するときは、次のようにします。 props.data を監視します。 分解してから観察するのではなく: const {データ} = props; ウォッチ(() => データ); これら2つには何か違いがありますか?これはあなたの深みをテストするもう一つの面接の質問です。 これで、Vue3+TypeScript で再帰メニュー コンポーネントを実装する方法についての記事は終了です。Vue3+TypeScript の再帰メニュー コンポーネントに関するより関連性の高いコンテンツについては、123WORDPRESS.COM で以前の記事を検索するか、以下の関連記事を引き続き参照してください。今後とも 123WORDPRESS.COM をよろしくお願いいたします。 以下もご興味があるかもしれません:
|
<<: Linux の一般的な Java プログラム起動スクリプトのコード例
>>: MySQL 文字列連結関数 GROUP_CONCAT の詳細な説明
上の境界線のみを表示する <table frame=above>下の境界線のみを表示する...
前回の記事では、MySQL の置換関数 (Replace) とセグメンテーション関数 (SubStr...
序文WeChat ミニプログラム プロジェクトでユーザー情報を取得し、ユーザー ログインを実装する場...
目次1. 糖衣構文とは何ですか? 2. VUE の構文糖とは何ですか? 1. 最も一般的な構文シュガ...
ウェブサイトを最適化するときは、エラー ページの使い方を学ぶ必要があります。たとえば、ウェブサイトに...
たとえば、次のように入力します。 XML/HTML コードdiv#ページ>(div#ヘッダー&...
開発環境では、vue プロジェクトは、ローカルで Express サーバーを構築することをベースにし...
サンバの概要Samba は、Linux および UNIX システム上で SMB プロトコルを実装する...
カメラを開くと通常はスキャンボックスが表示されますが、静的なQRコードではフォーカスを合わせたりスキ...
nginx でファイルサーバーを構築することもありますが、これは一般に公開されていますが、サーバーが...
Linux に触れたばかりの方には、この内容が役に立つかもしれません。Linux にしばらく触れてい...
MYSQL 5.6 スレーブレプリケーションの展開と監視MYSQL 5.6 のインストールと展開 #...
序文クラスメートが MLSQL Stack のストリーミング サポートを調査しています。そこで、フロ...
目次1. forループ: 基本的でシンプル2. forEach() メソッド: コールバック関数の使...
まず関数の自己呼び出しを知る必要がある関数の自己呼び出し - 自己呼び出し関数1 回限りの関数 - ...