LoginSignup
4
4

More than 5 years have passed since last update.

クライアントサイドJS

Last updated at Posted at 2018-09-04

Webアプリケーションの機能

webアプリケーションの機能を実現するためには、サーバーサイドの処とクライアントサイドの処理が必要である。サーバーサイドの処理のための言語は多くあるが、クライアントサイドの機能を実現するための言語はJavaScriptのみであると考えれても構わない。webアプリケーションの基本機能は以下の通り。

  • ドラッグアンドドロップ
  • 非同期読み込み
  • キーボードショートカット
  • アニメーション

JavaScriptの役割の一つにアプリケーションの見た目のわかりやすさや使いやすさといったユーザー体験を提供することがある。逆に言えば、Javascriptはインターフェースの実現に注力すべきで、JavaScriptだけですべての機能を実現すべきではない。理由としては

  • 多くのブラウザでJSを実行しないという設定が可能であること
  • ユーザー独自でJSを追加実行する機能を持つブラウザがあること

つまり意図した通りにJSが実行できないことがあるからである。

Webページを表示する時の流れ

基本的なHTML

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>

  1. HTMLをパース
  2. 外部JSファイル及び、CSSファイルを読み込む
  3. JavaScriptがパースされた時点で実行
  4. DOMツリーの構築完了
  5. 画像ファイルなどの外部ソースをロード
  6. すべて完了

DOMツリー構築完了後に画像ファイルなどが読み込まれることがポイント。
また、DOMツリーが構築された時点でJavascriptを実行することでユーザーにとっての待ち時間を減らすことができる。

Javascriptの記述方法と実行タイミング

  1. <script>タグ内に記述
  2. 外部JSファイルの読み込み
  3. Onload 
  4. DOMCountLoaded
  5. 動的ロード

1. <script>タグ内に記述

実行されるのは<script>タグが解析された直後なので、タグ以降のDOM要素は解析されない。そのため</body>閉じタグの直前に記述することで、他のすべてのDOM要素を解析した後に<script>タグを読み込むようにすることができる。

html
<script>
    const id = document.getElementById('sampleId');
    id.classList.toggle('js-class')
</script>

2. 外部JSファイルの読み込み

ファイルが読み込まれるのは<script>タグが解析された直後で、実行されるのはファイルが読み込まれた直後。1と同様に、</body>閉じタグの直前に記述すること。

html
<script src="js/sample.js"></script>

3. Onload 

onloadイベントハンドラに処理を記述することで、ページの読み込み完了後に処理を実行できる。ページ読み込み完了後なので、すべてのDOM要素を操作できる。
<body>タグ内に記述する方法と、JSファイル内に記述する方法がある。

html
<body onload="alert('hello')">
js
window.onload = function(){ alert('hello'); }

しかし、この場合すべてのページを読み込んだあとになるので大きな画像ファイルがあった場合、その画像ファイルの読み込みを待ってJSが実行されることになる。そのためJSの実行されるまでに必要以上に時間がかかってしまう。

4. DOMCountLoaded

onloadの待機時間問題を解決するのが、DOMCountLoaded。画像の読み込みを待つのと同時にJSの実行を行う。

js
document.addEventListener('DOMContentLoaded', function() {
  alert('hello');
})

上記は以下の処理と同じ

js
$(() => { /*DOMCountLoadedと同じタイミングで処理*/ });

5. 動的ロード

この方法を用いた場合、JSファイルのダウンロードから実行が開始されるまではその他の処理をブロックしない。

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

js
console.log('コンソール出すぜ');

様々なコンソールの種類についてはこちら
JavascriptのChromeでのデバッグ方法個人的まとめ2016

debugger

js
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プロパティ取得できる
js
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
js
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基礎】ノードの取得/属性の取得・設定

DOM(Document Object Model)とはHTMLドキュメントやXMLドキュメントをプログラムから利用するためのAPI。DOMではこれらのドキュメントをオブジェクトのツリー状の集合として取り扱う。それをDOMツリーという。DOMのなかの一つひとつのオブジェクトをノードと呼ぶ。

