DOM Living Standard
第1部:DOMの基礎
[編集]第1章:DOMとは
[編集]1.1 DOMの概要
[編集]DOMはDocument Object Model(文書オブジェクトモデル)の略称です。DOMは、HTML文書やXML文書をプログラムから操作するための標準的なインターフェースを定義しています。
1.1.1 DOMの歴史
[編集]DOMの歴史は、Webブラウザ間の互換性の問題から始まりました。1990年代後半、NetscapeとInternet Explorerは、それぞれ独自のJavaScriptベースのHTML操作手法を持っていました。この状況を改善するため、W3Cは1998年にDOM Level 1を策定しました。
DOMは、Web技術の進化に伴い、以下の3つのレベルを経て発展してきました。
- 1998年:DOM Level 1 - HTML要素へのアクセス、ノードの操作、基本的なイベント処理。
- 2000年:DOM Level 2 - イベントモデルの強化、スタイルシートオブジェクトモデル (CSSOM) の導入、XMLのサポート
- 2004年:DOM Level 3 - XPathのサポート、バリデーション、キーボードイベントの処理
2015年以降は、WHATWGによってDOM Living Standardとして管理されており、コミュニティ主導で常に最新のWeb技術に対応できるようになりました。
1.1.2 DOMの重要性
[編集]DOMは以下の理由で現代のWeb開発において重要な役割を果たしています:
- HTML文書の動的な操作を可能にする
- ユーザーインタラクションの処理を統一的に扱える
- クロスブラウザの互換性を提供する
- モダンなWebアプリケーション開発の基盤となる
1.2 DOMツリー
[編集]DOMは文書を木構造(ツリー構造)として表現します。この構造をDOMツリーと呼びます。
1.2.1 ツリー構造の基本概念
[編集]DOMツリーは以下の特徴を持ちます:
- 一つのルートノードから始まる階層構造
- 親子関係による要素の表現
- 兄弟要素による水平方向の関係
- 全ての要素、テキスト、属性がノードとして表現される
<!DOCTYPE html> <html> <head> <title>サンプル</title> </head> <body> <h1>見出し</h1> <p>段落</p> </body> </html>
上記のHTMLは、以下のようなツリー構造として表現されます:
html ├ head │└ title │ └ "サンプル" └ body ├ h1 │└ "見出し" └ p └ "段落"
1.3 ノードの概念
[編集]DOMにおいて、ドキュメントを構成する全ての部品は「ノード」として表現されます。
1.3.1 主要なノードタイプ
[編集]ノードタイプ一覧 ノードタイプ 説明 定数値 Element Node HTML要素を表す 1 Attribute Node 要素の属性を表す 2 Text Node テキストを表す 3 Document Node ドキュメント自体を表す 9 DocumentType Node DOCTYPE宣言を表す 10
1.4 ブラウザレンダリングとDOMの関係
[編集]ブラウザがHTML文書を読み込んでWeb画面を表示するまでの過程を理解することは重要です。
1.4.1 レンダリングプロセス
[編集]- HTMLの解析とDOMツリーの構築
- CSSの解析とCSSOMツリーの構築
- レンダーツリーの構築
- レイアウト(リフロー)の計算
- ページの描画(ペイント)
1.4.2 DOMの操作とレンダリングの関係
[編集]DOMの操作は、以下のようなレンダリングへの影響を持ちます:
- 要素の追加・削除:レンダーツリーの再構築が必要
- スタイルの変更:レイアウトの再計算が必要
- テキスト内容の変更:該当部分の再描画のみ
パフォーマンスのベストプラクティス:
- 複数の操作をまとめて行う
- DocumentFragmentの活用
- レイアウトスラッシング(連続的なレイアウト計算)の回避
演習問題
[編集]- DOMツリーの構造を図示してください。以下のHTMLを使用します:
<div class="container"> <h1 id="title">Hello</h1> <p class="text">World</p> </div>
- DOMの歴史において重要な出来事を年表形式でまとめてください。
- ブラウザのレンダリングプロセスについて、各段階で何が行われているか説明してください。
参考文献
[編集]第2章:Node基礎
[編集]2.1 Nodeインターフェース
[編集]Nodeインターフェースは、DOMにおける全てのノードの基本となるインターフェースです。
2.1.1 Nodeの基本的なプロパティ
[編集]主要なNodeプロパティ プロパティ名 説明 例 nodeName ノードの名前 要素の場合はタグ名(大文字) nodeType ノードの種類を表す数値 1=要素、3=テキスト nodeValue ノードの値 テキストノードの場合は文字列 textContent ノード内の全テキスト 子孫要素内のテキストも含む
2.1.2 ノード関係を表すプロパティ
[編集]- parentNode: 親ノード
- childNodes: 子ノードのNodeList
- firstChild: 最初の子ノード
- lastChild: 最後の子ノード
- nextSibling: 次の兄弟ノード
- previousSibling: 前の兄弟ノード
2.2 NodeListとHTMLCollection
[編集]2.2.1 NodeListの特徴
[編集]NodeListは、ノードの集合を表すオブジェクトです。
- 動的vs静的
- getElementsBy*メソッドの返り値は動的(live)
- querySelectorAll()の返り値は静的(static)
// 動的なNodeListの例 const divs = document.getElementsByTagName('div'); // 静的なNodeListの例 const staticDivs = document.querySelectorAll('div');
2.2.2 HTMLCollectionの特徴
[編集]HTMLCollectionは、要素ノードの集合を表すオブジェクトです。
- 常に動的(live)
- 名前付きアクセスが可能
- 要素ノードのみを含む
NodeListとHTMLCollectionの比較 特徴 NodeList HTMLCollection 動的更新 メソッドによる 常に動的 要素以外のノード 含むことがある 含まない forEach対応 ○ × 名前付きアクセス × ○
2.3 ノードタイプと種類
[編集]2.3.1 主要なノードタイプ一覧
[編集]詳細なノードタイプ 定数 値 説明 Node.ELEMENT_NODE 1 HTML要素 Node.ATTRIBUTE_NODE 2 属性 Node.TEXT_NODE 3 テキスト Node.CDATA_SECTION_NODE 4 CDATA セクション Node.PROCESSING_INSTRUCTION_NODE 7 処理命令 Node.COMMENT_NODE 8 コメント Node.DOCUMENT_NODE 9 ドキュメント Node.DOCUMENT_TYPE_NODE 10 DocumentType Node.DOCUMENT_FRAGMENT_NODE 11 DocumentFragment
2.3.2 ノードタイプの判別方法
[編集]// nodeTypeによる判別 if (node.nodeType === Node.ELEMENT_NODE) { console.log('これは要素ノードです'); } // instanceofによる判別 if (node instanceof Element) { console.log('これは要素ノードです'); }
2.4 主要なNodeメソッド
[編集]2.4.1 ノードの操作メソッド
[編集]- appendChild(node)
- 子ノードとして末尾に追加
- insertBefore(newNode, referenceNode)
- 特定のノードの前に挿入
- removeChild(node)
- 子ノードを削除
- replaceChild(newChild, oldChild)
- 子ノードを置換
- cloneNode(deep)
- ノードの複製を作成
2.4.2 メソッドの使用例
[編集]// 新しい要素の作成と追加 const div = document.createElement('div'); const text = document.createTextNode('Hello'); div.appendChild(text); // 要素の複製 const clone = div.cloneNode(true); // 要素の削除 const parent = div.parentNode; parent.removeChild(div);
演習問題
[編集]- 以下のHTMLで、全てのpタグ内のテキストを取得するコードを書いてください:
<div> <p>First paragraph</p> <p>Second <span>paragraph</span></p> <p>Third paragraph</p> </div>
- NodeListとHTMLCollectionの違いを説明し、それぞれの特徴的な使用例を示してください。
- 与えられたノードの全ての子孫ノードを深さ優先で走査するコードを書いてください。
参考文献
[編集]第3章:Element操作の基本
[編集]3.1 Elementインターフェース概要
[編集]ElementインターフェースはNodeインターフェースを継承し、HTML要素を表現するための追加機能を提供します。
3.1.1 ElementとNodeの関係
[編集]ElementとNodeの継承関係 インターフェース 主な機能 Node ツリー構造の操作、基本プロパティ Element 要素の操作、属性へのアクセス HTMLElement HTML固有の機能 具体的な要素(HTMLDivElement等) 要素固有の機能
3.2 要素の取得方法
[編集]3.2.1 単一要素の取得
[編集]要素取得メソッド(単一) メソッド 説明 返り値 getElementById() ID属性による取得 Element querySelector() CSS セレクタによる取得 Element
// ID による取得 const main = document.getElementById('main-content'); // セレクタによる取得 const firstArticle = document.querySelector('article.featured');
3.2.2 複数要素の取得
[編集]要素取得メソッド(複数) メソッド 説明 返り値 getElementsByClassName() クラス名による取得 HTMLCollection getElementsByTagName() タグ名による取得 HTMLCollection getElementsByName() name属性による取得 NodeList querySelectorAll() CSS セレクタによる取得 NodeList(静的)
// クラスによる取得 const items = document.getElementsByClassName('item'); // タグ名による取得 const paragraphs = document.getElementsByTagName('p'); // 複雑なセレクタによる取得 const specialItems = document.querySelectorAll('.container > .item:not(.disabled)');
3.3 属性の操作
[編集]3.3.1 標準属性の操作
[編集]- getAttribute(name)
- setAttribute(name, value)
- hasAttribute(name)
- removeAttribute(name)
const link = document.querySelector('a'); // 属性の取得 const href = link.getAttribute('href'); // 属性の設定 link.setAttribute('target', '_blank'); // 属性の確認 if (link.hasAttribute('rel')) { // 属性の削除 link.removeAttribute('rel'); }
3.3.2 データ属性(data-*)の操作
[編集]const element = document.querySelector('#user-card'); // データ属性の取得 const userId = element.dataset.userId; // データ属性の設定 element.dataset.status = 'active'; // HTML上での表現 // <div id="user-card" data-user-id="123" data-status="active">
3.4 クラスとスタイルの操作
[編集]3.4.1 クラスの操作
[編集]classList プロパティのメソッド:
メソッド 説明 add() クラスの追加 remove() クラスの削除 toggle() クラスの切り替え contains() クラスの存在確認 replace() クラスの置換
const element = document.querySelector('.card'); // クラスの追加 element.classList.add('highlight'); // 複数クラスの追加 element.classList.add('active', 'visible'); // クラスの切り替え(真偽値の指定も可能) element.classList.toggle('expanded', true); // クラスの置換 element.classList.replace('old-theme', 'new-theme');
3.4.2 スタイルの操作
[編集]style プロパティを使用した直接操作:
const box = document.querySelector('.box'); // 個別のスタイル設定 box.style.backgroundColor = '#ffff00'; box.style.width = '100px'; box.style.marginTop = '20px'; // 複数のスタイルをまとめて設定 Object.assign(box.style, { display: 'flex', justifyContent: 'center', alignItems: 'center' }); // getComputedStyleの使用 const computedStyle = window.getComputedStyle(box); console.log(computedStyle.width); // 実際に適用されている幅
3.4.3 CSSカスタムプロパティ(CSS変数)の操作
[編集]// CSS変数の設定 document.documentElement.style.setProperty('--main-color', '#ff0000'); // CSS変数の取得 const mainColor = getComputedStyle(document.documentElement) .getPropertyValue('--main-color');
演習問題
[編集]- 以下のHTMLで、特定の条件に基づいて要素のクラスとスタイルを変更するコードを書いてください:
<div class="card" id="user-profile"> <h2>User Profile</h2> <div class="status" data-status="active"></div> </div>
- querySelectorとgetElementByIdの性能の違いについて説明し、適切な使用場面を示してください。
- 要素のスタイルを変更する際の、直接のstyle操作とクラスの使用の比較を行い、それぞれのメリット・デメリットを説明してください。
参考文献
[編集]第2部:DOM操作の実践
[編集]第4章:DOM操作の基本テクニック
[編集]4.1 要素の作成と追加
[編集]4.1.1 要素の作成
[編集]要素の作成には主に以下のメソッドを使用します:
要素作成メソッド メソッド 説明 createElement() 新しい要素ノードの作成 createTextNode() テキストノードの作成 createDocumentFragment() ドキュメントフラグメントの作成 createComment() コメントノードの作成
// 基本的な要素の作成 const div = document.createElement('div'); const text = document.createTextNode('Hello World'); div.appendChild(text); // 複数の要素をまとめて作成 const fragment = document.createDocumentFragment(); for (let i = 0; i < 3; i++) { const p = document.createElement('p'); p.textContent = `Paragraph ${i + 1}`; fragment.appendChild(p); } document.body.appendChild(fragment);
4.1.2 innerHTML vs createElement
[編集]比較表 手法 メリット デメリット innerHTML - 記述が簡潔
- HTML文字列を直接使用可能
- XSS脆弱性のリスク
- イベントリスナーが消失
- パフォーマンスの問題
createElement - より安全
- 細かい制御が可能
- パフォーマンスが良い
- コードが冗長
- 複雑な構造の作成に手間
// innerHTMLの例 element.innerHTML = '<div class="container"><p>Text</p></div>'; // createElementの例 const container = document.createElement('div'); container.className = 'container'; const p = document.createElement('p'); p.textContent = 'Text'; container.appendChild(p);
4.2 要素の削除と置換
[編集]4.2.1 要素の削除
[編集]要素の削除には複数の方法があります:
// remove()メソッドを使用 element.remove(); // parentNodeを使用 element.parentNode.removeChild(element); // innerHTML parent.innerHTML = ''; // 子要素の削除 while (parent.firstChild) { parent.removeChild(parent.firstChild); }
4.2.2 要素の置換
[編集]// replaceChildの使用 const oldElement = document.getElementById('old'); const newElement = document.createElement('div'); oldElement.parentNode.replaceChild(newElement, oldElement); // replaceWithの使用 oldElement.replaceWith(newElement);
4.3 要素の複製
[編集]4.3.1 cloneNode()メソッド
[編集]// 浅いコピー(shallow clone) const clone = element.cloneNode(false); // 深いコピー(deep clone) const deepClone = element.cloneNode(true); // イベントリスナーの複製 function cloneWithEvents(element) { const clone = element.cloneNode(true); // イベントリスナーを手動で再付与する必要がある return clone; }
4.4 テキストとHTML操作
[編集]4.4.1 テキスト操作
[編集]テキスト操作プロパティの比較 プロパティ 説明 特徴 textContent 要素内の全てのテキスト HTMLタグをエスケープ innerText 表示されているテキスト レイアウトに依存 innerHTML HTML文字列 タグを解釈
// textContentの使用 element.textContent = 'Plain text & <b>bold</b>'; // タグがエスケープされる // innerTextの使用 element.innerText = 'Visible\ntext'; // CSSを考慮 // innerHTMLの使用 element.innerHTML = '<b>Bold</b> text'; // HTMLとして解釈
4.4.2 安全なHTML操作
[編集]// DOMPurifyなどのサニタイザーの使用 import DOMPurify from 'dompurify'; function setSecureHTML(element, html) { const clean = DOMPurify.sanitize(html); element.innerHTML = clean; } // テンプレートリテラルを使用した安全な構築 const name = 'User'; const div = document.createElement('div'); div.innerHTML = ` <h1>Welcome ${name}</h1> <p>Your account was created successfully.</p> `.trim();
4.5 パフォーマンス最適化
[編集]4.5.1 DocumentFragment
[編集]// 多数の要素を追加する場合 const fragment = document.createDocumentFragment(); const items = ['Item 1', 'Item 2', 'Item 3']; items.forEach(text => { const li = document.createElement('li'); li.textContent = text; fragment.appendChild(li); }); // 一度のDOM操作で追加 document.querySelector('ul').appendChild(fragment);
4.5.2 バッチ処理
[編集]// requestAnimationFrameを使用した最適化 function batchDOMUpdates(updates) { requestAnimationFrame(() => { updates.forEach(update => { // DOM更新処理 update(); }); }); } const updates = [ () => element1.style.width = '100px', () => element2.classList.add('active'), () => element3.textContent = 'Updated' ]; batchDOMUpdates(updates);
演習問題
[編集]- 以下のデータ構造をDOMに効率的に描画するコードを書いてください:
const data = [ { id: 1, title: 'Item 1', description: 'Description 1' }, { id: 2, title: 'Item 2', description: 'Description 2' }, { id: 3, title: 'Item 3', description: 'Description 3' } ];
- DocumentFragmentを使用する場合と使用しない場合のパフォーマンスの違いを計測するコードを書いてください。
- 動的なリスト更新において、既存の要素を再利用しながら効率的に更新を行うための実装を考えてください。
参考文献
[編集]第5章:イベント処理
[編集]5.1 イベントの基本概念
[編集]5.1.1 イベントとは
[編集]イベントは、DOMにおけるユーザーアクションやプログラムの状態変化を表す仕組みです。
主要なイベントタイプ カテゴリ イベント例 説明 マウス click, mousedown, mouseover マウス操作関連 キーボード keydown, keyup, keypress キー入力関連 フォーム submit, change, input フォーム操作関連 文書 DOMContentLoaded, load 文書読み込み関連 フォーカス focus, blur フォーカス関連 タッチ touchstart, touchend タッチ操作関連
5.1.2 イベントオブジェクト
[編集]// イベントオブジェクトの基本プロパティ element.addEventListener('click', (event) => { console.log({ type: event.type, // イベントの種類 target: event.target, // イベント発生要素 currentTarget: event.currentTarget, // イベントリスナーが設定された要素 timeStamp: event.timeStamp, // イベント発生時刻 isTrusted: event.isTrusted // ユーザーアクションかどうか }); });
5.2 イベントリスナー
[編集]5.2.1 イベントリスナーの登録方法
[編集]イベントリスナー登録の比較 手法 メリット デメリット addEventListener - 複数のリスナーを登録可能
- 詳細な制御が可能
- キャプチャリングフェーズの制御
- 記述が若干冗長
onイベント属性 - 簡潔な記述
- 直感的
- 一つのハンドラーのみ
- 柔軟性に欠ける
// addEventListener element.addEventListener('click', handleClick); element.addEventListener('click', handleClick, { once: true, // 一度だけ実行 passive: true, // パフォーマンス最適化 capture: false // イベントフェーズの制御 }); // onイベント属性 element.onclick = handleClick;
5.2.2 イベントリスナーの削除
[編集]// 名前付き関数の場合 function handleClick(event) { console.log('Clicked'); } element.addEventListener('click', handleClick); element.removeEventListener('click', handleClick); // 匿名関数の場合の対処 const listener = (event) => { console.log('Clicked'); }; element.addEventListener('click', listener); element.removeEventListener('click', listener);
5.3 イベントの伝播
[編集]5.3.1 イベントフェーズ
[編集]- キャプチャリングフェーズ:ルートから対象要素まで
- ターゲットフェーズ:対象要素で
- バブリングフェーズ:対象要素からルートまで
<div id="outer"> <div id="middle"> <button id="inner">Click me</button> </div> </div> <script> const outer = document.getElementById('outer'); const middle = document.getElementById('middle'); const inner = document.getElementById('inner'); // キャプチャリングフェーズ outer.addEventListener('click', e => { console.log('Outer - Capture'); }, true); // バブリングフェーズ outer.addEventListener('click', e => { console.log('Outer - Bubble'); }); // イベント伝播の制御 inner.addEventListener('click', e => { e.stopPropagation(); // 伝播を停止 // または e.stopImmediatePropagation(); // 後続のリスナーも含めて停止 }); </script>
5.4 イベントデリゲーション
[編集]5.4.1 イベントデリゲーションの基本
[編集]// 従来の方法(非効率) document.querySelectorAll('.item').forEach(item => { item.addEventListener('click', handleClick); }); // イベントデリゲーション(効率的) document.querySelector('.container').addEventListener('click', e => { if (e.target.matches('.item')) { handleClick(e); } });
5.4.2 イベントデリゲーションのパターン
[編集]// データ属性を使用したデリゲーション document.querySelector('.menu').addEventListener('click', e => { const action = e.target.closest('[data-action]'); if (action) { const actionType = action.dataset.action; handlers[actionType]?.(e); } }); // 複数のイベントタイプの委譲 function delegateEvents(element, events) { Object.entries(events).forEach(([eventType, handler]) => { element.addEventListener(eventType, e => { const target = e.target.closest('[data-handler]'); if (target) { handler.call(target, e); } }); }); }
5.5 カスタムイベント
[編集]5.5.1 カスタムイベントの作成
[編集]// カスタムイベントの定義 const event = new CustomEvent('userAction', { detail: { username: 'john_doe', action: 'login' }, bubbles: true, cancelable: true }); // イベントの発行 element.dispatchEvent(event); // イベントのリッスン element.addEventListener('userAction', e => { console.log(e.detail); });
5.5.2 イベントインターフェースの拡張
[編集]// カスタムイベントクラス class UserActionEvent extends CustomEvent { constructor(action, user) { super('userAction', { detail: { action, user }, bubbles: true, cancelable: true }); } get actionType() { return this.detail.action; } } // 使用例 element.dispatchEvent(new UserActionEvent('login', 'john_doe'));
演習問題
[編集]- イベントデリゲーションを使用して、動的に追加される要素にもイベントハンドリングを適用する実装を作成してください。
- バブリングとキャプチャリングの違いを理解するためのデモページを作成してください。
- カスタムイベントを使用して、複数のコンポーネント間で通信を行うパターンを実装してください。
参考文献
[編集]第6章:フォームとバリデーション
[編集]6.1 フォーム要素の操作
[編集]6.1.1 フォームへのアクセス
[編集]フォームアクセス方法 メソッド 説明 document.forms フォームコレクションへのアクセス document.getElementById() ID指定による取得 document.querySelector() セレクタによる取得 form.elements フォーム内の要素コレクション
// フォームへのアクセス例 const form = document.forms['loginForm']; const emailInput = form.elements['email']; const passwordInput = form.querySelector('[name="password"]'); // フォーム要素の値の取得と設定 console.log(emailInput.value); passwordInput.value = '';
6.1.2 フォーム要素の種類と特性
[編集]主要なフォーム要素 要素 値の取得 特記事項 input[type="text"] value 一行テキスト textarea value 複数行テキスト select value, selectedIndex 選択肢 input[type="checkbox"] checked 真偽値 input[type="radio"] checked グループ内で一つ選択 input[type="file"] files FileList オブジェクト
// チェックボックスの操作 const checkbox = form.querySelector('[type="checkbox"]'); console.log(checkbox.checked); // セレクトボックスの操作 const select = form.querySelector('select'); console.log(select.value); console.log(select.options[select.selectedIndex].text); // ファイル入力の操作 const fileInput = form.querySelector('[type="file"]'); const files = Array.from(fileInput.files);
6.2 入力値の検証
[編集]6.2.1 HTML5バリデーション属性
[編集]バリデーション属性 属性 説明 例 required 必須入力 <input required> pattern 正規表現パターン <input pattern="[0-9]{3}"> minlength/maxlength 文字数制限 <input minlength="8"> min/max 数値範囲 <input type="number" min="0"> type 入力タイプ <input type="email">
<form id="registrationForm"> <input type="email" required pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$" name="email"> <input type="password" required minlength="8" pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}" name="password"> <button type="submit">登録</button> </form>
6.2.2 Constraint Validation API
[編集]const input = document.querySelector('input[name="email"]'); // バリデーション状態の確認 console.log(input.validity); console.log(input.validationMessage); console.log(input.checkValidity()); // カスタムバリデーション input.setCustomValidity('このメールアドレスは使用できません'); // バリデーションの実装例 input.addEventListener('input', () => { if (input.validity.typeMismatch) { input.setCustomValidity('正しいメールアドレスを入力してください'); } else { input.setCustomValidity(''); } });
6.3 カスタムバリデーション
[編集]6.3.1 独自のバリデーションロジック
[編集]class FormValidator { constructor(form) { this.form = form; this.errors = new Map(); } addRule(fieldName, validatorFn, message) { const field = this.form.elements[fieldName]; field.addEventListener('input', () => { const isValid = validatorFn(field.value); if (!isValid) { this.errors.set(fieldName, message); this.showError(field, message); } else { this.errors.delete(fieldName); this.clearError(field); } }); } showError(field, message) { let errorDiv = field.nextElementSibling; if (!errorDiv || !errorDiv.classList.contains('error-message')) { errorDiv = document.createElement('div'); errorDiv.classList.add('error-message'); field.parentNode.insertBefore(errorDiv, field.nextSibling); } errorDiv.textContent = message; } clearError(field) { const errorDiv = field.nextElementSibling; if (errorDiv?.classList.contains('error-message')) { errorDiv.remove(); } } isValid() { return this.errors.size === 0; } } // 使用例 const validator = new FormValidator(form); validator.addRule('password', value => /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}$/.test(value), 'パスワードは8文字以上で、数字、小文字、大文字を含む必要があります' );
6.4 フォームデータの送信
[編集]6.4.1 FormDataの使用
[編集]form.addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(form); // FormDataの操作 formData.append('timestamp', Date.now()); formData.set('username', formData.get('username').trim()); try { const response = await fetch('/api/submit', { method: 'POST', body: formData }); const result = await response.json(); console.log('送信成功:', result); } catch (error) { console.error('送信失敗:', error); } });
6.4.2 JSONでの送信
[編集]function formToJSON(form) { const formData = new FormData(form); const data = {}; for (const [key, value] of formData.entries()) { if (data[key] !== undefined) { if (!Array.isArray(data[key])) { data[key] = [data[key]]; } data[key].push(value); } else { data[key] = value; } } return data; } // 使用例 form.addEventListener('submit', async (e) => { e.preventDefault(); const data = formToJSON(form); try { const response = await fetch('/api/submit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); const result = await response.json(); console.log('送信成功:', result); } catch (error) { console.error('送信失敗:', error); } });
演習問題
[編集]- 以下の要件を満たすフォームバリデータを実装してください:
- パスワードの強度チェック
- パスワード確認フィールドとの一致確認
- リアルタイムバリデーション
- エラーメッセージの表示
- FormDataを使用して、ファイルアップロードを含むフォームの送信処理を実装してください。
- カスタムバリデーションを実装し、非同期のバリデーション(例:ユーザー名の重複チェック)を行うフォームを作成してください。
参考文献
[編集]第3部:高度なDOM操作
[編集]第7章:Shadow DOMとWeb Components
[編集]7.1 Shadow DOMの概念
[編集]7.1.1 Shadow DOMとは
[編集]Shadow DOMは、カプセル化されたDOMツリーを作成するための仕組みです。メインのドキュメントDOMから独立した、隠蔽されたDOMツリーを提供します。
Shadow DOMの主要概念 用語 説明 Shadow Host Shadow DOMが付加される通常のDOM要素 Shadow Root Shadow DOMのルート節点 Shadow Tree Shadow Root配下のDOM構造 Shadow Boundary メインDOMとShadow DOMの境界
7.1.2 Shadow DOMの作成
[編集]// Shadow DOMの作成 const host = document.createElement('div'); const shadowRoot = host.attachShadow({ mode: 'open' }); // Shadow DOMへの要素追加 shadowRoot.innerHTML = ` <style> /* スコープされたスタイル */ p { color: red; } </style> <p>This is in shadow DOM</p> `; // モードの違い const closedRoot = element.attachShadow({ mode: 'closed' }); // 外部からアクセス不可 const openRoot = element.attachShadow({ mode: 'open' }); // 外部からアクセス可能
7.2 カスタム要素
[編集]7.2.1 カスタム要素の定義
[編集]// カスタム要素の基本クラス class UserCard extends HTMLElement { constructor() { super(); // Shadow DOMの初期化 const shadow = this.attachShadow({ mode: 'open' }); // テンプレートの作成 shadow.innerHTML = ` <style> .card { display: block; padding: 20px; border: 1px solid #ccc; } </style> <div class="card"> <h2><slot name="username"></slot></h2> <slot name="details"></slot> </div> `; } // ライフサイクルコールバック connectedCallback() { console.log('要素がDOMに追加された'); } disconnectedCallback() { console.log('要素がDOMから削除された'); } attributeChangedCallback(name, oldValue, newValue) { console.log(`属性${name}が${oldValue}から${newValue}に変更された`); } static get observedAttributes() { return ['user-id']; } } // カスタム要素の登録 customElements.define('user-card', UserCard);
7.2.2 カスタム要素の使用
[編集]<user-card user-id="123"> <span slot="username">John Doe</span> <div slot="details"> <p>Email: john@example.com</p> <p>Role: Developer</p> </div> </user-card>
7.3 テンプレートとスロット
[編集]7.3.1 テンプレート要素
[編集]<template id="user-card-template"> <style> .user-card { display: block; padding: 10px; border: 1px solid #ccc; margin: 10px; } .user-name { color: #333; font-size: 1.2em; } </style> <div class="user-card"> <h2 class="user-name"> <slot name="username">Default Name</slot> </h2> <div class="user-details"> <slot name="details">No details provided</slot> </div> </div> </template> <script> class ImprovedUserCard extends HTMLElement { constructor() { super(); const shadow = this.attachShadow({ mode: 'open' }); const template = document .getElementById('user-card-template') .content; shadow.appendChild(template.cloneNode(true)); } } customElements.define('improved-user-card', ImprovedUserCard); </script>
7.3.2 スロットの高度な使用
[編集]class AdvancedCard extends HTMLElement { constructor() { super(); const shadow = this.attachShadow({ mode: 'open' }); shadow.innerHTML = ` <style> ::slotted(h2) { color: blue; } ::slotted(.highlight) { background: yellow; } </style> <div class="container"> <slot name="header"></slot> <slot></slot> <slot name="footer"></slot> </div> `; // スロットの変更監視 shadow.querySelector('slot').addEventListener('slotchange', e => { console.log('スロットの内容が変更された', e.target); }); } } customElements.define('advanced-card', AdvancedCard);
7.4 スタイルのカプセル化
[編集]7.4.1 Shadow DOMのスタイリング
[編集]// スタイルのカプセル化 const style = ` /* ローカルスタイル */ :host { display: block; border: 1px solid #ccc; } :host(.theme-dark) { background: #333; color: white; } :host-context(.dark-theme) { background: #222; } /* スロット要素のスタイリング */ ::slotted(*) { margin: 5px; } ::slotted(p) { color: blue; } `; class StyledComponent extends HTMLElement { constructor() { super(); const shadow = this.attachShadow({ mode: 'open' }); shadow.innerHTML = ` <style>${style}</style> <div class="container"> <slot></slot> </div> `; } }
7.4.2 CSS Parts
[編集]<template id="tabbed-custom"> <style> .tab { padding: 10px; cursor: pointer; } .tab[part="active-tab"] { border-bottom: 2px solid blue; } </style> <div class="tabs"> <div class="tab" part="tab">Tab 1</div> <div class="tab" part="tab active-tab">Tab 2</div> </div> </template> <style> /* 外部からのスタイリング */ custom-tabs::part(tab) { color: #333; } custom-tabs::part(active-tab) { color: blue; } </style>
演習問題
[編集]- タブパネルをWeb Componentsで実装してください。以下の機能を含めてください:
- タブの動的な追加/削除
- カスタムイベント
- スロットを使用したコンテンツの挿入
- テーマのカスタマイズ機能
- Shadow DOMを使用して、スタイルが完全に分離されたモーダルダイアログコンポーネントを作成してください。
- カスタム要素のライフサイクルメソッドを活用した、データ同期機能付きのコンポーネントを実装してください。
参考文献
[編集]第8章:DOMとパフォーマンス
[編集]8.1 DOMパフォーマンスの最適化
[編集]8.1.1 DOM操作のコスト
[編集]DOMの操作は、JavaScriptの実行の中でも特にコストの高い処理の一つです。これは、DOM操作が以下の処理を引き起こすためです:
- レイアウトの再計算
- ページの再描画
- スタイルの再計算
8.1.2 最適化テクニック
[編集]バッチ処理
[編集]複数のDOM操作をまとめて実行することで、レンダリングエンジンの再計算を最小限に抑えることができます。
// 非効率な例 for (let i = 0; i < 1000; i++) { container.appendChild(document.createElement('div')); } // 最適化された例 const fragment = document.createDocumentFragment(); for (let i = 0; i < 1000; i++) { fragment.appendChild(document.createElement('div')); } container.appendChild(fragment);
DOM操作の最小化
[編集]- 要素の追加・削除を最小限に抑える
- クラス名の一括変更
- スタイル操作の集約
8.2 メモリリークの防止
[編集]8.2.1 一般的なメモリリークの原因
[編集]- イベントリスナーの未解除
- 削除された要素への参照の保持
- クロージャーによる意図しない参照の保持
8.2.2 メモリリーク対策
[編集]イベントリスナーの適切な管理
[編集]class Component { constructor() { this.handler = this.handleClick.bind(this); document.addEventListener('click', this.handler); } destroy() { document.removeEventListener('click', this.handler); } }
8.3 仮想DOM
[編集]8.3.1 仮想DOMの概念
[編集]仮想DOMは、実際のDOMの軽量な抽象表現です。以下の特徴があります:
- メモリ上でDOMの状態を管理
- 差分計算による効率的な更新
- バッチ処理による最適化
8.3.2 仮想DOMの実装
[編集]基本的な構造
[編集]const vnode = { type: 'div', props: { className: 'container', id: 'app' }, children: [ { type: 'p', props: {}, children: ['Hello, Virtual DOM'] } ] };
8.4 レンダリングパフォーマンス
[編集]8.4.1 レンダリングパイプライン
[編集]- JavaScript実行
- スタイル計算
- レイアウト
- ペイント
- コンポジット
8.4.2 パフォーマンス最適化テクニック
[編集]レイアウトスラッシング の防止
[編集]// 非効率な例 const width = element.offsetWidth; element.style.width = width + 100 + 'px'; const height = element.offsetHeight; element.style.height = height + 100 + 'px'; // 最適化された例 const width = element.offsetWidth; const height = element.offsetHeight; element.style.width = width + 100 + 'px'; element.style.height = height + 100 + 'px';
アニメーションの最適化
[編集]- transformとopacityの使用
- will-changeプロパティの適切な使用
- requestAnimationFrameの活用
8.4.3 パフォーマンス測定
[編集]- Performance API
- Chrome DevToolsのパフォーマンスパネル
- Lighthouse
8.5 実装例と推奨プラクティス
[編集]8.5.1 効率的なリスト更新
[編集]function updateList(items) { const fragment = document.createDocumentFragment(); items.forEach(item => { const li = document.createElement('li'); li.textContent = item; fragment.appendChild(li); }); list.innerHTML = ''; list.appendChild(fragment); }
8.5.2 スムーズなスクロール実装
[編集]function smoothScroll() { requestAnimationFrame(() => { // スクロール処理 }); }
8.6 DOMキャッシュと再利用
[編集]8.6.1 要素の参照のキャッシュ
[編集]頻繁にアクセスする要素の参照をキャッシュすることで、querySelector/getElementByIdの実行コストを削減できます。
// 非効率な例 function updateCount() { document.getElementById('counter').textContent = count++; } // 最適化された例 const counterElement = document.getElementById('counter'); function updateCount() { counterElement.textContent = count++; }
8.6.2 要素の再利用パターン
[編集]Object Pooling
[編集]class DOMPool { constructor() { this.pool = []; } acquire() { return this.pool.pop() || document.createElement('div'); } release(element) { element.textContent = ''; element.className = ''; this.pool.push(element); } }
8.7 非同期DOMアップデート
[編集]8.7.1 requestAnimationFrameの活用
[編集]画面の更新タイミングに合わせてDOM操作を行うことで、スムーズなアニメーションを実現します。
function updateUISmooth(elements) { let index = 0; function update() { if (index < elements.length) { const element = elements[index++]; element.style.transform = 'translateX(100px)'; requestAnimationFrame(update); } } requestAnimationFrame(update); }
8.7.2 requestIdleCallbackの活用
[編集]優先度の低いDOM操作をブラウザのアイドル時間に実行します。
function deferredUpdate(elements) { requestIdleCallback((deadline) => { while (deadline.timeRemaining() > 0 && elements.length > 0) { const element = elements.pop(); // 低優先度の更新処理 } }); }
8.8 レイアウトスラッシングの詳細
[編集]8.8.1 強制同期レイアウト
[編集]以下の操作は強制的にレイアウトの再計算を引き起こします:
- offsetWidth/Height
- clientWidth/Height
- scrollWidth/Height
- getComputedStyle()
8.8.2 最適化パターン
[編集]Read操作とWrite操作の分離
[編集]// 読み取り操作をまとめる const reads = elements.map(el => ({ width: el.offsetWidth, height: el.offsetHeight })); // 書き込み操作をまとめる reads.forEach((dims, i) => { elements[i].style.width = dims.width * 2 + 'px'; elements[i].style.height = dims.height * 2 + 'px'; });
8.9 高度な最適化テクニック
[編集]8.9.1 インターセクション最適化
[編集]画面に表示される要素のみを処理することで、パフォーマンスを改善します。
const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { // 要素が表示されたときの処理 entry.target.classList.add('visible'); } }); }); elements.forEach(element => observer.observe(element));
8.9.2 DOM操作の遅延実行
[編集]デバウンスとスロットル
[編集]function debounce(fn, delay) { let timeoutId; return function (...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn.apply(this, args), delay); }; } const debouncedUpdate = debounce((element) => { // DOM更新処理 }, 100);
8.10 パフォーマンスモニタリング
[編集]8.10.1 Performance Metrics
[編集]- First Contentful Paint (FCP)
- Largest Contentful Paint (LCP)
- First Input Delay (FID)
- Cumulative Layout Shift (CLS)
8.10.2 モニタリングの実装
[編集]// Performance Observer APIの活用 new PerformanceObserver((list) => { const entries = list.getEntries(); entries.forEach(entry => { console.log(`${entry.name}: ${entry.startTime}ms`); }); }).observe({entryTypes: ['paint', 'layout-shift']});
8.11 メモリプロファイリングとデバッグ
[編集]8.11.1 Chrome DevToolsを使用したメモリ分析
[編集]ヒープスナップショット
[編集]メモリリークを特定するための主要な手法:
class LeakExample { constructor() { this.elements = []; this.init(); } init() { // メモリリークを引き起こす可能性のあるコード setInterval(() => { const element = document.createElement('div'); this.elements.push(element); document.body.appendChild(element); }, 1000); } // cleanup処理が必要 destroy() { this.elements.forEach(element => { document.body.removeChild(element); }); this.elements = []; } }
8.11.2 メモリ使用量の監視
[編集]Performance Monitorの活用
[編集]// メモリ使用量の定期的な監視 if (performance.memory) { setInterval(() => { console.log({ totalJSHeapSize: performance.memory.totalJSHeapSize, usedJSHeapSize: performance.memory.usedJSHeapSize, jsHeapSizeLimit: performance.memory.jsHeapSizeLimit }); }, 1000); }
8.12 特殊なDOM操作パターン
[編集]8.12.1 動的リストの最適化
[編集]大量のリスト項目を扱う際の仮想スクロールの実装:
class VirtualScroller { constructor(container, itemHeight, totalItems) { this.container = container; this.itemHeight = itemHeight; this.totalItems = totalItems; this.visibleItems = Math.ceil(container.clientHeight / itemHeight) + 2; this.scrollTop = 0; this.init(); } init() { this.container.style.height = `${this.totalItems * this.itemHeight}px`; this.container.style.position = 'relative'; this.container.addEventListener('scroll', this.onScroll.bind(this)); this.renderVisible(); } onScroll() { requestAnimationFrame(() => this.renderVisible()); } renderVisible() { const startIndex = Math.floor(this.scrollTop / this.itemHeight); // 表示項目の更新処理 } }
8.12.2 レスポンシブな DOM 更新
[編集]画面サイズの変更に効率的に対応する実装:
class ResponsiveHandler { constructor() { this.observer = new ResizeObserver(this.onResize.bind(this)); this.debounceTimeout = null; } observe(element) { this.observer.observe(element); } onResize(entries) { if (this.debounceTimeout) { clearTimeout(this.debounceTimeout); } this.debounceTimeout = setTimeout(() => { entries.forEach(entry => { // サイズ変更に応じた DOM 更新 this.updateLayout(entry); }); }, 150); } updateLayout(entry) { const element = entry.target; const width = entry.contentRect.width; // レスポンシブな更新処理 if (width < 600) { element.classList.add('mobile'); } else { element.classList.remove('mobile'); } } }
8.13 高度なパフォーマンス最適化パターン
[編集]8.13.1 Worker を活用した DOM 操作
[編集]重い計算処理を Web Worker に委譲する実装:
// メインスレッド const worker = new Worker('worker.js'); worker.postMessage({ type: 'PROCESS_DATA', data: largeDataSet }); worker.onmessage = function(e) { const result = e.data; requestAnimationFrame(() => { // DOM更新を実行 updateUI(result); }); }; // worker.js self.onmessage = function(e) { if (e.data.type === 'PROCESS_DATA') { const result = processLargeDataSet(e.data.data); self.postMessage(result); } };
8.13.2 レンダリングの最適化
[編集]Paint と Composite のパフォーマンス向上:
class OptimizedRenderer { constructor(element) { this.element = element; this.setupOptimizations(); } setupOptimizations() { // レイヤー生成の最適化 this.element.style.willChange = 'transform'; // GPU アクセラレーションの活用 this.element.style.transform = 'translateZ(0)'; } animate() { requestAnimationFrame(() => { // アニメーションの実行 this.element.style.transform = `translateX(${this.position}px)`; if (this.isAnimating) { this.animate(); } }); } }
8.14 クリティカルレンダリングパスの最適化
[編集]8.14.1 レンダリングブロッキングリソースの最小化
[編集]レンダリングを遅延させる要因を制御する手法:
// スタイルシートの動的読み込み function loadDeferredStyles() { const stylesheet = document.createElement('link'); stylesheet.rel = 'stylesheet'; stylesheet.href = 'non-critical.css'; stylesheet.media = 'print'; document.head.appendChild(stylesheet); requestIdleCallback(() => { stylesheet.media = 'all'; }); } // スクリプトの遅延読み込み function loadDeferredScript(src) { const script = document.createElement('script'); script.src = src; script.defer = true; document.body.appendChild(script); }
8.14.2 DOMContentLoadedの最適化
[編集]document.addEventListener('DOMContentLoaded', () => { // 初期表示に必要な最小限の処理 initializeCriticalFeatures(); // 非クリティカルな処理を遅延実行 requestIdleCallback(() => { initializeNonCriticalFeatures(); }); }); function initializeCriticalFeatures() { // ユーザーが即座に必要とする機能の初期化 } function initializeNonCriticalFeatures() { // 後から必要になる機能の初期化 }
8.15 高度なデバッグテクニック
[編集]8.15.1 パフォーマンスマーカー
[編集]class PerformanceDebugger { static startMeasure(label) { performance.mark(`${label}-start`); } static endMeasure(label) { performance.mark(`${label}-end`); performance.measure(label, `${label}-start`, `${label}-end`); const measures = performance.getEntriesByName(label); console.log(`${label} took ${measures[0].duration}ms`); } static clearMeasures() { performance.clearMarks(); performance.clearMeasures(); } } // 使用例 PerformanceDebugger.startMeasure('DOM-update'); // DOM操作の実行 PerformanceDebugger.endMeasure('DOM-update');
8.15.2 レイアウトスラッシングの検出
[編集]class LayoutThrashingDetector { constructor() { this.lastRead = 0; this.lastWrite = 0; this.warnings = []; } trackRead(property) { const timestamp = performance.now(); if (timestamp < this.lastWrite) { this.warnings.push(`Possible layout thrashing: Reading ${property} after write`); } this.lastRead = timestamp; } trackWrite(property) { const timestamp = performance.now(); if (timestamp < this.lastRead) { this.warnings.push(`Possible layout thrashing: Writing ${property} after read`); } this.lastWrite = timestamp; } getWarnings() { return this.warnings; } }
8.16 最適化のトレードオフ
[編集]8.16.1 メモリと速度のバランス
[編集]キャッシュと再計算のトレードオフを考慮した実装:
class CacheStrategy { constructor(maxCacheSize = 1000) { this.cache = new Map(); this.maxCacheSize = maxCacheSize; } get(key, computeValue) { if (this.cache.has(key)) { return this.cache.get(key); } const value = computeValue(); if (this.cache.size >= this.maxCacheSize) { // LRU方式でキャッシュをクリア const firstKey = this.cache.keys().next().value; this.cache.delete(firstKey); } this.cache.set(key, value); return value; } clear() { this.cache.clear(); } }
8.16.2 更新頻度と処理コストの最適化
[編集]class UpdateOptimizer { constructor(threshold = 16) { this.threshold = threshold; // 60fps = ≈16.67ms this.lastUpdate = 0; this.pendingUpdate = null; } scheduleUpdate(updateFn) { const now = performance.now(); const timeSinceLastUpdate = now - this.lastUpdate; if (timeSinceLastUpdate >= this.threshold) { // 十分な時間が経過している場合は即時更新 this.executeUpdate(updateFn); } else { // そうでない場合は次のフレームまで遅延 if (this.pendingUpdate) { cancelAnimationFrame(this.pendingUpdate); } this.pendingUpdate = requestAnimationFrame(() => { this.executeUpdate(updateFn); }); } } executeUpdate(updateFn) { updateFn(); this.lastUpdate = performance.now(); this.pendingUpdate = null; } }
8.17 インタラクティブなUIの最適化
[編集]8.17.1 タッチイベントの最適化
[編集]モバイルデバイスでのスムーズなインタラクションを実現:
class TouchOptimizer { constructor(element) { this.element = element; this.lastTouchEnd = 0; this.setupListeners(); } setupListeners() { // パッシブイベントリスナーの使用 this.element.addEventListener('touchstart', this.handleTouchStart.bind(this), { passive: true }); this.element.addEventListener('touchmove', this.handleTouchMove.bind(this), { passive: false }); this.element.addEventListener('touchend', this.handleTouchEnd.bind(this)); } handleTouchStart(e) { // タッチ開始時の処理 this.startX = e.touches[0].clientX; this.startY = e.touches[0].clientY; } handleTouchMove(e) { // スクロール中の不要なイベントをキャンセル if (this.isScrolling) { e.preventDefault(); } } handleTouchEnd(e) { // ダブルタップの防止 const now = performance.now(); if (now - this.lastTouchEnd <= 300) { e.preventDefault(); } this.lastTouchEnd = now; } }
8.17.2 スクロールパフォーマンスの最適化
[編集]class ScrollOptimizer { constructor() { this.scrollTimeout = null; this.rafId = null; this.setupScrollListener(); } setupScrollListener() { window.addEventListener('scroll', this.handleScroll.bind(this), { passive: true }); } handleScroll() { if (this.scrollTimeout) { clearTimeout(this.scrollTimeout); } if (this.rafId) { cancelAnimationFrame(this.rafId); } this.rafId = requestAnimationFrame(() => { this.updateDuringScroll(); }); this.scrollTimeout = setTimeout(() => { this.updateAfterScrollEnd(); }, 150); } updateDuringScroll() { // スクロール中の軽量な更新処理 } updateAfterScrollEnd() { // スクロール終了後の重い処理 } }
8.18 エラー処理とリカバリー
[編集]8.18.1 DOM操作の例外処理
[編集]class DOMErrorHandler { static safelyUpdateDOM(element, updateFn) { if (!element || !(element instanceof Element)) { console.warn('Invalid element provided'); return false; } try { updateFn(element); return true; } catch (error) { console.error('DOM update failed:', error); this.attemptRecovery(element); return false; } } static attemptRecovery(element) { // DOM状態の復旧を試みる try { // 一時的な変更を元に戻す element.style.display = ''; element.classList.remove('updating'); // エラーログの送信 this.logError(); } catch (error) { console.error('Recovery failed:', error); } } static logError() { // エラーログの送信処理 } }
8.18.2 グレースフルデグラデーション
[編集]class FeatureDetector { static checkFeatureSupport() { const features = { intersectionObserver: 'IntersectionObserver' in window, resizeObserver: 'ResizeObserver' in window, mutationObserver: 'MutationObserver' in window, customElements: 'customElements' in window }; return features; } static setupPolyfills(features) { if (!features.intersectionObserver) { // フォールバックの実装 this.implementIntersectionObserverFallback(); } } static implementIntersectionObserverFallback() { // 簡易的なビューポート検出の実装 const elements = new Set(); window.addEventListener('scroll', () => { requestAnimationFrame(() => { elements.forEach(el => { const rect = el.getBoundingClientRect(); const isVisible = rect.top < window.innerHeight && rect.bottom >= 0; if (isVisible) { el.dispatchEvent(new CustomEvent('intersect')); } }); }); }); return { observe: (element) => elements.add(element), unobserve: (element) => elements.delete(element) }; } }
第9章:モダンDOM API
[編集]9.1 Intersection Observer API
[編集]9.1.1 基本概念
[編集]Intersection Observer APIは、要素の可視性を効率的に監視するためのAPIです。以下の用途に特に有効です:
- 遅延読み込み(Lazy Loading)
- 無限スクロール
- 広告の表示追跡
- アニメーションのトリガー
9.1.2 実装例
[編集]const options = { root: null, rootMargin: '0px', threshold: 0.5 }; const observer = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { // 要素が表示領域に入った時の処理 const element = entry.target; loadImage(element); observer.unobserve(element); } }); }, options); // 画像の遅延読み込み実装 function loadImage(element) { if (element.dataset.src) { element.src = element.dataset.src; delete element.dataset.src; } } // 監視の開始 document.querySelectorAll('img[data-src]').forEach(img => { observer.observe(img); });
9.2 Mutation Observer API
[編集]9.2.1 DOM変更の監視
[編集]Mutation Observer APIは、DOM要素の変更を監視するための機能を提供します:
- 属性の変更
- 子要素の追加・削除
- テキストコンテンツの変更
9.2.2 実装例
[編集]const config = { attributes: true, childList: true, subtree: true, characterData: true }; const observer = new MutationObserver((mutations) => { mutations.forEach(mutation => {o switch (mutation.type) { case 'childList': // 子要素の変更を処理 handleChildListMutation(mutation); break; case 'attributes': // 属性の変更を処理 handleAttributeMutation(mutation); break; case 'characterData': // テキストの変更を処理 handleCharacterDataMutation(mutation); break; } }); }); function handleChildListMutation(mutation) { mutation.addedNodes.forEach(node => { console.log('Added:', node); }); mutation.removedNodes.forEach(node => { console.log('Removed:', node); }); } // 監視の開始 observer.observe(targetNode, config);
9.3 ResizeObserver API
[編集]9.3.1 要素サイズの監視
[編集]ResizeObserver APIは、要素のサイズ変更を効率的に検知するためのAPIです:
- レスポンシブなレイアウト
- 動的なコンテンツサイズ調整
- カスタムレイアウト計算
9.3.2 実装例
[編集]const observer = new ResizeObserver(entries => { entries.forEach(entry => { const { width, height } = entry.contentRect; // サイズに応じたレイアウト調整 if (width < 600) { entry.target.classList.add('mobile'); } else { entry.target.classList.remove('mobile'); } // コンテンツの再配置 repositionContent(entry.target, width, height); }); }); function repositionContent(element, width, height) { // レイアウトの再計算と適用 const children = element.children; // レイアウトロジックの実装 } // 監視の開始 observer.observe(document.querySelector('.responsive-container'));
9.4 ポインターイベント
[編集]9.4.1 マルチデバイス対応
[編集]ポインターイベントは、マウス、タッチ、ペンなどの入力を統一的に扱うためのAPIです:
- クロスデバイス互換性
- 入力デバイスに依存しない実装
- ジェスチャー認識
9.4.2 実装例
[編集]class PointerHandler { constructor(element) { this.element = element; this.pointerDown = false; this.startX = 0; this.startY = 0; this.setupListeners(); } setupListeners() { this.element.addEventListener('pointerdown', this.handlePointerDown.bind(this)); this.element.addEventListener('pointermove', this.handlePointerMove.bind(this)); this.element.addEventListener('pointerup', this.handlePointerUp.bind(this)); this.element.addEventListener('pointercancel', this.handlePointerCancel.bind(this)); } handlePointerDown(event) { this.pointerDown = true; this.startX = event.clientX; this.startY = event.clientY; // ポインターの捕捉 this.element.setPointerCapture(event.pointerId); } handlePointerMove(event) { if (!this.pointerDown) return; const deltaX = event.clientX - this.startX; const deltaY = event.clientY - this.startY; // 移動距離に応じた処理 this.processMovement(deltaX, deltaY); } handlePointerUp(event) { this.pointerDown = false; this.element.releasePointerCapture(event.pointerId); } handlePointerCancel(event) { this.pointerDown = false; // キャンセル時の後処理 } processMovement(deltaX, deltaY) { // 移動に応じた処理の実装 } }
9.5 タッチイベント
[編集]9.5.1 タッチ操作の最適化
[編集]モバイルデバイスでのタッチ操作を最適化するためのAPIです:
- シングルタッチ
- マルチタッチ
- ジェスチャー認識
- スクロール最適化
9.5.2 実装例
[編集]class TouchHandler { constructor(element) { this.element = element; this.touchStartX = 0; this.touchStartY = 0; this.lastTap = 0; this.setupListeners(); } setupListeners() { this.element.addEventListener('touchstart', this.handleTouchStart.bind(this), { passive: true }); this.element.addEventListener('touchmove', this.handleTouchMove.bind(this), { passive: false }); this.element.addEventListener('touchend', this.handleTouchEnd.bind(this)); } handleTouchStart(event) { const touch = event.touches[0]; this.touchStartX = touch.clientX; this.touchStartY = touch.clientY; // ダブルタップの検出 const now = Date.now(); const doubleTapDelay = now - this.lastTap; if (doubleTapDelay < 300) { this.handleDoubleTap(event); } this.lastTap = now; } handleTouchMove(event) { const touch = event.touches[0]; const deltaX = touch.clientX - this.touchStartX; const deltaY = touch.clientY - this.touchStartY; // スワイプの検出と処理 this.processSwipe(deltaX, deltaY, event); } handleTouchEnd(event) { // タッチ終了時の処理 } handleDoubleTap(event) { // ダブルタップ時の処理 } processSwipe(deltaX, deltaY, event) { // スワイプ距離に応じた処理 if (Math.abs(deltaX) > Math.abs(deltaY)) { // 水平スワイプの処理 event.preventDefault(); this.handleHorizontalSwipe(deltaX); } } }
9.6 イベント処理の統合
[編集]9.6.1 マルチモーダル入力の処理
[編集]異なる入力方式を統合的に扱う実装:
class InputManager { constructor(element) { this.element = element; this.activePointers = new Map(); this.setupEventHandlers(); } setupEventHandlers() { // ポインターイベント this.element.addEventListener('pointerdown', this.handlePointerDown.bind(this)); this.element.addEventListener('pointermove', this.handlePointerMove.bind(this)); this.element.addEventListener('pointerup', this.handlePointerUp.bind(this)); // タッチイベント this.element.addEventListener('touchstart', this.handleTouchStart.bind(this), { passive: true }); this.element.addEventListener('touchmove', this.handleTouchMove.bind(this), { passive: false }); // 従来のマウスイベント(フォールバック) this.element.addEventListener('mousedown', this.handleMouseDown.bind(this)); } handlePointerDown(event) { this.activePointers.set(event.pointerId, { x: event.clientX, y: event.clientY, type: event.pointerType }); this.element.setPointerCapture(event.pointerId); } handlePointerMove(event) { if (this.activePointers.has(event.pointerId)) { const start = this.activePointers.get(event.pointerId); const deltaX = event.clientX - start.x; const deltaY = event.clientY - start.y; this.processInput({ type: 'move', inputType: event.pointerType, delta: { x: deltaX, y: deltaY } }); } } }
9.6.2 ジェスチャー認識
[編集]class GestureRecognizer { constructor(element) { this.element = element; this.gestures = new Map(); this.currentGesture = null; } registerGesture(name, conditions) { this.gestures.set(name, conditions); } detectGesture(points) { for (const [name, conditions] of this.gestures) { if (this.matchesGesture(points, conditions)) { return name; } } return null; } matchesGesture(points, conditions) { // ジェスチャーパターンのマッチング処理 return conditions.every(condition => { return this.evaluateCondition(points, condition); }); } evaluateCondition(points, condition) { // 個別の条件評価 switch (condition.type) { case 'distance': return this.evaluateDistance(points, condition); case 'direction': return this.evaluateDirection(points, condition); case 'speed': return this.evaluateSpeed(points, condition); default: return false; } } }
9.7 オブザーバーパターンの組み合わせ
[編集]9.7.1 複合オブザーバー
[編集]class ElementObserver { constructor(element) { this.element = element; this.observers = { intersection: null, resize: null, mutation: null }; this.callbacks = new Map(); } observe(types) { types.forEach(type => { switch (type) { case 'intersection': this.setupIntersectionObserver(); break; case 'resize': this.setupResizeObserver(); break; case 'mutation': this.setupMutationObserver(); break; } }); } setupIntersectionObserver() { this.observers.intersection = new IntersectionObserver( entries => this.handleIntersection(entries), { threshold: [0, 0.5, 1.0] } ); this.observers.intersection.observe(this.element); } setupResizeObserver() { this.observers.resize = new ResizeObserver( entries => this.handleResize(entries) ); this.observers.resize.observe(this.element); } setupMutationObserver() { this.observers.mutation = new MutationObserver( mutations => this.handleMutation(mutations) ); this.observers.mutation.observe(this.element, { attributes: true, childList: true, subtree: true }); } on(event, callback) { if (!this.callbacks.has(event)) { this.callbacks.set(event, new Set()); } this.callbacks.get(event).add(callback); } off(event, callback) { if (this.callbacks.has(event)) { this.callbacks.get(event).delete(callback); } } emit(event, data) { if (this.callbacks.has(event)) { this.callbacks.get(event).forEach(callback => callback(data)); } } disconnect() { Object.values(this.observers).forEach(observer => { if (observer) { observer.disconnect(); } }); } }
9.7.2 使用例
[編集]const element = document.querySelector('.tracked-element'); const observer = new ElementObserver(element); observer.on('intersection', entry => { if (entry.isIntersecting) { console.log('Element is visible'); } }); observer.on('resize', entry => { const { width, height } = entry.contentRect; console.log(`Element resized to ${width}x${height}`); }); observer.observe(['intersection', 'resize']); // cleanup window.addEventListener('unload', () => { observer.disconnect(); });
9.8 パフォーマンスモニタリングの統合
[編集]9.8.1 オブザーバーベースのパフォーマンス計測
[編集]class PerformanceMonitor { constructor() { this.metrics = new Map(); this.observers = new Set(); this.setupObservers(); } setupObservers() { // Intersection Observer for visibility metrics this.intersectionObserver = new IntersectionObserver( (entries) => this.handleVisibilityChanges(entries), { threshold: [0, 0.5, 1.0] } ); // Performance Observer for core web vitals this.performanceObserver = new PerformanceObserver((list) => { list.getEntries().forEach(entry => { this.metrics.set(entry.name, entry.startTime); this.notifyObservers(entry); }); }); try { this.performanceObserver.observe({ entryTypes: ['largest-contentful-paint', 'first-input', 'layout-shift'] }); } catch (e) { console.warn('Performance Observer API not fully supported'); } } handleVisibilityChanges(entries) { entries.forEach(entry => { const visibilityRatio = entry.intersectionRatio; const timestamp = performance.now(); this.metrics.set(`visibility_${entry.target.id}`, { ratio: visibilityRatio, timestamp: timestamp }); }); } trackElement(element) { this.intersectionObserver.observe(element); } addObserver(callback) { this.observers.add(callback); } notifyObservers(data) { this.observers.forEach(observer => observer(data)); } getMetrics() { return Object.fromEntries(this.metrics); } }
9.8.2 リアルタイムモニタリング実装
[編集]class RealTimeMonitor { constructor() { this.monitor = new PerformanceMonitor(); this.setupDashboard(); } setupDashboard() { this.dashboard = document.createElement('div'); this.dashboard.className = 'performance-dashboard'; document.body.appendChild(this.dashboard); this.monitor.addObserver((data) => { this.updateDashboard(data); }); } updateDashboard(data) { // メトリクスの更新と表示 const metrics = this.monitor.getMetrics(); this.renderMetrics(metrics); } renderMetrics(metrics) { this.dashboard.innerHTML = ` <div class="metrics-container"> <div class="metric"> <h3>LCP</h3> <span>${metrics.lcp || 'N/A'} ms</span> </div> <div class="metric"> <h3>FID</h3> <span>${metrics.fid || 'N/A'} ms</span> </div> <div class="metric"> <h3>CLS</h3> <span>${metrics.cls || 'N/A'}</span> </div> </div> `; } }
9.9 アクセシビリティの向上
[編集]9.9.1 アクセシビリティオブザーバー
[編集]class AccessibilityObserver { constructor() { this.mutationObserver = new MutationObserver( (mutations) => this.checkAccessibility(mutations) ); this.config = { childList: true, subtree: true, attributes: true, characterData: true }; } observe(root) { this.mutationObserver.observe(root, this.config); } checkAccessibility(mutations) { mutations.forEach(mutation => { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { this.validateElement(node); } }); } else if (mutation.type === 'attributes') { this.validateElement(mutation.target); } }); } validateElement(element) { // アクセシビリティチェック項目 this.checkAltText(element); this.checkARIALabels(element); this.checkColorContrast(element); this.checkFocusableElements(element); } checkAltText(element) { if (element.tagName.toLowerCase() === 'img' && !element.alt) { console.warn('Image missing alt text:', element); } } checkARIALabels(element) { if (element.getAttribute('role') && !element.getAttribute('aria-label')) { console.warn('ARIA role without label:', element); } } checkColorContrast(element) { // 色のコントラスト比の計算と検証 const style = window.getComputedStyle(element); const backgroundColor = style.backgroundColor; const color = style.color; // コントラスト比の計算とチェック // ... } checkFocusableElements(element) { if (element.tabIndex >= 0) { if (!element.getAttribute('role') && !element.hasAttribute('aria-label')) { console.warn('Focusable element without ARIA attributes:', element); } } } }
9.9.2 キーボードナビゲーション対応
[編集]class KeyboardNavigationManager { constructor() { this.focusableElements = new Set(); this.currentFocusIndex = -1; this.setupEventListeners(); } setupEventListeners() { document.addEventListener('keydown', this.handleKeyDown.bind(this)); // フォーカス可能な要素の監視 const observer = new MutationObserver(() => { this.updateFocusableElements(); }); observer.observe(document.body, { childList: true, subtree: true }); } updateFocusableElements() { const selector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'; const elements = Array.from(document.querySelectorAll(selector)); this.focusableElements = new Set(elements.filter(el => { const style = window.getComputedStyle(el); return style.display !== 'none' && style.visibility !== 'hidden'; })); } handleKeyDown(event) { switch (event.key) { case 'Tab': this.handleTabNavigation(event); break; case 'Enter': this.handleEnterKey(event); break; case 'Escape': this.handleEscapeKey(event); break; } } handleTabNavigation(event) { const elements = Array.from(this.focusableElements); if (event.shiftKey) { // 逆方向のナビゲーション this.currentFocusIndex = Math.max(0, this.currentFocusIndex - 1); } else { // 正方向のナビゲーション this.currentFocusIndex = Math.min(elements.length - 1, this.currentFocusIndex + 1); } elements[this.currentFocusIndex].focus(); event.preventDefault(); } }
9.10 インタラクティブ要素の最適化
[編集]9.10.1 タッチとポインターの統合処理
[編集]class InteractionManager { constructor(element) { this.element = element; this.interactions = new Set(); this.lastInteraction = null; this.setupEventHandlers(); } setupEventHandlers() { // タッチイベント this.element.addEventListener('touchstart', this.handleStart.bind(this), { passive: true }); this.element.addEventListener('touchmove', this.handleMove.bind(this), { passive: false }); this.element.addEventListener('touchend', this.handleEnd.bind(this)); // ポインターイベント this.element.addEventListener('pointerdown', this.handleStart.bind(this)); this.element.addEventListener('pointermove', this.handleMove.bind(this)); this.element.addEventListener('pointerup', this.handleEnd.bind(this)); this.element.addEventListener('pointercancel', this.handleCancel.bind(this)); } handleStart(event) { const interaction = { id: event.pointerId || event.touches[0].identifier, startX: event.clientX || event.touches[0].clientX, startY: event.clientY || event.touches[0].clientY, timestamp: performance.now(), type: event.type.startsWith('touch') ? 'touch' : 'pointer' }; this.interactions.add(interaction); this.lastInteraction = interaction; // イベントの種類に応じた追加処理 if (event.type.startsWith('pointer')) { this.element.setPointerCapture(event.pointerId); } } handleMove(event) { if (this.lastInteraction) { const currentX = event.clientX || event.touches[0].clientX; const currentY = event.clientY || event.touches[0].clientY; const deltaX = currentX - this.lastInteraction.startX; const deltaY = currentY - this.lastInteraction.startY; // 移動距離に基づくジェスチャー判定 if (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10) { this.handleGesture(deltaX, deltaY); } } } handleGesture(deltaX, deltaY) { const angle = Math.atan2(deltaY, deltaX) * 180 / Math.PI; const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); // ジェスチャーの種類を判定 if (distance > 50) { if (Math.abs(angle) < 45) { this.emit('swipeRight'); } else if (Math.abs(angle) > 135) { this.emit('swipeLeft'); } else if (angle > 45 && angle < 135) { this.emit('swipeDown'); } else { this.emit('swipeUp'); } } } }
9.10.2 スクロールの最適化
[編集]class ScrollOptimizer { constructor(container) { this.container = container; this.isScrolling = false; this.scrollTimeout = null; this.lastScrollPosition = 0; this.scrollDirection = null; this.setupScrollHandler(); } setupScrollHandler() { // Intersection Observerの設定 this.observer = new IntersectionObserver( (entries) => this.handleIntersection(entries), { root: this.container, threshold: [0, 0.5, 1] } ); // スクロールイベントの設定 this.container.addEventListener('scroll', this.handleScroll.bind(this), { passive: true }); // 要素の監視開始 this.observeChildren(); } handleScroll(event) { if (!this.isScrolling) { this.isScrolling = true; this.container.classList.add('is-scrolling'); requestAnimationFrame(() => this.updateScrollState()); } // スクロール方向の検出 const currentScrollPosition = this.container.scrollTop; this.scrollDirection = currentScrollPosition > this.lastScrollPosition ? 'down' : 'up'; this.lastScrollPosition = currentScrollPosition; // スクロール終了の検出 clearTimeout(this.scrollTimeout); this.scrollTimeout = setTimeout(() => { this.isScrolling = false; this.container.classList.remove('is-scrolling'); this.onScrollEnd(); }, 150); } updateScrollState() { if (this.isScrolling) { const scrollPercentage = (this.container.scrollTop / (this.container.scrollHeight - this.container.clientHeight)) * 100; this.emit('scroll', { percentage: scrollPercentage, direction: this.scrollDirection, position: this.container.scrollTop }); requestAnimationFrame(() => this.updateScrollState()); } } onScrollEnd() { this.emit('scrollEnd', { finalPosition: this.container.scrollTop, direction: this.scrollDirection }); } observeChildren() { // コンテナ内の要素を監視 Array.from(this.container.children).forEach(child => { this.observer.observe(child); }); } handleIntersection(entries) { entries.forEach(entry => { const ratio = entry.intersectionRatio; const element = entry.target; if (ratio > 0.5) { // 要素が50%以上表示された場合 element.classList.add('in-view'); this.emit('elementVisible', element); } else { element.classList.remove('in-view'); } }); } }
第4部:実践的なケーススタディ
[編集]第10章:実装パターン
[編集]10.1 動的なUIコンポーネント
[編集]動的なUIコンポーネントは、現代のWebアプリケーションにおける基本的な構成要素です。これらのコンポーネントは、ユーザーインタラクションに応じて状態を変更し、適切なフィードバックを提供します。
10.1.1 タブパネルの実装
[編集]タブパネルは、限られたスペースで複数のコンテンツを効率的に表示するUIパターンです。
- 基本的な実装例:
<div class="tabs" role="tablist"> <button role="tab" aria-selected="true" aria-controls="panel-1">Tab 1</button> <button role="tab" aria-selected="false" aria-controls="panel-2">Tab 2</button> </div> <div id="panel-1" role="tabpanel" tabindex="0">Content 1</div> <div id="panel-2" role="tabpanel" tabindex="0" hidden>Content 2</div>
- 主要な実装ポイント:
-
- WAI-ARIAロールと属性の適切な使用
- キーボードインタラクションのサポート
- フォーカス管理
- パネル切り替えのアニメーション
10.1.2 アコーディオン実装
[編集]アコーディオンは、階層的な情報を効率的に表示するためのUIパターンです。
- 実装上の考慮点:
-
- アニメーションのパフォーマンス最適化
- 適切なARIA属性の使用
- ネストされたアコーディオンの処理
- 状態管理の効率化
10.2 無限スクロール
[編集]10.2.1 基本実装
[編集]無限スクロールは、以下の要素で構成されます:
- Intersection Observer APIによる監視
- データ取得とDOMへの追加
- ローディング状態の管理
- エラーハンドリング
- コア実装例:
class InfiniteScroll { constructor(container) { this.container = container; this.observer = new IntersectionObserver( this.handleIntersect.bind(this), { threshold: 0.1 } ); this.setupSentinel(); } setupSentinel() { this.sentinel = document.createElement('div'); this.sentinel.className = 'scroll-sentinel'; this.container.appendChild(this.sentinel); this.observer.observe(this.sentinel); } async handleIntersect(entries) { const entry = entries[0]; if (entry.isIntersecting) { await this.loadMoreContent(); } } }
10.2.2 パフォーマンス最適化
[編集]パフォーマンスを確保するための主要な戦略:
- DOM操作の最小化
- メモリ管理
- 仮想スクロールの実装
- 不要なノードの削除
10.3 モーダルダイアログ
[編集]10.3.1 アクセシブルな実装
[編集]モーダルダイアログの実装要件 要件 実装方法 重要度 フォーカストラップ focusableElements配列の管理 高 キーボード操作 Escapeキーでの閉じる機能 高 ARIA属性 role="dialog"とaria-modal="true" 高 バックドロップ マウスとキーボードイベントの制御 中
10.3.2 スタック管理
[編集]- モーダルスタックの実装例:
class ModalStack { static stack = []; static push(modal) { this.stack.push(modal); this.updateZIndex(); } static pop() { const modal = this.stack.pop(); this.updateZIndex(); return modal; } static updateZIndex() { this.stack.forEach((modal, index) => { modal.style.zIndex = 1000 + index; }); } }
演習問題
[編集]- 提供されたタブパネルの実装に、キーボードナビゲーション機能を追加してください。
- 無限スクロールの実装に、エラーリトライ機能を実装してください。
- モーダルダイアログのスタック管理に、履歴管理機能を追加してください。
参考文献
[編集]第11章:アクセシビリティ
[編集]11.1 WAI-ARIAの実装
[編集]WAI-ARIAは、アクセシブルなWebアプリケーションを構築するための重要な仕様です。
11.1.1 基本的なARIA属性
[編集]主要なARIA属性と使用例 属性 目的 使用例 aria-label 要素の明示的なラベル付け <button aria-label="閉じる">×</button>
aria-describedby 追加の説明の関連付け <input aria-describedby="password-hint">
aria-expanded 展開状態の通知 <button aria-expanded="false">メニュー</button>
aria-live 動的更新の通知 更新内容
11.1.2 ランドマークロール
[編集]- 主要なランドマークロール:
-
role="banner"
- ヘッダー領域role="navigation"
- ナビゲーションrole="main"
- メインコンテンツrole="complementary"
- 補足情報role="contentinfo"
- フッター情報
11.2 キーボード操作
[編集]11.2.1 フォーカス管理
[編集]- フォーカス管理の基本原則:
-
- 論理的なフォーカス順序の維持
- フォーカスインジケータの視認性確保
- モーダル内でのフォーカストラップ
- スキップリンクの提供
- 実装例:
class FocusManager { constructor(container) { this.container = container; this.focusableElements = this.getFocusableElements(); } getFocusableElements() { return this.container.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); } trapFocus(event) { const firstFocusable = this.focusableElements[0]; const lastFocusable = this.focusableElements[this.focusableElements.length - 1]; if (event.shiftKey && document.activeElement === firstFocusable) { lastFocusable.focus(); event.preventDefault(); } else if (!event.shiftKey && document.activeElement === lastFocusable) { firstFocusable.focus(); event.preventDefault(); } } }
11.3 スクリーンリーダー対応
[編集]11.3.1 テキスト代替
[編集]- 画像のテキスト代替:
<!-- 装飾的な画像 --> <img src="decoration.png" alt="" role="presentation"> <!-- 意味のある画像 --> <img src="chart.png" alt="2023年の売上グラフ: 前年比20%増"> <!-- 複雑な画像 --> <figure> <img src="diagram.png" alt="システム構成図"> <figcaption> 詳細な説明:クライアントサーバー構成で... </figcaption> </figure>
11.3.2 ライブリージョン
[編集]- 動的コンテンツの通知:
ライブリージョンの使い分け aria-live値 使用場面 例 polite エラーメッセージ フォームのバリデーション結果 assertive 重要な通知 セッション切れ警告 off 通知不要な更新 装飾的なアニメーション
演習問題
[編集]- 以下のナビゲーションメニューをアクセシブルに実装してください:
<nav> <ul> <li><a href="#home">Home</a></li> <li><a href="#about">About</a></li> <li><a href="#contact">Contact</a></li> </ul> </nav>
- スクリーンリーダー対応のモーダルダイアログを実装してください。
- ライブリージョンを使用した通知システムを実装してください。
参考文献
[編集]第12章:テストとデバッグ
[編集]12.1 DOMのユニットテスト
[編集]12.1.1 テストフレームワーク
[編集]- 主要なテストツール:
-
- Jest - JavaScriptのテストフレームワーク
- jsdom - ブラウザ環境のエミュレーション
- Testing Library - DOMテスト用ユーティリティ
- 基本的なテスト実装:
describe('タブコンポーネント', () => { beforeEach(() => { document.body.innerHTML = ` <div class="tabs"> <button class="tab">Tab 1</button> <button class="tab">Tab 2</button> <div class="panel">Panel 1</div> <div class="panel">Panel 2</div> </div> `; tabs = new TabComponent(document.querySelector('.tabs')); }); test('タブクリックでパネルが切り替わる', () => { const secondTab = document.querySelectorAll('.tab')[1]; secondTab.click(); const panels = document.querySelectorAll('.panel'); expect(panels[0]).toHaveAttribute('hidden'); expect(panels[1]).not.toHaveAttribute('hidden'); }); });
12.2 E2Eテスト
[編集]12.2.1 Cypressによるテスト
[編集]- テストシナリオ例:
describe('フォーム送信', () => { it('バリデーションエラーを表示する', () => { cy.visit('/contact'); cy.get('input[name="email"]').type('invalid-email'); cy.get('form').submit(); cy.get('.error-message') .should('be.visible') .and('contain', '有効なメールアドレスを入力してください'); }); });
12.3 デバッグテクニック
[編集]12.3.1 開発者ツールの活用
[編集]- 主要なデバッグ機能:
-
- Elements パネル - DOM構造の確認
- Console - ログ出力とエラー確認
- Network - ネットワークリクエストの監視
- Performance - パフォーマンス分析
- Memory - メモリリークの検出
- デバッグ用ユーティリティ:
const DOMDebugger = { logNodeInfo(node) { console.group('Node Information'); console.log('Type:', node.nodeType); console.log('Name:', node.nodeName); console.log('Value:', node.nodeValue); console.log('Children:', node.childNodes.length); console.groupEnd(); }, watchAttributes(node, callback) { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'attributes') { callback(mutation); } }); }); observer.observe(node, { attributes: true }); return observer; } };
演習問題
[編集]- 提供されたタブコンポーネントのテストカバレッジを向上させてください。
- E2Eテストで以下のシナリオをテストしてください:
- ログインフロー
- 商品カート操作
- フォーム送信
- メモリリークを検出するためのデバッグツールを実装してください。
参考文献
[編集]附録
[編集]A:DOM仕様リファレンス
[編集]A.1 インターフェース一覧
[編集]A.1.1 主要なインターフェース
[編集]DOM主要インターフェース インターフェース名 説明 主要なメソッド/プロパティ Node 全てのDOMノードの基本インターフェース parentNode, childNodes, appendChild() Element HTML要素を表すインターフェース innerHTML, classList, getAttribute() Document ドキュメント全体を表すインターフェース createElement(), querySelector() Event イベントを表すインターフェース type, target, preventDefault() HTMLElement HTML要素の基本インターフェース style, dataset, hidden
A.1.2 特殊化されたインターフェース
[編集]- HTMLElementの派生インターフェース:
-
- HTMLAnchorElement -
<a>
要素用 - HTMLAreaElement -
<area>
要素用 - HTMLAudioElement -
<audio>
要素用 - HTMLBRElement -
<br>
要素用 - HTMLBaseElement -
<base>
要素用 - HTMLBodyElement -
<body>
要素用 - HTMLButtonElement -
<button>
要素用 - HTMLCanvasElement -
<canvas>
要素用 - HTMLDListElement -
<dl>
要素用 - HTMLDataElement -
<data>
要素用 - HTMLDataListElement -
<datalist>
要素用 - HTMLDialogElement -
<dialog>
要素用 - HTMLDivElement -
<div>
要素用 - HTMLDocument - ドキュメント全体用
- HTMLEmbedElement -
<embed>
要素用 - HTMLFieldSetElement -
<fieldset>
要素用 - HTMLFormControlsCollection - フォームコントロールコレクション用
- HTMLFormElement -
<form>
要素用 - HTMLFrameSetElement -
<frameset>
要素用(非推奨) - HTMLHRElement -
<hr>
要素用 - HTMLHeadElement -
<head>
要素用 - HTMLHeadingElement -
<h1>
~<h6>
要素用 - HTMLHtmlElement -
<html>
要素用 - HTMLIFrameElement -
<iframe>
要素用 - HTMLImageElement -
<img>
要素用 - HTMLInputElement -
<input>
要素用 - HTMLLIElement -
<li>
要素用 - HTMLLabelElement -
<label>
要素用 - HTMLLegendElement -
<legend>
要素用 - HTMLLinkElement -
<link>
要素用 - HTMLMapElement -
<map>
要素用 - HTMLMediaElement - メディア要素共通インターフェース用
- HTMLMenuElement -
<menu>
要素用 - HTMLMetaElement -
<meta>
要素用 - HTMLMeterElement -
<meter>
要素用 - HTMLModElement -
<ins>
および<del>
要素用 - HTMLOListElement -
<ol>
要素用 - HTMLObjectElement -
<object>
要素用 - HTMLOptGroupElement -
<optgroup>
要素用 - HTMLOptionElement -
<option>
要素用 - HTMLOptionsCollection -
<select>
要素内のオプションコレクション用 - HTMLOutputElement -
<output>
要素用 - HTMLParagraphElement -
<p>
要素用 - HTMLPictureElement -
<picture>
要素用 - HTMLPreElement -
<pre>
要素用 - HTMLProgressElement -
<progress>
要素用 - HTMLQuoteElement -
<blockquote>
および<q>
要素用 - HTMLScriptElement -
<script>
要素用 - HTMLSelectElement -
<select>
要素用 - HTMLSourceElement -
<source>
要素用 - HTMLSpanElement -
<span>
要素用 - HTMLStyleElement -
<style>
要素用 - HTMLTableCaptionElement -
<caption>
要素用 - HTMLTableCellElement -
<td>
および<th>
要素用 - HTMLTableColElement -
<col>
および<colgroup>
要素用 - HTMLTableElement -
<table>
要素用 - HTMLTableRowElement -
<tr>
要素用 - HTMLTableSectionElement -
<thead>
、<tbody>
、<tfoot>
要素用 - HTMLTemplateElement -
<template>
要素用 - HTMLTextAreaElement -
<textarea>
要素用 - HTMLTimeElement -
<time>
要素用 - HTMLTitleElement -
<title>
要素用 - HTMLTrackElement -
<track>
要素用 - HTMLUListElement -
<ul>
要素用 - HTMLUnknownElement - 未知のHTML要素用
- HTMLVideoElement -
<video>
要素用
- HTMLAnchorElement -
A.2 プロパティとメソッドリファレンス
[編集]A.2.1 Node プロパティ
[編集]- 基本プロパティ:
-
- nodeType - ノードの種類を表す数値
- nodeName - ノードの名前
- nodeValue - ノードの値
- textContent - ノード内のテキスト
- ツリー構造関連:
-
- parentNode - 親ノード
- childNodes - 子ノードのリスト
- firstChild - 最初の子ノード
- lastChild - 最後の子ノード
A.2.2 主要なメソッド
[編集]DOMメソッド一覧 メソッド 機能 使用例 querySelector() 最初の一致要素を取得 document.querySelector('#id') querySelectorAll() 一致する全要素を取得 document.querySelectorAll('div') appendChild() 子要素として追加 parent.appendChild(child) removeChild() 子要素を削除 parent.removeChild(child) setAttribute() 属性を設定 element.setAttribute('class', 'new')
A.3 イベント一覧
[編集]A.3.1 主要なイベントタイプ
[編集]- マウスイベント:
-
- click - クリック時
- dblclick - ダブルクリック時
- mousedown - マウスボタン押下時
- mouseup - マウスボタン解放時
- mousemove - マウス移動時
- キーボードイベント:
-
- keydown - キー押下時
- keyup - キー解放時
- keypress - 文字入力時
- フォームイベント:
-
- submit - フォーム送信時
- change - 値変更時
- input - 入力時
- focus - フォーカス取得時
- blur - フォーカス喪失時
B:ブラウザ互換性
[編集]B.1 主要ブラウザの実装状況
[編集]B.1.1 機能サポート表
[編集]DOM APIサポート状況 機能 Chrome Firefox Safari Edge querySelector ○ ○ ○ ○ Shadow DOM ○ ○ ○ ○ Custom Elements ○ ○ △ ○ Intersection Observer ○ ○ ○ ○
B.2 ポリフィル
[編集]B.2.1 一般的なポリフィル実装
[編集]- カスタムイベントポリフィル:
if (typeof window.CustomEvent !== 'function') { window.CustomEvent = function(event, params) { params = params || { bubbles: false, cancelable: false, detail: null }; const evt = document.createEvent('CustomEvent'); evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); return evt; }; }
B.3 フォールバック実装
[編集]B.3.1 機能検出とフォールバック
[編集]- 実装例:
const domUtils = { querySelector: function(selector, context) { context = context || document; if (context.querySelector) { return context.querySelector(selector); } // レガシーブラウザ用フォールバック return context.getElementsByTagName(selector)[0]; }, addClass: function(element, className) { if (element.classList) { element.classList.add(className); } else { element.className += ' ' + className; } } };
C:セキュリティ考慮事項
[編集]C.1 XSSの防止
[編集]C.1.1 基本的な防御策
[編集]- エスケープ処理:
function escapeHTML(str) { return str .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }
C.1.2 安全なDOM操作
[編集]- 推奨される実装パターン:
-
- textContentの使用
- createElementの使用
- DOMPurifyなどのライブラリの活用
C.2 コンテンツセキュリティポリシー
[編集]C.2.1 CSPの設定
[編集]- 基本的なCSPディレクティブ:
Content-Security-Policy: default-src 'self'; script-src 'self' trusted-scripts.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;
C.3 サニタイズ処理
[編集]C.3.1 入力データの検証
[編集]- バリデーション規則:
入力検証ルール データ種別 検証パターン 実装例 メールアドレス 正規表現 /^[^@]+@[^@]+\.[^@]+$/ 電話番号 数字とハイフンのみ /^[\d-]+$/ ユーザー名 英数字のみ /^[a-zA-Z0-9]+$/
C.3.2 出力データのエンコード
[編集]- コンテキスト別エンコード処理:
-
- HTML文脈でのエスケープ
- JavaScript文脈でのエスケープ
- URLパラメータのエンコード
- CSSの値のエスケープ