Claude CodeでjQuery資産を読解 — DOM依存の表にして保守する
10年物のjQueryはHTMLのidやclassに結び付いています。サブエージェントにファイルごとの調査を任せ、セレクタ・イベント・書き換え先・Ajaxの表を作らせる手順を示します。
jQuery資産の保守は、移行計画より先に「どのスクリプトがHTMLのどの属性に依存しているか」の表を作ると進めやすくなります。Claude Codeにはサブエージェントでファイルごとの調査を分けさせ、セレクタ・イベント・書き換え先・Ajaxの宛先を「ファイル:行」付きで返させます。人はその表をgrep -nで抜き取り検証します。
10年物のスクリプトは、$('#cart-total')のような文字列でHTMLと結び付いています。テンプレートのidを一つ直しただけで、エラーも出ずにボタンが反応しなくなります。この記事では、その結び付きを表にして、テンプレートを安全に直せる状態にするまでを扱います。
jQueryとHTMLの間にある「契約」を洗い出す
jQueryのコードは、HTMLに対して次の4種類の約束をしています。
- どの要素を掴むか(セレクタ)
- どの操作に反応するか(イベント)
- どの要素の中身を書き換えるか(書き換え先)
- サーバーのどのURLに何を送るか(Ajax)
サーバー側のテンプレートがid、class、data-属性を守っている間は、この約束が成り立ちます。テンプレートを直す人がスクリプトを読んでいなければ、約束は一方的に破られます。
イベントの結び付きは、委譲イベントでさらに見えにくくなります。jQueryの.on()は、第2引数にセレクタを渡すと委譲イベントになります。ハンドラは親要素に付き、イベントがその親まで伝わったとき、子孫のうちセレクタに合う要素に対してだけ呼ばれます。
$('#cart-list').on('click', '.btn-remove', function () { /* ... */ });この1行には、HTMLへの依存が2つあります。親の#cart-listが存在すること。その中にクリックされる.btn-removeがあること。どちらかのidやclassが変われば、ハンドラは黙って呼ばれなくなります。委譲イベントでは、後から追加された要素にもハンドラが効きます。そのぶん、依存先がページ上のどこにあるのかがスクリプトを読むだけでは分かりません。
表の列を先に決める
調査を頼む前に、返してほしい表の形を決めます。列が曖昧だと、サブエージェントごとに粒度がばらつき、集めたあとで突き合わせられません。
| 列 | 内容 | 例 |
|---|---|---|
| 場所 | 内容ファイル:行 | 例js/cart.js:2 |
| 種別 | 内容選択 / 委譲 / 書き換え / Ajax | 例委譲 |
| 対象 | 内容セレクタの文字列そのまま | 例#cart-list → .btn-remove |
| イベント・操作 | 内容click、submit、.html()など | 例click |
| 備考 | 内容動的に組み立てているなど | 例なし |
「対象」は、整形せず記述どおりに書かせます。後でgrep -nでその文字列を探すためです。種別を4つに固定すると、「イベントの宛先」と「書き換え先」の混同も防げます。
調査用サブエージェントを定義する
サブエージェントは、.claude/agents/にMarkdownファイルを置いて定義します。必須のフロントマターはnameとdescriptionだけです。toolsを省略すると使える全ツールを継承するため、読むだけの調査には絞っておきます。
---
name: jquery-surveyor
description: jQueryのスクリプト1ファイルを読み、DOM依存の表を返す。ファイルは書き換えない
tools: Read, Grep, Glob
model: sonnet
---
渡された1つのJSファイルだけを調べ、次の列のMarkdown表で返す。
列: 場所(ファイル:行) / 種別(選択・委譲・書き換え・Ajax) / 対象 / イベント・操作 / 備考
(テンプレート側の検索結果を頼まれたときは、列「HTML側の出現(有/無)」を足す)
- 対象はコードに書かれた文字列をそのまま写す。整形や推測をしない
- セレクタや URL を変数・文字列連結で組み立てている行は、備考に「動的」と書く
- 行番号は必ずファイルを読んで確かめる。読んでいない行は書かない
- コードを変更しないサブエージェントには、このシステムプロンプトと作業ディレクトリなどの基本情報だけが渡されます。会話の履歴は見えません。そのため、依頼文に「どのファイルか」「列は何か」を毎回書く必要があります。定義に列を書いておけば、依頼文はファイル名だけで済みます。
modelにはsonnetなどのエイリアスを指定できます。読み取り中心の作業なので、メインの会話より軽いモデルに回す選択肢があります。
ファイルごとに並列で調べさせる
親の会話では、ファイルの一覧を作ってから分担を頼みます。
js/ 配下の .js を一覧にして、1ファイルにつき1つ jquery-surveyor に調査させて。
結果は js/ のファイル名順に1つの表へまとめ、docs/dom-contract.md に書いて独立した調査は、複数のサブエージェントに同時に走らせられます。公式ドキュメントも、研究系の作業を並列に分ける使い方を挙げています。注意点も同じ箇所にあります。各サブエージェントの報告は親の会話に戻るため、詳細な結果を大量に返すと親のコンテキストを圧迫します。最初の表を5列に絞るのは、この理由もあります。HTML側の出現のように、検証の段階で必要になった列は、あとから足します。
並列数にも上限があります。既定では同時に20のサブエージェントが走っていると、次の起動はConcurrent subagent limit reachedで失敗します。ファイルが数百ある資産は、フォルダ単位で数十本ずつ頼む方が安全です。上限はCLAUDE_CODE_MAX_CONCURRENT_SUBAGENTSで変えられます。
Claudeがメインの会話から起動するサブエージェントは、何も設定しなければバックグラウンドで動きます。バックグラウンドでは使える組み込みツールが絞られますが、Read、Grep、Globは残るので、上の定義はそのまま動きます。
grep -nで抜き取り検証する
表は抜き取りで確かめてから使います。サブエージェントが返した行番号が合っているかを、人が数件確認します。手順は単純です。
- 表から行を5〜10件、種別が偏らないように選ぶ
- 「対象」の文字列をそのまま
grep -nに渡す - 返ってきた行番号が表と一致するかを見る
手元で、簡単なサンプルを使って試しました。js/cart.jsにカート画面用のスクリプト、tpl/cart.htmlに対応するテンプレートを置いた構成です。
grep -rn "btn-remove" ../js/cart.js:2: $('#cart-list').on('click', '.btn-remove', function () {
./tpl/cart.html:1:<ul id="cart-list"><li><button class="btn-remove" data-item-id="3">削除</button></li></ul>JSの2行目に委譲イベントがあり、テンプレートの1行目に対応するclassがあります。表の「js/cart.js:2」と一致するので、この行は合格です。次にセレクタを一括で拾うgrepも流します。
grep -rnE "\\\$\(['\"]|\.on\(" jsjs/cart.js:2: $('#cart-list').on('click', '.btn-remove', function () {
js/cart.js:5: $('#cart-total').html(html);
js/cart.js:8: $('form.js-coupon').on('submit', function (e) {
js/cart.js:10: $('.coupon-msg').text('確認中');サンプルには$(target)を呼ぶ行もありますが、引用符で始まらないため、この出力には出ません。ここに、grep検証の限界が出ています。
grepに映らない依存と、HTML側の不一致
文字列リテラルのセレクタは、grepでほぼ拾えます。拾えないのは、組み立てるセレクタです。サンプルではvar target = '#cart-' + kind;の後に$(target)が呼ばれています。セレクタはただの文字列なので、連結や変数経由で作られると、静的な検索では対象が確定しません。
だから、定義の「備考に動的と書く」が効きます。表の備考に「動的」が付いた行は、人が実行時の値を追って確かめる候補として別に集めます。
もう一つ、表はJS側の目録でしかありません。サーバー出力のHTMLと突き合わせて初めて「契約」になります。サンプルでは、$('.coupon-msg')に対応する要素がテンプレートに見当たりません。
grep -rn "coupon-msg" ../js/cart.js:10: $('.coupon-msg').text('確認中');JSにしか出てこないセレクタは、すでに死んでいるか、別のテンプレートが出力しているかのどちらかです。どちらなのかは、テンプレートを出力する側のコードを読まなければ決まりません。表に「HTML側の出現」列を足し、tpl/やビューのディレクトリに対する検索結果を、有/無で埋めさせると、この種の行が一覧で浮かびます。
jQuery側の仕様も、読み違えの原因になります。公式ドキュメントによると、.on()で直接結ぶハンドラは、呼び出し時点で存在する要素にだけ付きます。委譲イベントなら、後から追加された要素にも効きます。また、委譲イベントはSVGでは動作しません。表の備考に「委譲」と書いておけば、テンプレートを非同期に差し替える画面で何が起きるかを推測しやすくなります。
落とし穴
階層セレクタは、HTMLの入れ子まで契約に含める
$('body').on('click', '#commentForm .addNew', ...)のような階層セレクタは、idとclassだけでなく、その親子関係にも依存します。jQueryの公式ドキュメントも、階層セレクタは遅くなりやすく、より近い親にハンドラを付ける方がよいと説明しています。表の「対象」には階層のまま書き写し、テンプレートで入れ子を変えるときの確認項目にします。
表をテンプレートの変更に使う
表ができたら、docs/dom-contract.mdとしてリポジトリに置き、CLAUDE.mdから参照させます。
## テンプレートを触るとき
- ビューのid/class/data属性を変える前に docs/dom-contract.md を grep する
- 表に出てくる対象を変えるときは、js/ の該当行(ファイル:行)も同じ変更で直す
- 備考が「動的」の行は、実行時の値を確かめるまで変更しないテンプレートを直す作業では、まず変えたい属性値でgrep -rnを打ち、表と突き合わせます。表に載っていない出現があれば、表の取りこぼしです。取りこぼしが見つかった時点で、その行を表に足します。表は一度作って終わりではなく、保守の中で育てる台帳です。
業務用語とコード名の対応をCLAUDE.mdに置く考え方は、Claude Codeのドメイン用語集をCLAUDE.mdに置くと同じ発想です。セレクタの表は、その画面版にあたります。スクレイパーで同じ「セレクタが静かに壊れる」問題を扱う例は、BeautifulSoupのスクレイパーをfixtureで守る記事にあります。jQuery側のライブラリを更新する場面では、依存のメジャーアップデートの影響調査の突き合わせも使えます。
つまずきやすい点
- サブエージェントが行番号を作る: 読まずに推測した行番号が混ざることがあります。定義で「読んでいない行は書かない」と指示したうえで、抜き取り検証を省かないでください。
- 1ファイルが大きすぎる: 1つのスクリプトが数千行あるときは、行範囲で分けて2回に分けて頼みます。サブエージェントは新しいコンテキストで始まるので、範囲は依頼文に明記します。
maxTurnsで途中停止する:maxTurnsを設定した場合、上限に達すると出力は途中までの扱いで返ります。Claudeは続きを再開できます。表の末尾が欠けていないか、最終行を見てください。- 表を一度で信じる: 検証の抜き取りは、サブエージェントの出力が変わるたびにやり直します。定義の文面を直したあとは特にです。
なお、Claude Codeには読み取り専用の組み込みサブエージェントExploreもあり、コードベースの探索や読解にはそのまま使えます。自作の定義が要るのは、返す表の列と「読んでいない行は書かない」といった規則を固定したいときです。
まとめ
jQuery資産の保守で効くのは、読み方を最初に固定することです。列を決め、1ファイルずつ調べさせ、grep -nで数件を確かめれば、テンプレートを直す人が使える台帳になります。この方法の弱点は、組み立てられたセレクタに静的な検索が届かないことと、HTML側との突き合わせを別に行う必要があることです。その2点を、備考の「動的」と、足した「HTML側の出現」列で表に出しておけば、残る確認作業の範囲が見えます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。