LoginSignup
25

More than 5 years have passed since last update.

第2回 Webフロントエンド開発勉強会 〜楽曲検索アプリ作成 〜

Last updated at Posted at 2018-06-17

前回に続き、今回は iTunes Search API を利用した 楽曲検索アプリ作成 を実装することを目的として、そのために必要な JavaScript の知識と、JavaScript を学ぶうえで覚えておきたい知識の説明をしていきます。

第1回「Webフロントエンド開発勉強会」を実施していない方は事前に実施しておいてください

変数の巻き上げ

JavaScript には、関数内で宣言されたローカル変数は、すべてその関数の先頭で宣言されたものとみなされる 変数の巻き上げ という概念があり、これを知らないと意図しない動作をすることがありますので注意してください。

var value = 1;           // (1)
function func() {
    console.log(value);  // (2)
    var value = 2;       // (3)
    console.log(value);
}
func();

// 実行結果
// undefined
// 2

上記を見ると (2) での出力結果は (1) で設定した値の1が出力されそうに見えますが、(3) で value を宣言しているのでこの宣言が func関数の先頭に巻き上げられ、(2) の value は初期化前の (3)、つまり undefined が出力されます。
上記の例は以下と同様に解釈されます。

var value = 1;
function func() {
    var value;
    console.log(value);
    value = 2;
    console.log(value);
}
func();

this が示す値

Java 等の言語では、this は 自身のオブジェクトを参照しますが、JavaScript の this は少し変わっています。

以下の 1 ~ 5 は、いずれも this を出力する thisLog() 関数を呼び出していますが、出力結果はどうなるでしょうか?

function thisLog () {
    console.log(this);
}

// 1
thisLog();

// 2
new thisLog();

// 3
thisLog.call({ value: 'call です' });

// 4
thisLog.apply({ value: 'apply です' });

// 5
var obj = { value: 'obj です', method: thisLog };
obj.method();
実行結果
Window {postMessage: , blur: , focus: , close: , frames: Window, …}
thisLog {}
Object {value: "call です"}
Object {value: "apply です"}
Object {value: "obj です", method: }

同じ関数の this を出力しているのに、結果が全て異なります。
JavaScript の this は、呼び出し元によって参照する内容が変わってくるのです。

他にも呼び出し方はありますが、今回はこの 5パターンについて説明します。

1. 関数呼び出し

関数で呼び出した場合は、this はグローバルオブジェクトを参照します。
Webブラウザで実行する JavaScript ではグローバルオブジェクトが window のため、window が出力されています。

function thisLog () {
    this.action = '関数呼び出し';
    console.log(this);
}
// 関数呼び出し
thisLog();

// thisはグローバルオブジェクトを示すので、this.action は window.action に代入したことになる
console.log('action', window.action);

// 出力結果
// Window {postMessage: , blur: , focus: , close: , frames: Window, …}
// action 関数呼び出し

2. コンストラクタ呼び出し

function に対して new を付けて呼び出すとオブジェクトが作成され、その関数の内容はコンストラクタになります。
このようにしてコンストラクタ呼び出しをした場合、this はそのオブジェクト自身を参照します。

function thisLog () {
    this.action = 'コンストラクタ呼び出し';
    console.log(this);
}
// コンストラクタ呼び出し
var obj = new thisLog();

// thisは生成したオブジェクトを示すので、obj に代入されている
console.log('action', obj.action);

// 実行結果
// thisLog {action: "コンストラクタ呼び出し"}
// action コンストラクタ呼び出し

3. call 呼び出し

JavaScript には call メソッドが用意されており、call で呼び出した関数の this は、call の第一引数で渡した値を参照します。
関数に引数を渡す場合は、call の第二引数以降に値を渡します。

function thisLog (p1, p2, p3) {
    this.action = 'call 呼び出し';
    console.log(this);
    console.log(p1, p2, p3);
}
// call 呼び出し
var obj = { value: 'call です' };
thisLog.call(obj, 1, 2, 3);

// thisは第一引数に渡したオブジェクトを示すので、obj に代入されている
console.log('action', obj.action);

// 実行結果
// Object {value: "call です", action: "call 呼び出し"}
// 1 2 3
// action call 呼び出し

4. apply 呼び出し

call と同じように apply で呼び出した関数の this は、apply の第一引数で渡した値を参照します。
call との違いは、引数を渡す際に call は第二引数以降に渡していたのに対し、apply は第二引数に配列で渡します。

function thisLog (p1, p2, p3) {
    this.action = 'apply 呼び出し';
    console.log(this);
    console.log(p1, p2, p3);
}
// apply 呼び出し
var obj = { value: 'apply です' };
thisLog.apply(obj, [1, 2, 3]);

