プロジェクトの背景: Vue 2 SPA + Cesium 二次開発ベースの 3D エンジン (以下、総称して 3D エンジンと呼びます) + IndexedDB オフライン キャッシュ + データ大画面。最新のメンテナンスでは、3 つのバグの修正に重点が置かれています。トラブルシューティングのプロセスと修理計画は非常に一般的なものなので、記録してみました。これら 3 つの質問は無関係に見えるかもしれませんが、調べてみると、それらには 1 つの共通点があることがわかりました。全ては「国境」で起こる: コンポーネントのライフサイクルとグローバル状態の間の境界、メモリ オブジェクトと永続ストレージの間の境界、および同一生成元ポリシー間の境界。
落とし穴 1: 2 回目にこのページにアクセスすると、すべての監視ポイントのラベルが消えます。
現象
初めて 3D ページに入ったときは、すべてが正常に動作します。一旦終了して再度入ると地図は読み込めて画像も正常ですが、モニタリング ポイントのラベルが表示されないコンソールがエラーを報告します:
Cannot read properties of undefined (reading 'scene')
エラーの場所はビジネス コード内ではなく、ラベル モジュールが古いリスナーのバインドを解除する行にあります。
根本原因: モジュールレベルのシングルトンがコンポーネントのライフサイクルに達する
パフォーマンスを向上させるため、すべての DIV タグはレンダリング ループを共有します: モジュール レベルの変数 sharedViewer ビューア参照を保持します。次のように表されます。 scene.postRender 各フレームはすべてのタグを均一に駆動して位置更新を実行します (ダーティ チェックを使用すると、カメラが静止しているときのオーバーヘッドはゼロに近づきます)。
// label.js(修复前)
let sharedBound = false;
let sharedViewer = null;
function ensureSharedPostRender(viewer) {
if (sharedBound && sharedViewer === viewer) return;
// viewer 变化(场景重建)时解绑旧监听
if (sharedBound && sharedViewer && sharedViewer.scene && sharedViewer.scene.postRender) {
sharedViewer.scene.postRender.removeEventListener(sharedPostRender);
}
sharedViewer = viewer;
sharedViewer.scene.postRender.addEventListener(sharedPostRender);
sharedBound = true;
}
問題はSPAのページ破壊にある。ページを離れるとき、閲覧者は destroy() はい、でもこの共有レンダリング ループからは誰もログアウトする必要はありません——sharedViewer 破壊されたビューアをまだ指しています。 3D エンジンのビューアが破壊されると、内部の _cesiumWidget 空白のままになります、もう一度読んでください .scene TypeError を直接スローするだけです。
それで、2回目にページに入ったとき、新しいタグの登録が「古いモニターのバインドを解除する」ことになり、爆発しました。さらに恐ろしいのは、この異常が発生するのは次の場合です。登録パス上 – 最初のタグの登録が失敗し、その後のすべてのタグが失敗し、パフォーマンスが「すべて消えた」状態になりました。
修正: 破壊命令 + 古い参照防御
2 つの変更のうち 1 つは必須です。
最初のステップは、破壊の順序を正すことです。。原則は単純です。視聴者に依存した者が最初に破壊されます。ラベルの共有レンダリング サイクルはビューアに依存するため、 viewer.destroy() 次の前にログアウトします。
// index.vue beforeDestroy(修复后,节选)
beforeDestroy() {
// 1. 清理定时器
// 2. 先销毁全部 DivLabel(其共享渲染循环以模块级变量持有 viewer 引用,
// 须在 viewer 销毁前注销,防止 sharedViewer 残留已销毁 viewer)
destroyAllDivLabels();
// 3. 销毁鼠标事件处理器(同样依赖 viewer)
// 4. 最后销毁 Viewer 实例
}
2 番目のポイントは、「古い参照」を防ぐことです。。破壊順序が正しくても、すべてのパスが保存されるわけではありません (たとえば、異常なブランチはクリーニングをスキップします)。古いリスナーのバインドを解除する前に、まず古いビューアが破棄されたかどうかを確認してから、例外を処理します。
// label.js(修复后)
if (sharedBound && sharedViewer) {
try {
const staleDestroyed =
typeof sharedViewer.isDestroyed === 'function' && sharedViewer.isDestroyed();
if (!staleDestroyed && sharedViewer.scene && sharedViewer.scene.postRender) {
sharedViewer.scene.postRender.removeEventListener(sharedPostRender);
}
} catch (err) {
// 旧 viewer 已失效,其渲染循环已随销毁终止,无需也无法解绑
}
}
混同されやすいポイントは次のとおりです。try/catch 異常に飲み込むと不味くないですか?このシナリオでは、古いビューアが破棄されているため、レンダリング ループが発生します。破壊により自然終了した「バンドルを解除する」という行為自体には意味がありません。例外は、何もすることがないことを意味するだけです。防御側のコードには明確な意味論的な注釈がなければなりません。そうしないと、後発のコードが他のバグを隠していると考えるでしょう。
鼓舞する
- SPAのいたるところにモジュールレベルの変数はコンポーネントインスタンスの参照を保持しますすべての場所がライフサイクルの不一致によって最も大きな打撃を受けています。作成するときは、コンポーネントが破棄された後、誰がこの参照をクリアするのかを自問してください。
- 破壊命令の原則:依存関係は依存関係よりも先に破棄されます。
- 登録パス上の「古い参照」を防御します。履歴に残った不適切な参照によって初期化リンク全体がブロックされないようにしてください。
落とし穴 2: IndexedDB が DOM の受け入れを拒否する – DataCloneError
背景
脆弱なネットワーク/オフライン シナリオで 3D ブラウジングをサポートするために、オフライン キャッシュ レイヤーを作成しました。3D エンジンを書き換えました。 Resource._Implementations.loadWithXhr このインターセプト ポイントで、応答は IndexedDB に書き込まれます (本体 + メタ デュアル ストア。起動時にカーソルのみがメタをスキャンして LRU インデックスを構築します)。ネットワーク パスはエンジンのネイティブ実装を完全に再利用し、書き込まれた値はネイティブ出力値です。 「当然ながら型のない配置の問題」 – これは元々コメントに書かれたものです。
現象
パノラマシーンのXMLレスポンスをキャッシュする場合、put 直接投げる DataCloneErrorキャッシュの書き込みはサイレントに失敗し (catch によって消費されます)、オフライン モードをオンにするとホワイトボードになります。
根本原因: 構造化クローンの境界
IndexedDB は、次を使用してオブジェクトを保存します構造化されたクローン作成アルゴリズム(構造化クローン アルゴリズム)。そのサポート範囲は JSON よりも広いですが (Blob、ArrayBuffer、Map、Set はすべて受け入れられます)、厳密な境界があります。DOM ノードは複製できません。 XML に対する 3D エンジンのネイティブ応答の出力は次のとおりです。 XMLDocument——本物のDOMノード。
修復: シリアル化がライブラリにドロップされ、マーキングが復元されました
アイデア: ドキュメントをライブラリにドロップする前に使用する XMLSerializer 文字列にシリアル化し、メタに入力します xml: true マーク;読むときはマークを押してください DOMParser 分析セッションのドキュメント——元の出力と一致するタイプに復元します上流階級の消費者はまったく無関心です。
// DOM Document(含 XMLDocument)无法结构化克隆,需字符串化存取
function isDocument(v) {
return typeof Document !== 'undefined' && v instanceof Document;
}
// 读取还原:meta.xml 标记的条目以字符串存储,取出时解析回 Document
function decodeBody(meta, body) {
if (meta && meta.xml && typeof body === 'string' && typeof DOMParser !== 'undefined') {
try {
return new DOMParser().parseFromString(body, 'text/xml');
} catch (e) { /* 解析失败返回原始字符串 */ }
}
return body;
}
async function cachePut(key, url, value, extra) {
if (value == null) return;
// ...构建 meta...
if (isDocument(value)) {
meta.xml = true;
value = new XMLSerializer().serializeToString(value);
}
await idbPutEntry(key, value, meta);
}
隠された 2 番目のシーン: パスのタイプ アライメントの更新
執筆はまだ終わっていません。キャッシュには再検証ロジックがあります。ETag/Last-Modified を使用して条件付きリクエストを送信します。304 は古い値を引き続き使用し、200 は更新します。アップデート時 responseType が空の場合、「古い値の型」に従って新しい応答を調整する必要があります – 古い値は通常のオブジェクトです JSON.parseこの支店はもともと存在していました。ただし、古い値が XML の場合、それはすでにデータベース内の文字列であり、本体ストアから読み取られます。 old これも文字列であるため、古い型検出ロジックではまったく認識できません。
したがって、私たちは次のことを行う必要があります古いエントリのメタ それを渡してください、いまいましい meta.xml マーカーの位置合わせ:
async function cachePutAligned(key, url, response, responseType, extra, oldMeta) {
let value = response;
if (!responseType && typeof response === 'string') {
const oldIsPlain = /* ...旧值是普通对象... */;
if (oldIsPlain) {
value = JSON.parse(response);
} else if (oldMeta && oldMeta.xml) {
// 旧条目为 XML:对齐解析回 Document
value = new DOMParser().parseFromString(response, 'text/xml');
}
}
await cachePut(key, url, value, extra);
}
鼓舞する
- IndexedDB に何かを詰め込む前に、構造化クローン作成の境界について考えてください。
DataCloneError多くの場合、上位層のキャッチによって飲み込まれ、「サイレント キャッシュ障害」として現れ、そもそも見つけるのが困難です。 - 「アクセス タイプの調整」はすべての書き込みパスで true である必要があります——最初の書き込み、更新の再検証、およびアライメント分析中にパスが見つからなかった場合、型が一貫していないダーティ データがキャッシュに残されます。
現象
コンソールはアラームを点滅させ続けます。
Refused to get unsafe header "ETag"
根本原因: CORS のデフォルトの公開ヘッダー ホワイトリスト
クロスドメイン応答では、ブラウザは JS の読み取りのみを許可します。単純な応答ヘッダー:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified。ETag サーバー上で明示的に構成されていない限り、リストには含まれません Access-Control-Expose-Headers: ETag。クロスオリジン応答で呼び出される xhr.getResponseHeader('ETag')ブラウザは上記の警告を直接拒否してスローします。
私たちのキャッシュは再検証検証に ETag/Last-Modified/Content-Length の 3 点セットを使用しており、3 次元リソース (タイル、モデル) がたまたまクロスドメイン OSS/CDN 上にあるため、検証が実行されるたびにアラームが発生します。
修正: 警告なしで読み取りが低下する
xhr.getAllResponseHeaders() 特徴があります:それはCORS によって公開されたヘッダーのみが返され、アラームはトリガーされません。。次に、元のテキストから手動で解析します。
function getHeaderSafe(xhr, name) {
const lines = (xhr.getAllResponseHeaders() || '').split(/\r\n/);
const lower = name.toLowerCase();
for (let i = 0; i < lines.length; i++) {
const idx = lines[i].indexOf(':');
if (idx > -1 && lines[i].slice(0, idx).trim().toLowerCase() === lower) {
return lines[i].slice(idx + 1).trim();
}
}
return null;
}
ETagが取得できない場合は戻る null検証チェーンは自動的に Last-Modified / Content-Length の比較に縮退します。これはすでに存在するため、機能はそのままですが、整合性ネゴシエーションの精度は低下します。根本的な解決策は、サーバー側に行を追加することです Access-Control-Expose-Headers: ETagただし、リソース サービスが自分の手元にない場合は、クライアントをダウングレードして読み取るのが最も現実的な解決策です。
鼓舞する
- CORS 公開ヘッダーは次のとおりです。サーバー構成クライアントがそれを読み取れない場合、それはバグではなく仕様です。
- 検証ロジックにすでにダウングレード チェーンがある場合、クライアントが実行できるのは、「アラームをトリガーせずにダウングレードされた読み取り」です。
添付ファイル: 同じ提出で簡単に修正された 2 つの小さな問題。
1. シームレススクロールクローンDOM、Vueイベントは無効です。 vue-seamless-scroll 実装では、リスト DOM をコピーしてシームレスな接続を確立します。クローン化されたノードには Vue イベント バインディングがなく、下半分をクリックしても応答がありません。解決策はイベント委任です。ルート ノードは統合されます。 @click、使用 e.composedPath() 目的のカードを見つけてから開始します data-obj データを取得します。ちなみに、最適化を行いました。data-obj すべてのレンダリングが JSON.stringify データ項目全体が保存されるだけになりました { name, index }クリックしたらリストを確認してください:
getDataObj(name, index) {
return JSON.stringify(index === undefined ? { name } : { name, index });
}
2. v-for 鍵は使わないでください Math.random()。 元のコードに主キーがない場合は手がかりがありません Math.random()——キーが再レンダリングされるたびに、すべてのキーが変更されます。これは、Vue にすべてのノードの破棄と再構築を強制することと同じであり、スクロール リストは直接ブラインド化されます。ビジネス主キーがない場合のフォールバック index それで十分です。
最後に書きます
これら 3 つの落とし穴を検討すると、共通点が非常に明確になります。
| ピット | 境界 |
|---|---|
| 視聴者は二度目の完全破壊に入る | コンポーネントのライフサイクル ↔ モジュールレベルのグローバル状態 |
| データクローンエラー | メモリオブジェクト ↔ IndexedDB の永続性 |
| 安全でないヘッダーの警告 | ページ JS ↔ 同一生成元ポリシー |
境界における問題には次のような共通の特徴があります。単一のテストと従来の共同デバッグをカバーするのは困難——実際に「終了して再入力」し、実際に「XML をキャッシュ」し、実際に「ドメインを越える」必要があります。したがって、この種の問題はチェックリストにまとめて解決する価値があります。グローバル参照をクリーンアップするのは誰ですか?永続型は揃っていますか?クロスドメインヘッダーを読み取ることはできますか?
この落とし穴の記録がいくつかの落とし穴を避けるのに役立つことを願っています。