Webアプリケーションの機能
webアプリケーションの機能を実現するためには、サーバーサイドの処とクライアントサイドの処理が必要である。サーバーサイドの処理のための言語は多くあるが、クライアントサイドの機能を実現するための言語はJavaScriptのみであると考えれても構わない。webアプリケーションの基本機能は以下の通り。
- ドラッグアンドドロップ
- 非同期読み込み
- キーボードショートカット
- アニメーション
JavaScriptの役割の一つにアプリケーションの見た目のわかりやすさや使いやすさといったユーザー体験を提供することがある。逆に言えば、Javascriptはインターフェースの実現に注力すべきで、JavaScriptだけですべての機能を実現すべきではない。理由としては
- 多くのブラウザでJSを実行しないという設定が可能であること
- ユーザー独自でJSを追加実行する機能を持つブラウザがあること
つまり意図した通りにJSが実行できないことがあるからである。
Webページを表示する時の流れ
基本的なHTML
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<img src="dummy.URL" alt="">
</body>
</html>
- HTMLをパース
- 外部JSファイル及び、CSSファイルを読み込む
- JavaScriptがパースされた時点で実行
- DOMツリーの構築完了
- 画像ファイルなどの外部ソースをロード
- すべて完了
DOMツリー構築完了後に画像ファイルなどが読み込まれることがポイント。
また、DOMツリーが構築された時点でJavascriptを実行することでユーザーにとっての待ち時間を減らすことができる。
Javascriptの記述方法と実行タイミング
- <script>タグ内に記述
- 外部JSファイルの読み込み
- Onload
- DOMCountLoaded
- 動的ロード
1. <script>タグ内に記述
実行されるのは<script>タグが解析された直後なので、タグ以降のDOM要素は解析されない。そのため</body>閉じタグの直前に記述することで、他のすべてのDOM要素を解析した後に<script>タグを読み込むようにすることができる。
<script>
const id = document.getElementById('sampleId');
id.classList.toggle('js-class')
</script>
2. 外部JSファイルの読み込み
ファイルが読み込まれるのは<script>タグが解析された直後で、実行されるのはファイルが読み込まれた直後。1と同様に、</body>閉じタグの直前に記述すること。
<script src="js/sample.js"></script>
3. Onload
onloadイベントハンドラに処理を記述することで、ページの読み込み完了後に処理を実行できる。ページ読み込み完了後なので、すべてのDOM要素を操作できる。
<body>タグ内に記述する方法と、JSファイル内に記述する方法がある。
<body onload="alert('hello')">
window.onload = function(){ alert('hello'); }
しかし、この場合すべてのページを読み込んだあとになるので大きな画像ファイルがあった場合、その画像ファイルの読み込みを待ってJSが実行されることになる。そのためJSの実行されるまでに必要以上に時間がかかってしまう。
4. DOMCountLoaded
onloadの待機時間問題を解決するのが、DOMCountLoaded。画像の読み込みを待つのと同時にJSの実行を行う。
document.addEventListener('DOMContentLoaded', function() {
alert('hello');
})
上記は以下の処理と同じ
$(() => { /*DOMCountLoadedと同じタイミングで処理*/ });
5. 動的ロード
この方法を用いた場合、JSファイルのダウンロードから実行が開始されるまではその他の処理をブロックしない。
var script = document.createElement('script');
script.src = 'http://example.com/hoge.js';
document.body.appendChild(script);
しかし、この方法でDOMに追加した場合、src属性に書かれているURLに非同期的に通信が発生し、読み込みが完了したものから実行されるため、実行の順番を確保しないといけない場合は、ただ追加すればよいわけではない。
詳しくはコチラ
動的にJSを実行する方法
JSのデバッグ方法
- console.log()
- debugger
- ネットワーク監視
console
console.log('コンソール出すぜ');
様々なコンソールの種類についてはこちら
JavascriptのChromeでのデバッグ方法個人的まとめ2016
debugger
const arr = [1, 4, 5];
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
debugger;
}
//配列一つひとつ回るごとに検証を行う。
ブラウザのdevtoolで、ブレイクポイントを設定し、ステップ実行することができる。
ステップ実行処理には4通り
- 処理再開・・・処理が再開
- ステップオーバー・・・関数呼び出しがあっても、処理せずに次に進む
- ステップイン・・・関数呼び出しがあれば、その関数へ処理遷移して進む
- ステップアウト・・・関数が終了するまで実行し、関数を抜ける。
ネットワーク監視
どういったHTTP通信がされているのか?
ちゃんとステータスコードやレスポンスが返ってきているのか?を確認する場合に、Networkタブの利用する
クロスブラウザ対応
Webブラウザを構成するものとして、HTMLレンダリングエンジンとJavaScriptエンジンがあり、主要ブラウザによって採用しているものが異なる。違うエンジンを搭載したブラウザに対応するために2種類の方法がある。
ユーザーエージェントで判断する
- navigatorオブジェクトのuserAgentプロパティ取得できる
var user = navigator.userAgent;
//"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36"
user.indexOf('MSIE') > 0
//indexOfは最初に値が現れたインデックスを返し、なければ-1を返す
機能の有無で判断する
- addEventListener()やattachEvent()(現在は非標準)など、機能を持っているかいないかで分ける
- ユーザーエージェントで判断すると、ユーザーエージェントがどの機能を有しているかをしっている必要がある。またwebブラウザが新しい機能を実装するたびに、書き換える必要がある。基本的には機能実装の有無でクロスブラウザ対策するが、IE6ではこの処理、IE7ではこの処理というような、特定ブラウザの特定バージョンだけ処理を変えたいときはユーザーエージェントで判断する。
windowオブジェクト
クライアントサイドJSではwindowオブジェクトがグローバルオブジェクトになる。プロパティはいかの種類がある。
- navigator
- location
- history
- frames
const user = navigator.userAgent;
//"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36"
//URLの取得
const url = location.href;
//"https://s.codepen.io/......
//画面遷移
location.href = 'https://dummy.URL';
//ブラウザの履歴に残らない画面遷移
location.replace = 'https://dummy.URL';
//プロトコルの取得
var pr = location.protocol;
//https:
//Historyオブジェクトは履歴をたどる
history.back(); //履歴を戻る
history.forward(); //履歴を進める
history.go(-2); //履歴を2回分戻る
//windowに複数のフレームが存在している時フレームをたどる
window.frames[1];
DOMについて
DOM(Document Object Model)とはHTMLドキュメントやXMLドキュメントをプログラムから利用するためのAPI。DOMではこれらのドキュメントをオブジェクトのツリー状の集合として取り扱う。それをDOMツリーという。DOMのなかの一つひとつのオブジェクトをノードと呼ぶ。
1. DOMはAPIである
DOM APIを利用することでドキュメントを制御する事ができる。
Javascriptを利用することで、DOM APIとつながる事ができる。
2. DOMツリーについて
<!DOCTYPE html>
<html lang="ja">
<head>
<title>タイトル</title>
<meta chareset="utf-8">
</head>
<body>
<h1>大見出し</h1>
<!-- 本文が入る -->
<p id="text">テキストテキストテキスト</p>
</body>
</html>
上記をDOMツリーで表すと
document *1
└ html *2
├ head *2
│ ├ 空白 *3
│ ├ title *2
│ ├ 空白 *3
│ ├ meta *2
│ └ 空白 *3
├ 空白 *3
└ body *2
├ 空白 *3
├ h1 *2
│ └ タイトル *3
├ 空白 *3
├ コメント *4
├ 空白 *3
├ p *2
│ └ テキストテキストテキスト *3
└ 空白 *3
*1: ドキュメントノード
*2: 要素ノード
*3: テキストノード
*4: コメントノード
- ルートはdocumentオブジェクトから始まる。
- 空白は空白ノードというテキストノードの一種
- オブジェクトの集まりをツリー状にすると上記の通り
3. ノードについて
DOMの中の一つ一つのオブジェクトをノードと呼ぶ。
要素とノードは別のもの。ノードは要素のスーパータイプに当たる。
ノードには以下の種類があり、様々に分かれている。
要素だけがノードではない。
ノードの種類 | 概要 |
---|---|
ドキュメントノード | ドキュメント全体を表すDocumentオブジェクト |
要素ノード | 要素を表すオブジェクト |
テキストノード | テキストを表すオブジェクト |
コメントノード | コメントを表すオブジェクト |
属性ノード | 属性を表すオブジェクト |
ノードのプロパティを利用して取得
//子ノードへの参照
document.childNodes[1];
//最初の子ノード。
document.firstChild;
//兄弟要素の中で次のノード
document.nextSibling;
ただし、上記のプロパティを利用すると空白のテキストノードを取得してしまう。
そのため、要素ノードを取り出すためのプロパティが存在する。
//子要素ノードへの参照
document.children[1];
//最初の子ノード。
document.firstElementChild;
//兄弟要素の中で次のノード
document.nextElementSibling;
しかし上記の方法でも、要素を取得するとなると不便である。
そのため、要素ノードを取得するためのメソッドが存在する。
getElementを利用する方法
//idを取得
document.getElementById('id');
//クラス名で取得
document.getElementsByClassName('class');
//タグ名で取得
document.getElementsByTagName('div');
//name属性から取得
document.getElementsByName('名前');
getElement系を利用した場合、取得するオブジェクトはライブオブジェクトである。
querySelectorを利用する方法
//複数の要素があっても、最初の要素だけ取得
document.querySelector('#id') === document.getElementById('id'); //true
//該当する全ての要素を取得する
document.querySelectorAll('div') === document.getElementsByTagName('div');
ライブオブジェクト
- getElement系を利用した場合、取得するオブジェクトはNodeListで、それはライブオブジェクトである。
- querySelectorを利用した場合、取得するオブジェクトはStaticNodeList。ライブオブジェクトではない。
ライブオブジェクトとはなにか
JSの操作で文章に変更が加えられたときに、同時にその内容が変化するということ。生きている、動的なオブジェクトであるということ。
//ライブオブジェクトの場合
const divs = document.getElementsByTagName('div');
console.log(divs.length); // -> 0
const newDiv = document.createElement('div');
document.body.appendChild(newDiv);
console.log(divs.length); // -> 1 変化あり
//divsはappendされる前から取得済みだったが、lengthは変化する
//ライブオブジェクトではない場合
const divs = document.querySelectorAll('div');
console.log(divs.length); // -> 0
const newDiv = document.createElement('div');
document.body.appendChild(newDiv);
console.log(divs.length); // -> 0 変化なし
ライブオブジェクトの注意点
- forループで無限ループする
- 一度配列に変換してから実行した方がパフォーマンス性が高い
forループで無限ループする
以下の例は、div要素を取得して、それに対して入れ子のようにdiv要素を追加していく処理。現在ある要素にだけ子要素を足していきたい場合、以下の処理では無限に入れ子が続いてしまう。
const divs = document.getElementsByTagName('div');
for (let i = 0; i < divs.length ; i++) {
let newDIv = document.createElement('div');
let newText = document.createTextNode('newDiv');
newDIv.appendChild(newText);
divs[i].appendChild(newDIv)
}
理由はforループが回るごとにdivsのlengthが取得されてしまうから。
そのため、divs.lengthは最初に定義しておく必要がある。
const divs = document.getElementsByTagName('div');
for (let i = 0; len = divs.length; i < len ; i++) {
let newDIv = document.createElement('div');
let newText = document.createTextNode('newDiv');
newDIv.appendChild(newText);
divs[i].appendChild(newDIv)
}
配列に変換
配列への変換は、Arrayクラスと、callを使いオブジェクトを返す。
Array.prototype.slice.call(document.getElementsByTagName('span')
以下は、html内の全てのspan要素を取得して、100回分それらをコンソールに表示する処理を表している。そのまま取得したNodeListと、配列に変換したもの、2つの処理を表している。パフォーマンス性の違いは以下の通りである。
let elem;
elem = document.getElementsByTagName('span');
//そのまま処理
var statTime1 = new Date();
for (let i = 0; i < 100; i++) {
for (let j = 0; j < elem.length ; j++) {
console.log(elem[j])
}
}
var endTime1 = new Date();
//配列に変換処理
var statTime2 = new Date();
len = elem.length
let elem2 = Array.prototype.slice.call(document.getElementsByTagName('span'));
for (let i = 0; i < 100; i++) {
for (let j = 0;j < len; j++) {
console.log(elem2[j])
}
}
var endTime2 = new Date();
console.log(endTime1 - statTime1); //699
console.log(endTime2 - statTime2); //377
やはり、配列に変換したもののほうが、処理が早くなる結果が返る。
ノードの追加、作成、置換、削除
//作成
let newDIv = document.createElement('div');
let newText = document.createTextNode('newDiv');
//追加
newDIv.appendChild(newText);
//置換
newDiv.replaceChild(newNode, oldNode);
//削除
newDiv.removeChild(newText);
HTMLの変更、text内部の変更
//作成
const div = document.getElementById('id');
//htmlの変更
div.innerHTML = '<div>hello world</div>';
//テキストの変更
div.textContent = 'hell world';
DocumentFragmentの利用
10個のdiv要素を画面に追加する場合のDOMパフォーマンスを改善するにはどうすべきか?10回描画処理をするのであれば、パフォーマンス性が悪い。
フラグメントを作成し、それに格納。forループ終了後にそのフラグメントを親要素にappendすれば、描画の処理は1回で済ませることができる。
//最初にフラグメントを作成
let fragment = document.createDocumentFragment();
//ループを回す
for (let i = 0; i < 10; i++) {
newDiv = document.createElement('div');
fragment.appendChild(newDiv);
}
//最後に描画処理を行う。
document.getElementById('parent').appendChild(fragment);