// thisは第一引数に渡したオブジェクトを示すので、obj に代入されている
console.log('action', obj.action);

// 実行結果
// Object {value: "apply です", action: "apply 呼び出し"}
// 1 2 3
// action apply 呼び出し

5. メソッド呼び出し

オブジェクトにメソッドとして定義して呼び出した場合、thisは呼び出し元オブジェクトを参照します。

function thisLog () {
    this.action = 'メソッド呼び出し';
    console.log(this);
}
// メソッド呼び出し
var obj = { value: 'obj です', method: thisLog };
obj.method();

// thisは呼び出し元オブジェクトを示すので、obj に代入されている
console.log('action', obj.action);

// 実行結果
// Object {value: "obj です", method: , action: "メソッド呼び出し"}
// action メソッド呼び出し

これらのパターンは以下でより詳しく説明されています。
JavaScriptの「this」は「4種類」??

新しいウィンドウで開く

JavaScript で新しいウィンドウで開く場合には、window.open(URL, ウィンドウ名 [, オプション]) 関数を使用します。

第一引数には、開きたいURLを指定します。
第二引数には、現在のタブに開く場合は _self、別のタブに開くには _blank、別のウィンドウで開くには ウィンドウ名 等を指定します。
第三引数には、ウィンドウサイズやメニューバーの有無、ツールバーの有無等を指定します。

index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Window open</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script src="main.js"></script>
</head>
<body>
    <button onclick="onOpenClick()">Open</button>
    <button onclick="onCloseClick()">Close</button>
</body>
</html>
main.js
var win;

function onOpenClick(button) {
    win = window.open('https://qiita.com/', 'qiita', 'width=640, height=640');
}

function onCloseClick() {
    if (win) {
        win.close();
    }
}

チャレンジ

Openボタンを押したときに、ラジオボタンで選択した形式でウィンドウを開くようにしてください。

index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Window open</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script src="main.js"></script>
</head>
<body>
    <form id="targetForm">
        <div><input type="radio" name="target" value="blank" checked="checked">新しいタブで開く</div>
        <div><input type="radio" name="target" value="self">現在のタブで開く</div>
        <div><input type="radio" name="target" value="new-name">新しいウィンドウで開く(ウィンドウ名指定)</div>
        <div><input type="radio" name="target" value="new-undefined">新しいウィンドウで開く(ウィンドウ名未指定)</div>
    </form>
    <div>
            <button onclick="onOpenClick()">Open</button>
    </div>
        <div style="margin-top: 60px;">
        <button onclick="onCloseClick()">Close</button>
    </div>
</body>
</html>
main.js
function onOpenClick(button) {
}

function onCloseClick() {
}

ヒント

答え
main.js
var winArray = [];

function onOpenClick(button) {

    var form = document.getElementById('targetForm');
    var url = './index.html';
    var win;

    switch (form.target.value) {
        case 'blank':
            win = window.open(url, '_blank');
            break;
        case 'self':
            win = window.open(url, '_self');
            break;
        case 'new-name':
            win = window.open(url, 'window-name', 'width=640, height=640');
            break;
        case 'new-undefined':
            win = window.open(url, undefined, 'width=640, height=640');
            break;
    }

    if (win) {
        winArray.push(win);
    }
}

function onCloseClick() {
    for (var i in winArray) {
        winArray[i].close();
    }
}

イベントの伝播

JavaScript では click や change 等のイベントが発生した際に、そのイベント親要素へ伝播する特性を持っています。

イベントバブリング

以下の HTML と JavaScript を実行してチェックボックスをクリックしてみてください。

index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>イベントの伝播</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
    <div id="div1">
        <p id="p1">
            <span id="span1">
                <input type="checkbox" id="checkbox1">
            </span>
        </p>
    </div>
</body>
<script src="main.js"></script>
</html>
main.js
function onClickHandler(event) {
    console.log('フェーズ' + event.eventPhase, event.type + 'イベント', event.currentTarget);
}

window.addEventListener('click', onClickHandler);
document.addEventListener('click', onClickHandler);
var elements = document.getElementsByTagName('*');
for (var i = 0; i < elements.length; i++) {
    elements[i].addEventListener('click', onClickHandler);
}

イベントリスナーの引数にはイベントオブジェクトが渡され、type には click や change 等のイベント名、currentTarget にはイベントを受け取った要素が入っており、チェックボックスをクリックすると以下の結果が出力されます。

実行結果
フェーズ2 clickイベント
input#checkbox1 {accept: "", alt: "", autocomplete: "", autofocus: false, defaultChecked: false, …}

フェーズ3 clickイベント
span#span1 {title: "", lang: "", translate: true, dir: "", dataset: DOMStringMap, …}

