👏

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