Salesforce Lightning × Chrome拡張の罠:SPA の hidden DOM に注意!
はじめに
- 業務効率化のために、Salesforce の画面から情報を取得する Chrome拡張 を作成しています。
- Salesforce のオブジェクト詳細ページ(CASE など)に表示されている項目を抽出し、
別ツールへ渡す処理を実装していたところ、一部のフィールドが正しく取得できないケースが発生しました。 - 原因の特定に少し時間がかかったため、同じように Salesforce Lightning を扱う方の参考になればと思い、今回の発見を Tips としてまとめておきます。
起きた問題
-
複数のレコード詳細ページを順番に開きながら、Chrome拡張で特定の項目(件名・顧客名など)を取得する処理を実装していたところ、今開いているページの値ではなく、過去に開いたページの情報が表示されてしまうケースが発生しました。🤯
-
当初は、
- DOM の構造がおかしい?
- 自分のスクリプトが以前の値を保持してしまっている?
- キャッシュの問題?
…など様々な要因を疑いましたが、根本原因の特定にかなり時間を要しました。
-
調べてみると、Salesforce Lightning は SPA(Single Page Application)のため特別な対応が必要なことがわかりました。。。
SPA(Single Page Application)とは
-
ページを1回だけ読み込み、その後は全部 JavaScript で画面を書き換える仕組みの Web アプリ
- URLだけ変わる
- 画面の中身はJavascriptが勝手に書き換わる
- ページ全体の再読み込みはしない
-
Salesforceを見て見ても、URLは変わっているのにページ全体の再読み込みがされていない(画面が白くならない)と違和感を感じ。。。。そんな仕組みがあるとは初めてしりました💦😲
その他有名なSPAサービス
- SNS(X、Facebook、Instagram)
- ビジネスツール(Jira、Notion、Confluence、Salesforce Lightning)
- メールサービス(Gmail、Outlookなど)
などなど
※主要サービスの大半はSPAで動いていると知って驚き😲
原因特定
調べてみると、Salesforce Lightning は SPA(Single Page Application)のため、
レコードを切り替えても 前のレコードの DOM が非表示のまま残り続けることが分かりました。
例えば、CASE A → CASE B と移動した場合、HTML はこうなっています:
<!-- 👇 CASE A(前に開いていたレコード。非表示) -->
<div class="field" style="display:none">
<span class="label">顧客名</span>
<a href="/A">株式会社AAA</a> <!-- ← これが残る -->
</div>
<!-- 👇 CASE B(現在表示中のレコード) -->
<div class="field">
<span class="label">顧客名</span>
<a href="/B">株式会社BBB</a>
</div>
画面上では CASE B だけ見えていても、
HTML 上には CASE A の要素がそのまま残っている状態です。
この状態で、
document.querySelector(".field a");
のように 最初の一致だけ取得するコードを書くと、
見えないけど DOM 上では上にある非表示の CASE A の <a> が先にヒットする
という現象が発生し、
👉 「今見ているレコードではなく、前のレコードの顧客名」を拾ってしまう
という問題につながっていました。これはSalesforceならではのトラップのようです!😱
解決方法
- 根本原因は、Salesforce Lightning が過去レコードの hidden DOM を残すことでした。
そこで「非表示の DOM を除外して、画面に見えている要素だけを採用する」仕組みを入れました。 - 初心者だったので、CursorでAIに相談しながら作り上げました!
* GPT-5さんのご提案は「content.js に isElementVisible() という可視判定関数を追加し、
フィールド抽出時は必ずこの関数を通してフィルタリングする」こと!
// ---------------------------------------------
// 要素が「画面に実際に表示されているか」を判定するヘルパー関数
// Salesforce Lightning は hidden の旧DOMが残るため、
// 値取得前に必ずこの関数でフィルタリングします。
// こちらはサンプルです!
// ---------------------------------------------
function isElementVisible(el) {
if (!el) return false;
// Lightning がよく使う aria-hidden 系の非表示要素
if (el.closest('[aria-hidden="true"]')) return false;
// レイアウト上表示されていない(描画領域がない)
const rects = el.getClientRects();
if (rects.length === 0) return false;
const style = window.getComputedStyle(el);
// display:none / visibility:hidden / opacity:0 の場合
if (
style.display === "none" ||
style.visibility === "hidden" ||
parseFloat(style.opacity || "1") === 0
) {
return false;
}
// offsetParent が null → 多くの場合は非表示扱い
// ※ position:fixed の場合だけ例外
if (el.offsetParent === null && style.position !== "fixed") return false;
return true;
}
// ---------------------------------------------
// 使い方の例:見えているケースタイトルだけ取得したい場合
// ---------------------------------------------
const candidate = document.querySelector('.case-title');
if (isElementVisible(candidate)) {
console.log('現在表示されているケースのタイトル:', candidate.innerText);
}
まとめ(これだけ覚えておいて)
- Salesforce Lightning は SPA なので、過去のレコードの DOM が非表示のまま残ることがあります。
Chrome拡張で値を取るときは必ず、
👉 “見えている要素だけ” を使う(= isElementVisible でフィルタする)
これだけ覚えておけば、古い値を拾うトラップを避けられます!
Discussion