前回の続きです。 宣言通り、let, template, fetch の三本立てです。
局所変数を定義する let
注意) Safari では動きません
JavaScript では変数を定義するときに
var を使います。
この場合、変数のスコープは関数単位になります。
そのため、以下のような場合には for のスコープの外でも i の値が参照できてしまいます。
for (var i = 0; i < 5; i++) {
console.log(i); // 1,2,3,4
}
console.log(i) // 5
例えば、以下ようなミスをしてしまうと、書いた人の意図をとは違う動きをしてしまいます。
for (var i = 0; i < 5; i++) {
console.log(i); // 1,2,3,4
}
for (var i; i < 10; i++) { // i を初期化してない!!
console.log(i); // 5,6,7,8,9
}
let を使って変数宣言すると、ブロックスコープの変数を作ることが出来ます。
以下の例のように、ブロックの外で参照しようとしてもエラーになります。
for (let i = 0; i < 5; i++) {
console.log(i); // 1,2,3,4
}
console.log(i) // エラー
広いスコープで変数を持たせることは、メリットよりもデメリットの方が多いです。 使えるのならば、let はどんどん使っていきたいです。
要素の雛形を定義する template タグ
注意)IE と Safari では動きません
HTML5 から便利な
template というタグが追加されました。
JavaScript を使って DOM の要素を追加するとき、追加する要素の雛形を template タグで用意することが出来ます。
一覧の画面で「宛先表示」というボタンを押すと、画面にオーバーラップするような感じで宛先の一覧テーブルが表示されます。 これは ajax を使ってデータを取得して描画するので、HTML 要素の生成についても JavaScript で行う必要がありました。
let res = getResults(); // res には結果の配列が入っている
for (var i = 0; i < res.length; i++) {
// HTML の文字列を生成し、tbody に追加
document.querySelector('#destinationTable tbody').innerHTML += '<tr>\
<td>' + (res[i].member_id ? res[i].member_id : '-') + '</td>\
<td>' + res[i].nickname + '</td>\
<td>' + res[i].mail + '</td>\
<td>' + res[i].delivery_status + '</td>\
<td>' + (res[i].campaign_code ? res[i].campaign_code : 'なし') + '</td>\
</tr>';
}
書いている途中で 絶対に間違っていると何度も思いましたが、 一応、これでもちゃんと動きます。 ソースを表示するとインデントとか完全に崩れてますが、 一応、これでもちゃんと動きます。
が、流石にこれを完成品としてしまうのは納得できなかったので、別の方法を探したところ、template タグについての記述を見つけました。
上の酷いスクリプトを、まず文字列ベタ書きだった HTML の部分を template タグで書き換えます。
<template id='templateTbody'>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</template>
上記の物を適当な場所(head か body の中ならどこでもいいそうです)に置きます。
次に、for 文の中身をこのテンプレートを用いた形に書き換えます。 ついでに、原始的な for 文を for-of の形に書き直しましょう。
for (let result of res) {
let tbodyTemplate = document.querySelector('#templateTBody');
let tds = tbodyTemplate.content.querySelector('td');
tds[0].innerHTML = (result.member_id ? result.member_id : '-');
tds[1].innerHTML = result.nick_name;
tds[2].innerHTML = result.mail;
tds[3].innerHTML = convertDelivertStatus(result.delivery_status);
tds[4].innerHTML = (result.campaign_code ? result.campaign_code : 'なし');
let tbodyElems = document.importNode(tbodyTemplate.content, true);
document.querySelector('#destinationTable tbody').appendChild(tbodyElems);}
コード量としては倍になってしまいましたが、動的要素と静的要素が分離できたので、コードの可用性としては上がったと思います。
見た感じで何となく分かると思うのですが、ちょっとだけ解説します。
let tbodyTemplate = document.querySelector('#templateTBody');
let tds = tbodyTemplate.content.querySelector('td');
template タグの要素は
content という属性を持っており、中身の HTML 要素へは content を使ってアクセスします。
(innerHTML などを使ったアクセスはできません。これが普通のタグとの違いです)
この例では、template タグの中身にある td 要素の配列を取得しています。
そして、各要素に必要な情報を詰めます。
tds[0].innerHTML = (result.member_id ? result.member_id : '-');
tds[1].innerHTML = result.nick_name;
tds[2].innerHTML = result.mail;
tds[3].innerHTML = convertDelivertStatus(result.delivery_status);
tds[4].innerHTML = (result.campaign_code ? result.campaign_code : 'なし');
次に、テンプレートの中身を document に
importNode 関数を使って書き出しています。
importNodeの 第二引数はtemplate タグ内の要素を全て追加するかどうかのフラグです。
第二引数を false にすると、templateタグ の直下にあるタグしか追加されません。
let tbodyElems = document.importNode(tbodyTemplate.content, true);
最後に、この要素を所定の場所に追加します。
(この例では
appendChild ですが、順番などをもっと指定したい場合には insertBefore を使います)
document.querySelector('#destinationTable tbody').appendChild(tbodyElems);}
content と importNode にちょっと面食らいますが、実際に書いてみるとそれ以外には特に難しいことはないと思います。
Ajax 通信(等)のための fetch API
注意)IE と Safari では動きません
fetch とは?
ちょっと前までは、JavaScript の機能を使って Ajax を実装しようとすると
XMLHttpRequest という API を使う必要がありました。
これが
死ぬほど使いづらい API で、当時流行っていた「えいじゃっくす?」を試してみようと思った学生時代の私は絶望しました。
$.ajax で簡潔に書ける jQuery が流行ったことも納得です。
fetch API を使うと、 XMLHttpRequest よりも簡単にリクエストの送信やレスポンスの受信が書けるようになります。
fetch の現状
少し脱線しますが、WIZY では SuerAgentという、 Ajax 用のライブラリを使っています。 jQuery や SuperAgent は外部のライブラリを読み込む必要があるのですが、IE 含め最近のブラウザならば大体は動きます。 一方、fetch はまだまだ仕様策定中であり、実装しているブラウザも Google Chrome や FireFox, Edge だけです。 サービスに使える段階にはまだまだありません。今後に期待しましょう。
Promise オブジェクト
fetch を説明するにあたり、まず fetch が使用している Promise オブジェクト について説明をします。 Promise は JavaScript の悪評の一つである「コールバック地獄」を回避するためのオブジェクトです。
Ajax の様にどこかの API と通信を行って、その結果を基に処理を行うような場合、JavaScript では非同期の処理になります。 これまで、JavaScriptの非同期処理で関数を順番に実行するにはコールバック関数で書く必要がありました。 そのため、APIから値を取得 -> その値を元にさらにAPIから値を取得 -> さらに… とやっていくと、どんどんコールバック関数のネストが深くなります。これが俗に言う「コールバック地獄」です。
// コールバック地獄、引き継いだ人は死ぬ
doSomething(function(result) {
doSomethingElse(result, function(newResult) {
doThirdThing(newResult, function(finalResult) {
console.log('Got the final result: ' + finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);
同じ処理を Promise オブジェクトを使った関数で書き直すとこうなります。 (関数の書き方はここでは割愛します。知りたい方は、最後にある参考のページを参照してください) 所謂メソッドチェインの書き方が出来るので、実行の順序がぱっと見でとても分かりやすくなります。
doSomething().then(function(result) {
return doSomethingElse(result);
}).then(function(newResult) {
return doThirdThing(newResult);
}).then(function(finalResult) {
console.log('Got the final result: ' + finalResult);
}).catch(failureCallback);
fetch もこの書き方で記述出来ます。
Fetch の使い方例
例えば、
/user/profile というエンドポイントに対して GET リクエストを行う場合、
fetch('/user/profile')
でリクエストを投げて結果を受け取ります。 結果を JSON 形式にしてコンソールに出力するには以下のように書きます。
fetch('/user/profile').then(function(response) {
return response.json(); // 結果をJSON形式に
}).then(function(responseJson) {
console.log(responseJson);
})
JSON 以外にも、blob (画像データなど) や text (HTML データなど) についても同様の書き方で書けます。
fetch('/user/profile').then(function(response) {
return response.blob(); // 結果をBlob形式に
}).then(function(responseBlob) {
// img の src 属性に指定するなど
})
fetch('/user/profile').then(function(response) {
return response.text(); // 結果をText形式に
}).then(function(responseText) {
console.log(responseText);
})
また、エンドポイントの後 Object 形式でオプションを記述することができます。
例えば、GET 以外のリクエストメソッドを使う場合もオプションに記述します。
fetch('/user/profile', {
method: 'POST',
body: {
name: "レコチョクマ",
message: "クマーーーー!",
},
}).then(function(response) {
// レスポンスを元に、ちゃんと更新できているかチェック
}).catch(function(e) {
// エラーが発生した場合の処理
})
fetch('/user/profile', {
method: 'PUT',
body: {
name: "レコチョクマ",
message: "クマーーーー!",
},
}).then(function(response) {
// ちゃんと追加できているかチェック
}).catch(function(e) {
// エラーが発生した場合の処理
})
fetch('/user/profile', {
method: 'DELETE',
}).then(function(response) {
// ちゃんと削除できているかチェック
}).catch(function(e) {
// エラーが発生した場合の処理
})
補足
Ajax 通信で POST 等を行う場合には、成功/失敗を HTTP ステータスコードでチェックすると思います。
fetch は内部で例外が発生すると、
catch という場所に強制的に飛ばされるのですが、catch 内で API からのエラーメッセージを書く処理は、ありがちな割には、結構トリッキーな書き方が必要になります。
(jQuery もこの辺りの処理が書きやすかったとは言い難いですが…)
なお、fetch はデフォルトでは Cookie を送信しません。
認証が必要なAPIがあったときでも、jQuery や SuperAgentならば勝手にヘッダに認証情報を付けてリクエストを投げてくれます。
一方、fetchは認証情報を付けるには
credentials オプションで明示的に書く必要があります。
same-origin: 同一のドメインであれば認証情報を送信するinclude: 常に認証情報を送信する
fetch('/user/secretMessage', {
credentials: 'include',
}).then(function(response) {
return response.json(); // 結果をJSON形式に
}).then(function(responseJson) {
console.log(responseJson);
})
他のライブラリに慣れているとハマる可能性があるので気をつけてください。 私はこれで20分ハマりました。
参考
江藤 光
まだまだ気持ちは新人です。