フェーズ3 clickイベント
p#p1 {align: "", title: "", lang: "", translate: true, dir: "", …}

フェーズ3 clickイベント
div#div1 {align: "", title: "", lang: "", translate: true, dir: "", …}

フェーズ3 clickイベント
body {text: "", link: "", vLink: "", aLink: "", bgColor: "", …}

フェーズ3 clickイベント
html {version: "", title: "", lang: "", translate: true, dir: "", …}

フェーズ3 clickイベント
#document {location: Location, implementation: DOMImplementation, URL: "http://localhost:5500/04_event_bubbling_2/", documentURI: "http://localhost:5500/04_event_bubbling_2/", origin: "http://localhost:5500", …}

フェーズ3 clickイベント
Window {postMessage: , blur: , focus: , close: , frames: Window, …}

チェックボックスの要素しかクリックしていないのに、親要素のイベントリスナーも呼ばれていることがわかります。

これは、イベントバブリングと呼ばれる仕組みによるもので、イベントが発生した際には以下のようなフェーズでイベントが伝播していきます。

イベントフェーズ.png

チェックボックスをクリックしたとき、window からチェックボックスの親要素(この場合はspan)までイベントが伝わり、2. ターゲットフェーズ でイベント発生要素(button)にイベントが伝わります。
その他、3. バブリングフェーズ でチェックボックスに一番近い親要素から順に window へイベントが伝わります。

先ほどのイベントオブジェクトの eventPhase にはこのイベントフェーズが格納されており、1 がキャプチャフェーズ、2 がイベントフェーズ、3 がバブリングフェーズを意味します。

イベントキャプチャリング

イベントリスナーを追加する関数 addEventListener() には第三引数があり、省略または false を指定した場合にはバブリングフェーズ、true を指定した場合にはキャプチャリングでリスナーが呼ばれます。

main.js
function onClickHandler(event) {
    console.log('フェーズ' + event.eventPhase, event.type + 'イベント', event.currentTarget);
}

window.addEventListener('click', onClickHandler, true);
document.addEventListener('click', onClickHandler, true);
var elements = document.getElementsByTagName('*');
for (var i = 0; i < elements.length; i++) {
    elements[i].addEventListener('click', onClickHandler, true);
}
実行結果
フェーズ1 clickイベント
Window {postMessage: , blur: , focus: , close: , frames: Window, …}

フェーズ1 clickイベント
#document {location: Location, implementation: DOMImplementation, URL: "http://localhost:5500/04_event_bubbling_3/", documentURI: "http://localhost:5500/04_event_bubbling_3/", origin: "http://localhost:5500", …}

フェーズ1 clickイベント
html {version: "", title: "", lang: "", translate: true, dir: "", …}

フェーズ1 clickイベント
body {text: "", link: "", vLink: "", aLink: "", bgColor: "", …}

フェーズ1 clickイベント
div#div1 {align: "", title: "", lang: "", translate: true, dir: "", …}

フェーズ1 clickイベント
p#p1 {align: "", title: "", lang: "", translate: true, dir: "", …}

フェーズ1 clickイベント
span#span1 {title: "", lang: "", translate: true, dir: "", dataset: DOMStringMap, …}

フェーズ2 clickイベント
main.js:2
input#checkbox1 {accept: "", alt: "", autocomplete: "", autofocus: false, defaultChecked: false, …}

イベント伝播の停止

イベントオブジェクトの stopPropagation() 関数を呼ぶことで、それ以降の親要素へのイベントの伝播を停止します。

main.js
function onClickHandler(event) {
    console.log('フェーズ' + event.eventPhase, event.type + 'イベント', event.currentTarget);

    // span1 でイベントの伝播を停止する
    if (event.currentTarget.id === 'span1') {
        event.stopPropagation();
    }
}

window.addEventListener('click', onClickHandler);
document.addEventListener('click', onClickHandler);
var elements = document.getElementsByTagName('*');
for (var i = 0; i < elements.length; i++) {
    elements[i].addEventListener('click', onClickHandler);
}
実行結果
フェーズ2 clickイベント
input#checkbox1 {accept: "", alt: "", autocomplete: "", autofocus: false, defaultChecked: false, …}

フェーズ3 clickイベント
span#span1 {title: "", lang: "", translate: true, dir: "", dataset: DOMStringMap, …}

イベント既定の動作の停止

イベントオブジェクトの stopPropagation() 関数を呼ぶことで、イベントの規定の動作(チェックボックスの場合はチェックの付け外し)を停止できます。

