Hello there, ('ω')ノ
DOMで追加された要素はなぜ「ソース」に無いのに「Elements」では見えるのか
document.write と DevTools で読み解く表示の流れ
この記事では、次の2点をセットで解説します。
- 要素(Elementsパネル)にあるのに、ページのソースには無い理由
- document.write を使ったページの表示の流れ(サーバ + ブラウザ)
なぜ「ソース」には無くて「Elements」にはあるのか
- ページのソース表示(View Source) は、サーバから受け取った 初期HTML(生のレスポンス) をそのまま見せます。
JavaScript があとから DOM を変更しても ソース表示には反映されません。 - Elements(要素)パネル は、JavaScript 実行後の “現在のライブDOM” を表示します。
document.write/innerHTML/appendChildなどで追加・変更されたノードも すべて見えます。
今回のケースでは、インラインスクリプトがパース中に実行されて以下を差し込みます:
document.write('<img src="/resources/images/tracker.gif?searchTerms=' + query + '">');
この <img> は 初期HTMLには存在しない ため、ソースには出ず、Elementsには出る――これが答えです。
ライブDOMをテキストで確認したいときは Console で
document.documentElement.outerHTMLを見ると、挿入後のHTML全体が取得できます。
表示までの流れ(サーバ + ブラウザ)
1) ユーザの操作
- 画面の検索フォーム
<form action="/" method="GET">にキーワード(例:test)を入力して送信。 - ブラウザが
GET /?search=testをサーバへ送る。
2) サーバ側(サーバサイドレンダリング)
- サーバはクエリ
search=testを受け取り、ブログ記事を検索。 ヒット件数(ここでは 0)と検索語(
'test')をテンプレートに埋め込んで HTML を生成。例:
<h1>0 search results for 'test'</h1>
生成済みの HTML をレスポンスとして返す。
※ この「
0 search results for 'test'」はクライアント側JSではなく サーバ側 で組み立てられて返ってきている。
3) ブラウザのHTMLパースと描画
- 受け取ったHTMLを順にパースして描画。
<section class="blog-header">内の
<h1>0 search results for 'test'</h1>
がそのまま画面に表示される。
4) クライアント側JavaScriptの実行(トラッキング用)
- インラインスクリプトが実行される:
function trackSearch(query) {
document.write('<img src="/resources/images/tracker.gif?searchTerms='+query+'">');
}
var query = (new URLSearchParams(window.location.search)).get('search');
if (query) {
trackSearch(query);
}
window.location.search(例:?search=test)をURLSearchParamsで解析し、query = "test"を取得。trackSearch("test")が呼ばれ、document.writeにより以下が スクリプトの位置 に挿入される:
<img src="/resources/images/tracker.gif?searchTerms=test">
- その結果、
/resources/images/tracker.gif?searchTerms=testへのリクエストが送られ、トラッキングが行われる。 ※document.writeは パース中 に実行されると、その位置に書き込みを行う(後からDOMに append するのではなく、文書ストリームへ書くイメージ)。
5) 結果画面
- 上部(ブログヘッダ)にはサーバが埋め込んだ
<h1>0 search results for 'test'</h1>
が表示。
検索結果リストは 0 件なので、
<section class="blog-list no-results">内の「Back to Blog」リンクのみが見える。同時に、トラッキング用
<img>も読み込まれている。
Elementsパネルで実際の <img> を確認する手順
すぐに該当ノードへジャンプ(最短)
Console に以下を入力し Enter:
const el = document.querySelector('img[src*="tracker.gif"]'); inspect(el); // Elementsパネルでそのノードにフォーカス
Elements内検索
- Elementsをアクティブにして
Ctrl + F(Mac:Cmd + F) - 検索語:
tracker.gif/img[src*="tracker.gif"]/searchTerms=
挿入の瞬間を捕まえる(DOMブレークポイント)
- Elements で
<section class="search">か<body>を右クリック - Break on → Subtree modifications
- リロードすると、挿入時に Sources で停止し、Call Stack から発火元へ辿れる
位置の目安:今回のHTML構造では
<section class="search">と<section class="blog-list">の間(インライン<script>の直後)に<img>が現れます。
実装・調査のTIPS
document.write 実行時に必ず止める(発火元特定)
// Consoleで実行 → リロードすると document.write 実行時に停止 debug(Document.prototype.write); // または debug(document.write) // 調査終了後に解除 undebug(Document.prototype.write);
ライブDOMをテキストで確認
document.documentElement.outerHTML
セキュリティのポイント(DOM XSS の危険)
- 「
0 search results for 'test'」の生成は サーバ側ロジック。JS は件数や見出しの生成に関与していません。 - JS は
location.searchから検索語を取り出し、document.writeでトラッキング用<img>を差し込むだけ。 - 未エスケープの
queryを文字列連結してdocument.writeに流し込むのは危険です。 悪意ある入力で<script>などを注入され、DOM XSS に繋がります(本ラボの主題)。
まとめ
- ソース表示 = 初期HTML(静的)、Elements = 実行後DOM(動的)。
document.writeなどで追加された要素はソースには無いが Elements には見える。 - 表示の流れは「ユーザ入力 → サーバで件数表示 → クライアントJSが
document.writeで<img>を挿入」。 - 調査は Elements検索/inspect()、DOMブレークポイント、
debug(Document.prototype.write)が即効。 - セキュリティ的には、入力の適切なエスケープと、
document.writeの利用回避を検討する(createElement+setAttributeなど)。
Best reagards, (^^ゞ