ブラウザで使うデバッグ技術
(alan.norbauer.com)- ブラウザの開発者ツールを中級以上に扱えるなら、条件付きブレークポイントを単なる停止条件ではなく、ログ、トレース、状態変更、時間測定ツールへ拡張できる
console.log、console.count、console.trace、console.timeなどの Console API をブレークポイントや Watch Pane に入れると、実行を止めずにフローと状態を記録できる- 関数の引数の数、時間、CSS の計算値、実行回数、ランダムサンプリング、グローバルな boolean を組み合わせると、停止するタイミングをはるかに精密に制御できる
- Chrome の
monitor、debug、copy、$0、getEventListeners、monitorEventsは、関数呼び出し、DOM、イベント、クリップボード操作をコンソールから素早く扱えるようにする - DOM が JS やユーザーイベントによって継続的に変わる画面では、JS の一時停止、
setTimeout(debugger)、DOM スナップショット、フォーカス監視が HTML/CSS デバッグに特に有用
条件付きブレークポイントをデバッグツールへ拡張する
-
条件付きブレークポイントには、真偽条件だけでなく 副作用のある JavaScript 式も入れられる
-
これを活用すると、基本的なブレークポイントをログ、トレース、状態変更、時間測定ツールのように使える
-
ログポイントとトレースポイント
- 条件付きブレークポイントで
console.logを呼び出すと、実行を止めずにコンソールへ値を残す ログポイントのように動作する console.countを使うと、その行が何回実行されたかもあわせて数えられる- 2020年5月のアップデート時点で、主要ブラウザはログポイント/トレースポイントを直接サポートしている
- 条件付きブレークポイントで
-
Watch Pane で Console API を使う
- Watch Pane にも
console.logのような式を入れられる - アプリケーションがデバッガで停止するたびに、
console.table(localStorage)で localStorage のスナップショットを取れる - DOM 変更ブレークポイントと Watch 式を併用すると、DOM サブツリー変更後のスナップショットを配列に保存できる
(window.doms = window.doms || []).push(document.documentElement.outerHTML)- DOM 変更ブレークポイントは、実行を止めないようにはできない
- Watch Pane にも
-
コールスタックのトレース
- ローディングスピナーを表示する関数と隠す関数があり、表示呼び出しだけが残る問題を探すときは、
console.traceを条件付きブレークポイントに入れられる - 表示関数の最後のスタックトレースで呼び出し元をクリックすると、その呼び出し位置へ移動できる
- ローディングスピナーを表示する関数と隠す関数があり、表示呼び出しだけが残る問題を探すときは、
-
実行中にプログラムの動作を変える
- 条件付きブレークポイント式で関数の引数を上書きすると、ブラウザ内でプログラムの動作を即座に変えられる
- たとえば
getPerson関数のid値を変えられる id=1のように true と評価される式はデバッガを停止させるため、止めたくない場合は, falseを付け足せる
-
すばやい性能測定
- 条件付きブレークポイントの評価時間が混ざるため、精密な性能プロファイリングには向かない
- すばやく測定したい場合は、開始地点の条件に
console.time('label')、終了地点の条件にconsole.timeEnd('label')を入れて、実行時間をコンソールに残せる
停止条件をより精密にする
-
関数の引数の数を活用する
- 現在の関数が3個の引数で呼び出されたときだけ止めるには、次の条件を使える
arguments.callee.length === 3- オプション引数を持つオーバーロード関数で有用
- 定義された引数の数と実際に渡された引数の数が異なるときだけ止めるには、次の条件を使える
(arguments.callee.length) != arguments.length- 関数呼び出し位置のバグを見つける助けになる
-
時間ベースの条件
- ページ読み込み後5秒が経過するまでは止めたくない場合、次の条件を使える
performance.now() > 5000- 初期ページ読み込み後だけブレークポイントに関心がある場合に適している
- ブレークポイントが設定された後の次の5秒間は止まらず、その後は止まるようにするには、基準時刻をグローバル変数に保存できる
window.baseline = window.baseline || Date.now(), (Date.now() - window.baseline) > 5000- コンソールで
window.baseline = Date.now()としてカウンターを再開できる
-
CSS 状態ベースの条件
- 計算済み CSS 値を条件にして停止できる
- たとえば
document.bodyの背景色が赤のときだけ止めるには、次の条件を使える
window.getComputedStyle(document.body).backgroundColor === "rgb(255,0,0)" -
実行回数とサンプリング
- 行が実行されるたびにカウンターを増やし、2回に1回だけ止められる
window.counter = (window.counter || 0) + 1, window.counter % 2 === 0- ランダムサンプリングで10回に1回だけ止めるには、次の条件を使える
Math.random() < 0.1 -
特定の位置では絶対に止まらないようにする
- Chrome で gutter を右クリックして “Never Pause Here” を選ぶと、常に
falseである条件付きブレークポイントが作成され、その行では止まらなくなる - XHR ブレークポイントで特定の行を除外したり、特定の例外を無視したりするときに活用できる
- Chrome で gutter を右クリックして “Never Pause Here” を選ぶと、常に
-
インスタンス ID の自動付与
- クラスコンストラクターに条件付きブレークポイントを入れて、各インスタンスを配列に保存できる
(window.instances = window.instances || []).push(this)- 特定インスタンスの一意な ID は配列インデックスで確認できる
window.instances.indexOf(instance) -
ブレークポイントのグローバルトグル
- 1つ以上の条件付きブレークポイントをグローバル booleanで制御できる
- コンソールで直接変える、別のブレークポイントで変える、タイマーで変えるといった方法が可能
window.enableBreakpoints = true setTimeout(() => (window.enableBreakpoints = true), 5000)
関数とクラス呼び出しのトレース
-
クラスメソッド呼び出しの監視
- Chrome の
monitorコマンドラインメソッドを使うと、クラスメソッド呼び出しを簡単にトレースできる Dogクラスのすべてのインスタンスに対するメソッド呼び出しを見たいなら、prototype のすべてのプロパティにmonitorを適用できる
var p = Dog.prototype; Object.getOwnPropertyNames(p).forEach((k) => monitor(p[k]));- コンソールには
function bark called with arguments: 2のような呼び出し情報が出力される - ログだけを残す代わりに呼び出し時に実行を止めたいなら、
monitorの代わりにdebugを使える
- Chrome の
-
特定インスタンスでクラスをトレースする
- クラス名は分からないがインスタンスがある場合、コンストラクターの prototype を通じて同じ方法で監視できる
var p = instance.constructor.prototype; Object.getOwnPropertyNames(p).forEach((k) => monitor(p[k]));- 特定のクラスに固定せず、任意のインスタンスに対して動作する関数を作るときに有用
-
コンソールで関数呼び出し直前にデバッグする
- デバッグしたい関数をコンソールから呼び出す前に
debuggerを実行し、次の関数呼び出しへ Step into すると関数実装に入れる
debugger; fn(1);- 関数定義の場所を探すのが面倒な場合や、
fnが動的にバインドされていてソース位置が分からない場合に有用 - Chrome では
debug(fn)を呼び出して、fnが呼び出されるたびに関数内部で止めることもできる
- デバッグしたい関数をコンソールから呼び出す前に
URL 変更とプロパティ読み取りのデバッグ
-
SPA ルーティング前に止める
- シングルページアプリケーションが URL を変える直前に止めるには、
history.pushState、history.replaceState、window.onhashchange、window.onpopstateにdebuggerを入れた関数を接続できる
const dbg = () => { debugger; }; history.pushState = dbg; history.replaceState = dbg; window.onhashchange = dbg; window.onpopstate = dbg;- このバージョンの
dbgには、ナビゲーションを壊さずに停止する実装までは含まれていない window.location.replaceやwindow.location.assignを直接呼び出す場合は、代入直後にページがアンロードされるため、この方法では処理できない- Chrome では関連メソッドに
debugを設定して、リダイレクト元とその時点の状態を確認できる
debug(window.location.replace); debug(window.location.assign); - シングルページアプリケーションが URL を変える直前に止めるには、
-
プロパティ読み取り地点を見つける
- オブジェクトの特定プロパティが読み取られる瞬間を知りたいなら、getter の中に
debuggerを入れられる
{ get configOption() { debugger; return true; } }- 設定オプションをどのコードがどのように使っているか確認するときに有用
- 元のソースで変更するか、条件付きブレークポイントを使って適用できる
- オブジェクトの特定プロパティが読み取られる瞬間を知りたいなら、getter の中に
コンソールでデータをコピーする
copy()Console API を使うと、文字列が切り詰められることなくブラウザの情報をクリップボードへコピーできる- 現在の DOM スナップショット、画像のようなリソースメタデータ、大きな JSON blob、localStorage ダンプをコピーするときに使える
copy(document.documentElement.outerHTML)
copy(performance.getEntriesByType("resource"))
copy(JSON.parse(blob))
copy(localStorage)
HTML/CSS デバッグ
-
JS 実行を止めた状態で DOM を検査する
- DOM inspector で Chrome/Windows 基準の
ctrl+\を押すと、いつでも JS 実行を止められる - JS が DOM を変えたり、mouseover のようなイベントが DOM を変えたりする状況でも、現在の DOM スナップショットを安定して検査できる
- DOM inspector で Chrome/Windows 基準の
-
消える要素を検査する
- マウスを移動すると消える条件付き DOM 要素は、コンソールに5秒タイマーを仕掛けてデバッガを実行すると検査できる
setTimeout(function() { debugger; }, 5000);- 5秒間 UI を表示した後に JS 実行が止まると要素が消えないため、開発者ツールへ移動して検査できる
- JS 実行が止まっている間は、要素の検査、CSS 編集、JS コンソールコマンドの実行が可能
- カーソル位置やフォーカスに依存する DOM を検査するときに効果的
-
DOM スナップショットを記録する
- 現在の DOM をコピーするには、次のコマンドを使える
copy(document.documentElement.outerHTML);- 1秒ごとに DOM 文字列を配列へ保存するには、次のように
setIntervalを使える
doms = []; setInterval(() => { const domStr = document.documentElement.outerHTML; doms.push(domStr); }, 1000);- 配列に保存せず、コンソールへ継続的に出力することもできる
-
フォーカスされた要素を監視する
document.activeElementを定期的に比較して、フォーカス変更をコンソールへ出力できる
(function () { let last = document.activeElement; setInterval(() => { if (document.activeElement !== last) { last = document.activeElement; console.log("Focus changed to: ", last); } }, 100); })(); -
太字表示された要素を見つける
- すべての要素から、計算済みの
fontWeightが"bold"または"700"である要素を見つけられる
const isBold = (e) => { let w = window.getComputedStyle(e).fontWeight; return w === "bold" || w === "700"; }; Array.from(document.querySelectorAll("*")).filter(isBold);- 現在 inspector で選択している要素の子孫だけを探すには、
$0を基準に検索できる
Array.from($0.querySelectorAll("*")).filter(isBold); - すべての要素から、計算済みの
-
選択された要素とイベントを扱う
- コンソールの
$0は、Element Inspector で現在選択されている要素を指す自動参照 - Chrome と Edge では、直前に検査した要素を
$1、その前の要素を$2のようにアクセスできる - Chrome では、現在選択中の要素のイベントリスナーを確認できる
getEventListeners($0)- 選択された要素のすべてのイベントをデバッグするには、
monitorEvents($0)を使える - 特定のイベントグループだけを見たい場合は、配列を渡せる
monitorEvents($0, ["control", "key"]) - コンソールの
1件のコメント
Hacker Newsのコメント
この数十年で、ブラウザ内蔵のデバッグツールは本当に大きく進化した。
JSを長く使ってきた身として、ブラウザ内でコードのデバッグをここまで直感的にしてくれた人たちには深く感謝している。
バックエンドや他の言語など別の開発領域に行くと、現代のブラウザが標準で提供しているこのデバッグツールのエコシステムが恋しくなる。
Python、Java、C++で何年もかなりデバッグしてきたが、ツールが足りないと感じたことはなかった。
もちろん、この分野で使える選択肢を知らない人はかなり多く見てきた。
あの地獄よりはコンソールを選ぶ。
setTimeout(function() { debugger; }, 5000);は賢い。結局、Chromeデバッガが自分自身をデバッグする再帰的な亀のスタックに勝つ唯一の方法は、
debugger文だけだ。悪名高いMyspace Sammyワームのsam.plも、訪問者が難読化されたHTMLホームページを掘り下げられないようにデバッグの罠を使っていた。
メインループを回しながらあちこちに厄介な
debugger;文を挿入し、30FPS / 32ms間隔で実行してDevToolsを使い物にならなくする。debugger文を「無視」する方法がないからだ。cmd-\を使えばいい。現在実行中だったJSの行でページが止まり、問題の要素をいつものように検査できる。
debugger文を無視**してほしい。正当な
debugger文の使用例のほとんどは、自分のコードで作業中なら単なるブレークポイントで置き換えられる。queryObjectsが抜けているのが目についた。特定のコンストラクタで作られたすべてのオブジェクト一覧を返す、かなりぶっ飛んだAPIだ。
例えば
queryObjects(Function)で、ヒープ上にあるすべての関数一覧を取得できる。あるモジュール内に入っている「private」関数まで返してくれる。
本当にぶっ飛んだ機能で、おそらくChromium専用だと思う。
ウォッチ式はどうにもまともに使いこなせない。
スコープと更新ルールがあまりにも分かりにくく、グローバル変数しか監視できないのかと思うが、それすら期待どおりに動かず、テスト時には結局ログを値で埋め尽くすことになる。
コンソールにData.gui [1]風のUIを入れて、変数や設定値を見ながらテストできるようにすべきだと何年も考えてきた。
このCodePen [2]で実際の動作を見られる。
ブラウザには理論上は優れたデバッグ機能がたくさんあるが、実際には安定して動作しているようには思えない。
ブレークポイントでさえ、すべて安定してヒットさせることができない。
コードが展開されているときは大丈夫だが、バンドルされただけで、ミニファイしていなくても多くのデバッガ機能が壊れるように感じた。
フロントエンドエンジニアではないので、たぶん何か間違ったことをしているのかもしれない。
フラストレーションの原因である可能性が高いし、オンの状態で能動的にデバッグする体験がこれほど悪いのに、なぜデフォルトでオンになっているのか今でも理解できない。
IIFE のスコープ内で、コード実行を止めずに IIFE のローカル変数へアクセスする方法があるとよいのですが。
デバッガを説得してそうさせる方法はあるのでしょうか?
https://en.m.wikipedia.org/wiki/Immediately_invoked_function...
IIFE の外に完全にいるなら不可能だと思います。
探している変数は、実行前後にはメモリ上に存在しない可能性もあります。
JavaScript には他の言語のような静的変数がないので、IIFE が呼び出されるたびに内部変数は捨てられます。
JavaScript は Web Worker のようなものを使わない限りかなり強いシングルスレッドモデルなので、
async/await呼び出しで直接レースコンディションを作ろうとしているのでなければ、IIFE スコープの外にそうした変数がメモリ上に残っているという概念自体が成り立ちにくいです。varを乱用して IIFE スコープの外で変数を初期化すれば、最後の IIFE 呼び出しが作った値は見られます。少し修正は必要ですが、労力に対する効果は大きいです。
https://github.com/kristopolous/_inject
任意の時点で、ほぼどんな関数コンテキストでも渡り歩き、何が起きていたかを見ることができます。
参照カウンタを悪用して、コンテキストが破棄されないようにする方式です。
クライアント側 JS をたくさんやっていた頃には本当に有用でした。
これをきちんと作ったキラーアプリ版は、どのコンテキストでも REPL を開ける形になるでしょう。
今の状態でもうまく使うにはかなり熟練が必要です。
https://firefox-source-docs.mozilla.org/devtools-user/debugg...
追記: リンク先の記事で文字どおり最初に扱っている内容だと、たった今気づきました。
[[Scopes]]疑似プロパティを見ればよいです。Firefox には方法がないようです。
完全を期すなら Werkzeug を勧められます。
Django バックエンド開発で使っていますが、ものすごく便利です。
例外が発生する場所なら、いつでもどこでもブラウザ内で「PDB」シェルを使えるようにしてくれます。
1/0を入れるだけでよいので簡単です。とても単純で直感的です。
django-extension/runserver_plusが必要ですか?いつも使っているコツの一つは、UI 文字列で読み込まれたスクリプトを検索してデバッグすることです。
Network パネルに移動してネットワークリクエストの記録を開始し、左サイドバーを開いて、探したいコードや UI 文字列を検索します。
たいてい怪しいバンドル JS チャンクファイルの中で見つかり、結果をクリックするとそのファイルのネットワークリクエストが開きます。
そのファイル内のどこかで右クリックして「Open in Sources」のような項目を選ぶと、デバッガに移動します。
あとは
debugger文を置けばよく、この過程でソースマップもおそらく読み込まれます。Python/Elixir プログラマとして長く働いてきて、
pdb.set_trace()とIEx.pry()をよく使ってきました。最近、かなり汚い NodeJS バックエンドを引き継いだのですが、まともなデバッグツールなしで残り少ない髪をかきむしっています。
console.logデバッグに戻りましたが、原始人になった気分です。これほど人気のあるエコシステムにまともなデバッグ REPL がないとは信じられません。どこへ行けばよいか教えてもらえますか?
node --inspect-brkを使えばよく、VS Code や Chrome DevTools で接続できます。難しい部分は、TypeScript 変換のようなビルドツールが
nodejsコマンドの前にあるかどうかです。単に
nodeを実行しているならかなり簡単です。デバッグ REPL は JavaScript コンソールそのものです。
デバッグオプションとして追加すれば、ブレークポイントを設定して関数の中へステップ実行できます。
ほとんどがオブジェクト内にカプセル化されているので、JavaScript はかなりデバッグしやすいです。
--inspectまたは--inspect-brkです。VSCode では “open JavaScript debug terminal” コマンドも使えます。
このコマンドは、どんな
nodeコマンドでも自動的にデバッガが付いた状態で起動されるターミナルを開いてくれます。https://pypi.org/project/snoop/
深いトレース用のデコレータです。
Node JS エコシステムではこの機能を見たことがありません。
“Debugging Property Reads” セクションで、条件付きブレークポイントを使って
{configOption: true}を{get configOption() { debugger; return true; }}にどう変えるのか気になります。コンソールで値を getter で上書きすればよく、書き込み権限があるならソースコードで直接変えてもかまいません。
これはぜひ学ぶべきもののように思うけれど、専用の本や学習資料があるのか気になる
それとも、ひたすら Web 開発とフロントエンドを掘り下げるしかないのだろうか?
作業の真っ最中にリリースノートを読もうとリンクをクリックする気にはならないかもしれないが、新バージョンが出るたびに 5 分だけ目を通すだけでも十分価値がある
数分で見られて消化しやすい動画もあり、簡単な概要をつかめる
目的は新機能の紹介だが、経験上、ツールの現在の限界もよく理解できる