main.js
function onClickHandler(event) {
    console.log('フェーズ' + event.eventPhase, event.type + 'イベント', event.currentTarget);
    if (event.currentTarget.id === 'checkbox1') {

        // checkbox1 の既定の動作を停止
        event.preventDefault();
    }
}

window.addEventListener('click', onClickHandler);
document.addEventListener('click', onClickHandler);
var elements = document.getElementsByTagName('*');
for (var i = 0; i < elements.length; i++) {
    elements[i].addEventListener('click', onClickHandler);
}

チャレンジ

チェックボックスがチェックされたときに、class に bg がある div要素の class に bg-blue を追加して背景色を青色にしてください。
※変更するのは main.js だけです

index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>チャレンジ</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" type="text/css" media="screen" href="main.css" />
</head>
<body>
    <div class="bg">
        <div>
            <div class="bg">
                <div>
                    <div class="bg">
                        <div>
                            <input type="checkbox">
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</body>
<script src="main.js"></script>
</html>
main.css
div {
    padding: 16px;
    background: white;
    border: 1px solid gray;
}

.bg-blue {
    background: blue;
}
main.js
// div タグを取得
var divElements = document.getElementsByTagName('div');
for (var i = 0; i < divElements.length; i ++) {
    // changeイベントのリスナーを追加
    divElements[i].addEventListener('change', function(event) {

        // ここに処理を追加してください
    });
}

ヒント

  • イベントオブジェクト(addEventListener のコールバック関数の引数) の event.target にイベント発生要素が入っている
  • イベントオブジェクトの event.currentTarget にイベントを受け取った要素が入っている
  • DOMオブジェクトの classList で 要素の class リストが取得できる(class はスペース区切りで複数持てる)
  • classList.contains('クラス名') で クラス名が class に含まれるか判定できる
  • classList.add('クラス名') で class の追加、classList.remove('クラス名') で class の削除ができる

答え
main.js
// div タグを取得
var divElements = document.getElementsByTagName('div');
for (var i = 0; i < divElements.length; i ++) {
    // changeイベントのリスナーを追加
    divElements[i].addEventListener('change', function(event) {
        // イベントを受け取った要素の class に bg が含まれる場合
        if (event.currentTarget.classList.contains('bg')) {
            // イベント発生要素がチェックされている場合
            if (event.target.checked) {
                // イベントを受け取った要素の class に bg-blue を追加
                event.currentTarget.classList.add('bg-blue');
            } else {
                // イベントを受け取った要素の class の bg-blue を削除
                event.currentTarget.classList.remove('bg-blue');
            }
        }
    });
}

クロージャ

JavaScript では、関数とその関数が作られた状態を保持するオブジェクトがあり、これをクロージャと言います。

以下の createSum() 関数は、関数内で定義した無名関数を返しています。
ここで返された関数がクロージャとなり、自身の関数と自身が作成された createSum() の状態を保持しているため、createSum() で返された関数に数値を渡して呼び出すと、createSum() の変数 total が加算されていきます。

function createSum() {
    var total = 0;
    return function (num) {
        total += num;
        return total;
    }
}

var sum = createSum();

console.log(sum(1));
// 出力結果
// 1

console.log(sum(2));
// 出力結果
// 3

console.log(sum(3));
// 出力結果
// 6

チャレンジ

引数が null の場合は作成時に指定した初期値、null でない場合はそのままの値を返すクロージャを作成する ifNull() 関数を作成してください。

var value = ifNull('初期値');

console.log(value(1));
console.log(value(null));
console.log(value(3));

// 実行結果
// 1
// 初期値
// 3

答え
function ifNull(defaultValue) {
    return function (value) {
        if (value === null) {
            return defaultValue;
        } else {
            return value;
        }
    }
}

プロトタイプ

Java や C# のようなオブジェクト指向はクラスベースの言語が一般的ですが、JavaScript は プロトタイプベースと言われる言語です。

ES6 で class キーワードが導入されましたが、これはシンタックスシュガーであり、JavaScript はプロトタイプベースです

JavaScript のオブジェクトは、プロトタイプと呼ばれる他のオブジェクトへの内部的な繋がりを持っており、あるオブジェクトのプロトタイプが null に到達するまでそれが続きます。

以下のコードを実行してみてください。

var Animal = function () {
    this.name = '動物';
};

var Cat = function () {
    this.name = '';
    this.family = 'ネコ科';
};

var Rion = function () {
    this.name = 'ライオン';
};

Cat.prototype = new Animal();
Rion.prototype = new Cat();

var rion = new Rion();

console.log(rion);

コンソールに以下が出力されます。

image.png

__proto__ がそのオブジェクトのプロトタイプ(他のオブジェクトとの繋がり)となり、rion.__proto__ === Rion.prototype となります。