1. DOMはAPIである

DOM APIを利用することでドキュメントを制御する事ができる。
Javascriptを利用することで、DOM APIとつながる事ができる。

2. DOMツリーについて

html
<!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オブジェクト
要素ノード 要素を表すオブジェクト
テキストノード テキストを表すオブジェクト
コメントノード コメントを表すオブジェクト
属性ノード 属性を表すオブジェクト

ノードのプロパティを利用して取得

js
//子ノードへの参照
document.childNodes[1];

//最初の子ノード。
document.firstChild;

//兄弟要素の中で次のノード
document.nextSibling;

ただし、上記のプロパティを利用すると空白のテキストノードを取得してしまう。
そのため、要素ノードを取り出すためのプロパティが存在する。

js
//子要素ノードへの参照
document.children[1];

//最初の子ノード。
document.firstElementChild;

//兄弟要素の中で次のノード
document.nextElementSibling;

しかし上記の方法でも、要素を取得するとなると不便である。
そのため、要素ノードを取得するためのメソッドが存在する。

getElementを利用する方法

js
//idを取得
document.getElementById('id');

//クラス名で取得
document.getElementsByClassName('class');

//タグ名で取得
document.getElementsByTagName('div');

//name属性から取得
document.getElementsByName('名前');

getElement系を利用した場合、取得するオブジェクトはライブオブジェクトである。

querySelectorを利用する方法

js
//複数の要素があっても、最初の要素だけ取得
document.querySelector('#id') === document.getElementById('id'); //true

//該当する全ての要素を取得する
document.querySelectorAll('div') === document.getElementsByTagName('div');

ライブオブジェクト

  • getElement系を利用した場合、取得するオブジェクトはNodeListで、それはライブオブジェクトである。
  • querySelectorを利用した場合、取得するオブジェクトはStaticNodeList。ライブオブジェクトではない。
ライブオブジェクトとはなにか

JSの操作で文章に変更が加えられたときに、同時にその内容が変化するということ。生きている、動的なオブジェクトであるということ。

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要素を追加していく処理。現在ある要素にだけ子要素を足していきたい場合、以下の処理では無限に入れ子が続いてしまう。

js
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は最初に定義しておく必要がある。

js
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を使いオブジェクトを返す。

js
Array.prototype.slice.call(document.getElementsByTagName('span')



以下は、html内の全てのspan要素を取得して、100回分それらをコンソールに表示する処理を表している。そのまま取得したNodeListと、配列に変換したもの、2つの処理を表している。パフォーマンス性の違いは以下の通りである。

js

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

やはり、配列に変換したもののほうが、処理が早くなる結果が返る。


ノードの追加、作成、置換、削除

js
//作成
let newDIv = document.createElement('div');
let newText = document.createTextNode('newDiv');

//追加
newDIv.appendChild(newText);

//置換
newDiv.replaceChild(newNode, oldNode);

//削除
newDiv.removeChild(newText);

HTMLの変更、text内部の変更

js
//作成
const div = document.getElementById('id');

//htmlの変更
div.innerHTML = '<div>hello world</div>';

//テキストの変更
div.textContent = 'hell world';

DocumentFragmentの利用

10個のdiv要素を画面に追加する場合のDOMパフォーマンスを改善するにはどうすべきか?10回描画処理をするのであれば、パフォーマンス性が悪い。

フラグメントを作成し、それに格納。forループ終了後にそのフラグメントを親要素にappendすれば、描画の処理は1回で済ませることができる。

js
//最初にフラグメントを作成
let fragment = document.createDocumentFragment();

//ループを回す
for (let i = 0; i < 10; i++) {
  newDiv = document.createElement('div');
  fragment.appendChild(newDiv);
}

//最後に描画処理を行う。
document.getElementById('parent').appendChild(fragment);
4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4