Rion オブジェクトのプロトタイプに Cat オブジェクト、Cat オブジェクトのプロトタイプに Animal オブジェクトの繋がりがあることがわかります。

以下を実行すると、rionfamily プロパティを持っていませんが、ネコ科 が出力されます。

console.log(rion.family);

// 実行結果
// ネコ科

これは、プロトタイプチェーン という仕組みで、オブジェクトにプロパティが存在しなかった場合、__proto__ のオブジェクトを参照して、指定したプロパティが見つかるか、__proto__ が null になるまで参照していきます。

この場合、rionfamily がないのでプロトタイプの cat を参照し、catfamily が存在するので、ネコ科 が出力されています。

チャレンジ

引数で渡した文字を配列の各要素に結合する combine() 関数を Arrayprototype に追加してください。

// ここに追加

var arr = ['A', 'B', 'C'];

console.log(arr.combine('1'));

// 実行結果
// ["A1", "B1", "C1"]

答え
Array.prototype.combine = function (value) {
    for (var i in this) {
        this[i] += value;
    }
    return this;
};

var arr = ['A', 'B', 'C'];

console.log(arr.combine('1'));

// 実行結果
// ["A1", "B1", "C1"]

HTTPリクエスト

JavaScript で サーバーとHTTP通信するには、XMLHttpRequest を利用することで可能です。

XMLHttpRequest で生成したオブジェクトの open() で第一引数に GET や POST 等のメソッド、第二引数に URL を定義し、send() を呼び出すと処理が実行され、レスポンスを受け取ったときに onload() で定義した関数が呼ばれます。

以下は、iTunes API で情報を取得した場合の例です。

// HTTPリクエストオブジェクトを生成
let request = new XMLHttpRequest();

// HTTPメソッド、リクエストURLを設定
request.open('GET', 'https://itunes.apple.com/lookup?id=292706922');

// レスポンス型をJSONに設定
request.responseType = 'json';

// レスポンスを受け取ったときの処理
request.onload = function() {
    console.log(request.response);
};

// リクエスト実行
request.send();

// 実行結果
// {
//   resultCount: 1,
//   results: [{
//     amgArtistId: 989429
//     artistId: 292706922
//     artistLinkUrl: "http: /itunes.apple.com/us/artist/akb48/292706922?uo=4"
//     artistName: "AKB48"
//     artistType: "Artist"
//     primaryGenreId: 27
//     primaryGenreName: "J-Pop"
//     wrapperType: "artist"
//   }]
// }

ECMAScript 2015

ECMAScript 2015 (ES2015 や ES6 と呼ばれています)は、2015年に標準化された JavaScript の標準規格です。
それ以前の ES5 の問題点を解消したり、新しい機能の追加されたりして、より便利にシンプルに記述できるようになりました。

この記事では詳細な説明はしませんが、以下の記事に詳しくまとめられています。

楽曲検索アプリ作成

以下の HTMLをベースに各ステップを実装して iTunes Search API で楽曲を検索するアプリを作成してください。

index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Page Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" type="text/css" media="screen" href="main.css" />
    <script src="main.js"></script>
</head>
<body>
    <input type="text" id="inputKeyword">
    <button onclick="onSearch()">検索</button>
    <div id="resultContent"></div>
</body>
</html>

iTunes Search API の仕様は以下を参照してください。
https://affiliate.itunes.apple.com/resources/documentation/itunes_search_api_jp/

Step1. 楽曲名リストを表示

検索ボタンを押したときに、ダミーデータの楽曲名をリスト表示してください。

step1

main.js
function onSearch() {
    // 検索結果のダミーデータ
    var results = [
        { trackName: '楽曲名1' },
        { trackName: '楽曲名2' },
        { trackName: '楽曲名3' },
        { trackName: '楽曲名4' },
        { trackName: '楽曲名5' }
    ];
}

ヒント

答え
main.js
function onSearch() {
    // 検索結果のダミーデータ
    var results = [
        { trackName: '楽曲名1' },
        { trackName: '楽曲名2' },
        { trackName: '楽曲名3' },
        { trackName: '楽曲名4' },
        { trackName: '楽曲名5' }
    ];
    // 結果要素を取得
    var content = document.getElementById('resultContent');

    // 結果要素をクリア
    content.innerHTML = '';

    // 結果の件数分繰り返す
    for (var index in results) {
        var item = results[index];

        // 楽曲要素を作成
        var div = document.createElement('div');
        div.innerHTML = item.trackName;         // 曲名

        // 結果要素の子要素に楽曲要素を追加
        content.appendChild(div);
    }
}

Step2. 画像表示とスタイル設定

ダミーデータを変更して、アートワーク(画像)、楽曲名(太字)、アーティスト名を表示してください。

step2

    // 検索結果のダミーデータ
    var results = [
        { artistName: 'アーティスト名1', trackName: '楽曲名1', artworkUrl100: 'https://placehold.jp/03a9f4/ffffff/100x100.png?text=Image1' },
        { artistName: 'アーティスト名2', trackName: '楽曲名2', artworkUrl100: 'https://placehold.jp/cddc39/ffffff/100x100.png?text=Image2' },
        { artistName: 'アーティスト名3', trackName: '楽曲名3', artworkUrl100: 'https://placehold.jp/deb887/ffffff/100x100.png?text=Image3' },
        { artistName: 'アーティスト名4', trackName: '楽曲名4', artworkUrl100: 'https://placehold.jp/ff9800/ffffff/100x100.png?text=Image4' },
        { artistName: 'アーティスト名5', trackName: '楽曲名5', artworkUrl100: 'https://placehold.jp/ff5722/ffffff/100x100.png?text=Image5' }
    ];

ヒント

答え
main.js
function onSearch() {
    // 検索結果のダミーデータ
    var results = [
        { artistName: 'アーティスト名1', trackName: '楽曲名1', artworkUrl100: 'https://placehold.jp/03a9f4/ffffff/100x100.png?text=Image1' },
        { artistName: 'アーティスト名2', trackName: '楽曲名2', artworkUrl100: 'https://placehold.jp/cddc39/ffffff/100x100.png?text=Image2' },
        { artistName: 'アーティスト名3', trackName: '楽曲名3', artworkUrl100: 'https://placehold.jp/deb887/ffffff/100x100.png?text=Image3' },
        { artistName: 'アーティスト名4', trackName: '楽曲名4', artworkUrl100: 'https://placehold.jp/ff9800/ffffff/100x100.png?text=Image4' },
        { artistName: 'アーティスト名5', trackName: '楽曲名5', artworkUrl100: 'https://placehold.jp/ff5722/ffffff/100x100.png?text=Image5' }
    ];
    // 結果要素を取得
    var content = document.getElementById('resultContent');

    // 結果要素をクリア
    content.innerHTML = '';

    // 結果の件数分繰り返す
    for (var index in results) {
        var item = results[index];

        // 楽曲要素を作成
        var div = document.createElement('div');
        div.innerHTML = '<img src=' + item.artworkUrl100 + '>' +        // 画像
            '<div class="track-name">' + item.trackName + '</div>' +    // 曲名
            '<div class="artist-name">' + item.artistName + '</div>';   // アーティスト名

        // 結果要素の子要素に楽曲要素を追加
        content.appendChild(div);
    }
}
main.css
.track-name {
    font-weight: bold;
}

Step3. APIで取得した結果を表示

XMLHttpRequest を使って、iTunes Search API を呼び出し、テキストボックスに入力したキーワードのアーティストの楽曲を検索し取得結果を表示してください。

step3

ヒント

  • XMLHttpRequest の使い方は HTTP を参照
  • iTunes API の使い方は iTunes APIの使い方まとめ を参照
  • パラメータは term, country, media, attribute を指定する

答え
main.js
function onSearch() {
    // 入力した要素を取得
    var inputKeyword = document.getElementById('inputKeyword');
    // 結果要素を取得
    var content = document.getElementById('resultContent');
    // リクエストURLを定義
    var url = 'https://itunes.apple.com/search?term=' + inputKeyword.value + '&country=jp&media=music&attribute=artistTerm';
    // HTTPリクエストオブジェクトを生成
    var request = new XMLHttpRequest();

    // HTTPメソッド、リクエストURL、レスポンス型を設定
    request.open('GET', url);
    request.responseType = 'json';

    // 検索結果の応答を受け取ったときの処理
    request.onload = function() {

        // 結果要素をクリア
        content.innerHTML = '';

        // 結果の件数分繰り返す
        for (var index in request.response.results) {
            var item = request.response.results[index];

            // 楽曲要素を作成
            var div = document.createElement('div');
            div.innerHTML = '<img src=' + item.artworkUrl100 + '>' +        // 画像
                '<div class="track-name">' + item.trackName + '</div>' +    // 曲名
                '<div class="artist-name">' + item.artistName + '</div>';   // アーティスト名

            // 結果要素の子要素に楽曲要素を追加
            content.appendChild(div);
        }
    }

    // 検索実行
    request.send();
}

Step4. 検索対象の切り替え

アーティスト名、曲名のラジオボタンを追加して、選択した値を対象として検索するようにしてください。

step4

ヒント

答え
index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Page Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" type="text/css" media="screen" href="main.css" />
    <script src="main.js"></script>
</head>
<body>
    <input type="text" id="inputKeyword">
    <button onclick="onSearch()">検索</button>
    <form id="attributeForm">
        <input type="radio" name="attribute" value="artistTerm" checked="checked">アーティスト名
        <input type="radio" name="attribute" value="songTerm">曲名
    </form>
    <div id="resultContent"></div>
</body>
</html>
main.js
function onSearch() {
    // 入力した要素を取得
    var inputKeyword = document.getElementById('inputKeyword');
    // ラジオボタンのフォーム要素を取得
    var attributeForm = document.getElementById('attributeForm');
    // 結果要素を取得
    var content = document.getElementById('resultContent');
    // リクエストURLを定義
    var url = 'https://itunes.apple.com/search?term=' + inputKeyword.value + '&country=jp&media=music&attribute=' + attributeForm.attribute.value;
    // HTTPリクエストオブジェクトを生成
    var request = new XMLHttpRequest();

    // HTTPメソッド、リクエストURL、レスポンス型を設定
    request.open('GET', url);
    request.responseType = 'json';

    // 検索結果の応答を受け取ったときの処理
    request.onload = function() {

        // 結果要素をクリア
        content.innerHTML = '';

        // 結果の件数分繰り返す
        for (var index in request.response.results) {
            var item = request.response.results[index];

            // 楽曲要素を作成
            var div = document.createElement('div');
            div.innerHTML = '<img src=' + item.artworkUrl100 + '>' +        // 画像
                '<div class="track-name">' + item.trackName + '</div>' +    // 曲名
                '<div class="artist-name">' + item.artistName + '</div>';   // アーティスト名

            // 結果要素の子要素に楽曲要素を追加
            content.appendChild(div);
        }
    }
    request.send();
}

Step5. iTunes ページ表示

検索結果の楽曲をクリックしたときに、新しいウィンドウで iTunes の楽曲ページを表示してください。

step5

ヒント

  • iTunesページの URLは trackViewUrl で取得できる
  • 新しいウィンドウで開く方法は 新しいウィンドウで開く を参照
  • 配列のループ処理は forEach で関数としてループする方法もある

答え

どの楽曲をクリックしても最後に表示されている楽曲が開いてしまった人もいるのではないでしょうか。
これは、(変数の巻き上げ)[#変数の巻き上げ] が発生しているからです。

検索結果の繰り返し文を for in 文で実装した場合、インデクスとして受け取る変数は onSearch() 関数の先頭で宣言したものと扱われます。
そのため、楽曲をクリックしたときのインデックスの値はループして一番最後に到達した値が入っている状態となり、どの楽曲をクリックしても最後の楽曲が表示されてしまいます。

これを回避するには、forEach を使用する方法があります。
forEach は、配列の各要素を関数としてループするので、その関数内で宣言した変数は onSearch() まで巻き上げられることはありません。

main.js
function onSearch() {
    // 入力した要素の取得
    var inputKeyword = document.getElementById('inputKeyword');
    // ラジオボタンのフォーム要素を取得
    var attributeForm = document.getElementById('attributeForm');
    // 結果要素の取得
    var content = document.getElementById('resultContent');
    // リクエストURLを定義
    var url = 'https://itunes.apple.com/search?term=' + inputKeyword.value + '&country=jp&media=music&attribute=' + attributeForm.attribute.value;
    // HTTPリクエストオブジェクトを生成
    var request = new XMLHttpRequest();

    // HTTPメソッド、リクエストURL、レスポンス型を設定
    request.open('GET', url);
    request.responseType = 'json';

    // 検索結果の応答を受け取ったときの処理
    request.onload = function() {

        // 結果要素をクリア
        content.innerHTML = '';

        // 結果の件数分繰り返す
        request.response.results.forEach(function (item) {

            // 楽曲要素を作成
            var div = document.createElement('div');
            div.innerHTML = '<img src=' + item.artworkUrl100 + '>' +        // 画像
                '<div class="track-name">' + item.trackName + '</div>' +    // 曲名
                '<div class="artist-name">' + item.artistName + '</div>';   // アーティスト名

            // クリックイベントリスナー設定
            div.addEventListener('click', function (event) {
                // iTunesのページを新しいウィンドウで開く
                window.open(item.trackViewUrl, 'trackView', 'width=400, height=600');
            });

            // 結果要素の子要素に楽曲要素を追加
            content.appendChild(div);
        });
    }
    request.send();
}

Step6. 横並びレイアウト

検索結果を横に並べて表示してください。

step6

ヒント

答え
main.js
function onSearch() {
    // 入力した要素の取得
    var inputKeyword = document.getElementById('inputKeyword');
    // ラジオボタンのフォーム要素を取得
    var attributeForm = document.getElementById('attributeForm');
    // 結果要素の取得
    var content = document.getElementById('resultContent');
    // リクエストURLを定義
    var url = 'https://itunes.apple.com/search?term=' + inputKeyword.value + '&country=jp&media=music&attribute=' + attributeForm.attribute.value;
    // HTTPリクエストオブジェクトを生成
    var request = new XMLHttpRequest();

    // HTTPメソッド、リクエストURL、レスポンス型を設定
    request.open('GET', url);
    request.responseType = 'json';

    // 検索結果の応答を受け取ったときの処理
    request.onload = function() {

        // 結果要素をクリア
        content.innerHTML = '';

        // 結果の件数分繰り返す
        request.response.results.forEach(function (item) {

            // 楽曲要素を作成
            var div = document.createElement('div');
            div.classList.add('result-item');
            div.innerHTML = '<img src=' + item.artworkUrl100 + '>' +        // 画像
                '<div class="track-name">' + item.trackName + '</div>' +    // 曲名
                '<div class="artist-name">' + item.artistName + '</div>';   // アーティスト名

            // クリックイベントリスナー設定
            div.addEventListener('click', function (event) {
                // iTunesのページを新しいウィンドウで開く
                window.open(item.trackViewUrl, 'trackView', 'width=400, height=600');
            });

            // 結果要素の子要素に楽曲要素を追加
            content.appendChild(div);
        });
    }
    request.send();
}
main.css
#resultContent {
    display: flex;
    flex-wrap: wrap;
}

.result-item {
    width: 200px;
    margin-bottom: 16px;
}

.track-name {
    font-weight: bold;
}

Extra1. ページング

ページング機能を追加してください。

ex1.gif

ヒント

答え
index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Page Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" type="text/css" media="screen" href="main.css" />
    <script src="main.js"></script>
</head>
<body>
    <input type="text" id="inputKeyword">
    <button onclick="onSearch(0)">検索</button>
    <form id="attributeForm">
        <input type="radio" name="attribute" value="artistTerm" checked="checked">アーティスト名
        <input type="radio" name="attribute" value="songTerm">曲名
    </form>
    <div id="resultContent"></div>
    <div id="pager">
        <button onclick="onPrev()"></button>
        <span id="pageIndex"></span>
        <button onclick="onNext()"></button>
    </div>
</body>
</html>
main.css
#resultContent {
    display: flex;
    flex-wrap: wrap;
}

#pager {
    display: flex;
    justify-content: space-around;
}

.result-item {
    width: 200px;
    margin-bottom: 16px;
}

.track-name {
    font-weight: bold;
}
main.js
var limit = 10;
var currentPage = 0;

function onSearch(page) {
    // 入力した要素の取得
    var inputKeyword = document.getElementById('inputKeyword');
    // ラジオボタンのフォーム要素を取得
    var attributeForm = document.getElementById('attributeForm');
    // 結果要素の取得
    var content = document.getElementById('resultContent');
    // リクエストURLを定義
    var url = 'https://itunes.apple.com/search?term=' + inputKeyword.value +
        '&country=jp&media=music&attribute=' + attributeForm.attribute.value +
        '&limit=' + limit + '&offset=' + (limit * page);
    // HTTPリクエストオブジェクトを生成
    var request = new XMLHttpRequest();

    // HTTPメソッド、リクエストURL、レスポンス型を設定
    request.open('GET', url);
    request.responseType = 'json';

    // 検索結果の応答を受け取ったときの処理
    request.onload = function() {

        currentPage = page;

        // 結果要素をクリア
        content.innerHTML = '';

        // 結果の件数分繰り返す
        request.response.results.forEach(function (item) {

            // 楽曲要素を作成
            var div = document.createElement('div');
            div.classList.add('result-item');
            div.innerHTML = '<img src=' + item.artworkUrl100 + '>' +        // 画像
                '<div class="track-name">' + item.trackName + '</div>' +    // 曲名
                '<div class="artist-name">' + item.artistName + '</div>';   // アーティスト名

            // クリックイベントリスナー設定
            div.addEventListener('click', function (event) {
                // iTunesのページを新しいウィンドウで開く
                window.open(item.trackViewUrl, 'trackView', 'width=400, height=600');
            });

            // 結果要素の子要素に楽曲要素を追加
            content.appendChild(div);

            // ページインデックスの表示を更新
            document.getElementById('pageIndex').innerText = (currentPage + 1);
        });
    }
    request.send();
}

function onNext() {
    onSearch(currentPage + 1);
}

function onPrev() {
    if (0 < currentPage) {
        onSearch(currentPage - 1);
    }
}

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
25