Promiseを活用する
ここまでのセクションで、Fetch APIを使ってAjax通信を行い、サーバーから取得したデータを表示できました。 最後に、Fetch APIの返り値でもあるPromiseを活用してソースコードを整理することで、エラーハンドリングをしっかり行います。
関数の分割
まずは、大きくなりすぎたfetchUserInfo
関数を整理しましょう。
この関数では、Fetch APIを使ったデータの取得・HTML文字列の組み立て・組み立てたHTMLの表示をしています。
そこで、HTML文字列を組み立てるcreateView
関数とHTMLを表示するdisplayView
関数を作り、処理を分割します。
また、後述するエラーハンドリングを行いやすくするため、アプリケーションにエントリーポイントを設けます。
index.js
に新しくmain
関数を作り、main
関数からfetchUserInfo
関数を呼び出すようにします。
function main() {
fetchUserInfo("js-primer-example");
}
function fetchUserInfo(userId) {
fetch(`https://api.github.com/users/${encodeURIComponent(userId)}`)
.then(response => {
if (!response.ok) {
console.error("エラーレスポンス", response);
} else {
return response.json().then(userInfo => {
// HTMLの組み立て
const view = createView(userInfo);
// HTMLの挿入
displayView(view);
});
}
}).catch(error => {
console.error(error);
});
}
function createView(userInfo) {
return escapeHTML`
<h4>${userInfo.name} (@${userInfo.login})</h4>
<img src="${userInfo.avatar_url}" alt="${userInfo.login}" height="100">
<dl>
<dt>Location</dt>
<dd>${userInfo.location}</dd>
<dt>Repositories</dt>
<dd>${userInfo.public_repos}</dd>
</dl>
`;
}
function displayView(view) {
const result = document.getElementById("result");
result.innerHTML = view;
}
ボタンのclickイベントで呼び出す関数もこれまでのfetchUserInfo
関数からmain
関数に変更します。
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>Ajax Example</title>
</head>
<body>
<h2>GitHub User Info</h2>
<input id="userId" type="text" value="js-primer-example" />
<button onclick="main();">Get user info</button>
<div id="result"></div>
<script src="index.js"></script>
</body>
</html>
Promiseのエラーハンドリング
次にfetchUserInfo
関数を変更し、Fetch APIの返り値でもあるPromiseオブジェクトをreturn
します。
この変更によって、fetchUserInfo
関数を呼び出すmain
関数のほうで非同期処理の結果を扱えるようになります。
Promiseチェーンの中で投げられたエラーは、Promiseのcatch
メソッドを使って一箇所で受け取れます。
次のコードでは、fetchUserInfo
関数から返されたPromiseオブジェクトを、main
関数でエラーハンドリングしてログを出力します。
fetchUserInfo
関数のcatch
メソッドでハンドリングしていたエラーは、main
関数のcatch
メソッドでハンドリングされます。
一方、Responseのok
プロパティで判定していた400や500などのエラーレスポンスがそのままではmain
関数でハンドリングできません。
そこで、Promise.reject
メソッドを使ってRejectedなPromiseを返し、Promiseチェーンをエラーの状態にします。
Promiseチェーンがエラーとなるため、main
関数のcatch
でハンドリングできます。
function main() {
fetchUserInfo("js-primer-example")
.catch((error) => {
// Promiseチェーンの中で発生したエラーを受け取る
console.error(`エラーが発生しました (${error})`);
});
}
function fetchUserInfo(userId) {
// fetchの返り値のPromiseをreturnする
return fetch(`https://api.github.com/users/${encodeURIComponent(userId)}`)
.then(response => {
if (!response.ok) {
// エラーレスポンスからRejectedなPromiseを作成して返す
return Promise.reject(new Error(`${response.status}: ${response.statusText}`));
} else {
return response.json().then(userInfo => {
// HTMLの組み立て
const view = createView(userInfo);
// HTMLの挿入
displayView(view);
});
}
})
.catch(err => {
return Promise.reject(new Error(`Failed fetch user(id: ${userId}) info`, { cause: err }));
});
}
Promiseチェーンのリファクタリング
現在のfetchUserInfo
関数はデータの取得に加えて、HTMLの組み立て(createView
)と表示(displayView
)も行っています。
fetchUserInfo
関数に処理が集中して見通しが悪いため、fetchUserInfo
関数はデータの取得だけを行うように変更します。
併せてmain
関数で、データの取得(fetchUserInfo
)、HTMLの組み立て(createView
)と表示(displayView
)という一連の流れをPromiseチェーンで行うように変更していきます。
Promiseのthen
メソッドでつながるPromiseチェーンは、then
に渡されたコールバック関数の返り値をそのまま次のthen
へ渡します。
ただし、コールバック関数の返り値がPromiseである場合は、そのPromiseで解決された値を次のthen
に渡します。
つまり、then
のコールバック関数が同期処理から非同期処理に変わったとしても、次のthen
が受け取る値の型は変わらないということです。
Promiseチェーンを使って処理を分割する利点は、同期処理と非同期処理を区別せずに連鎖できることです。
一般に、同期的に書かれた処理を後から非同期処理へと変更するのは、全体を書き換える必要があるため難しいです。
そのため、最初から処理を分けておき、処理をthen
を使ってつなぐことで、変更に強いコードを書けます。
どのように処理を区切るかは、それぞれの関数が受け取る値の型と、返す値の型に注目するのがよいでしょう。
Promiseチェーンで処理を分けることで、それぞれの処理が簡潔になりコードの見通しがよくなります。
index.js
のfetchUserInfo
関数とmain
関数を次のように書き換えます。
まず、fetchUserInfo
関数がResponseのjson
メソッドの返り値をそのまま返すように変更します。
Responseのjson
メソッドの返り値はJSONオブジェクトで解決されるPromiseなので、次のthen
ではユーザー情報のJSONオブジェクトが渡されます。
次に、main
関数がfetchUserInfo
関数のPromiseチェーンで、HTMLの組み立て(createView
)と表示(displayView
)を行うように変更します。
function main() {
fetchUserInfo("js-primer-example")
// ここではJSONオブジェクトで解決されるPromise
.then((userInfo) => createView(userInfo))
// ここではHTML文字列で解決されるPromise
.then((view) => displayView(view))
// Promiseチェーンでエラーがあった場合はキャッチされる
.catch((error) => {
console.error(`エラーが発生しました (${error})`);
});
}
function fetchUserInfo(userId) {
return fetch(`https://api.github.com/users/${encodeURIComponent(userId)}`)
.then(response => {
if (!response.ok) {
return Promise.reject(new Error(`${response.status}: ${response.statusText}`));
} else {
// JSONオブジェクトで解決されるPromiseを返す
return response.json();
}
})
.catch(err => {
return Promise.reject(new Error(`Failed fetch user(id: ${userId}) info`, { cause: err }));
});
}
Async Functionへの置き換え
Promiseチェーンによって、Promiseの非同期処理と同じ見た目で同期処理を記述できるようになりました。
さらにAsync Functionを使うと、同期処理と同じ見た目でPromiseの非同期処理を記述できるようになります。
Promiseのthen
メソッドによるコールバック関数の入れ子がなくなり、手続き的で可読性が高いコードになります。
また、エラーハンドリングも同期処理と同じくtry...catch
構文を使うことができます。
main
関数を次のように書き換えましょう。まず関数宣言の前にasync
をつけてAsync Functionにしています。
次にfetchUserInfo
関数の呼び出しにawait
をつけます。
これによりPromiseに解決されたJSONオブジェクトをuserInfo
変数に代入できます。
もしfetchUserInfo
関数の中で例外が投げられた場合は、try...catch
構文でエラーハンドリングできます。
このように、あらかじめ非同期処理の関数がPromiseを返すようにしておくと、Async Functionにリファクタリングしやすくなります。
async function main() {
try {
const userInfo = await fetchUserInfo("js-primer-example");
const view = createView(userInfo);
displayView(view);
} catch (error) {
console.error(`エラーが発生しました (${error})`);
}
}
ユーザーIDを変更できるようにする
仕上げとして、今までjs-primer-example
で固定としていたユーザーIDを変更できるようにしましょう。
index.htmlに<input>
タグを追加し、JavaScriptから値を取得するためにuserId
というIDを付与しておきます。
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>Ajax Example</title>
</head>
<body>
<h2>GitHub User Info</h2>
<input id="userId" type="text" value="js-primer-example" />
<button onclick="main();">Get user info</button>
<div id="result"></div>
<script src="index.js"></script>
</body>
</html>
index.jsにも<input>
タグから値を受け取るための処理を追加すると、最終的に次のようになります。
index.js
async function main() {
try {
const userId = getUserId();
const userInfo = await fetchUserInfo(userId);
const view = createView(userInfo);
displayView(view);
} catch (error) {
console.error(`エラーが発生しました (${error})`);
}
}
function fetchUserInfo(userId) {
return fetch(`https://api.github.com/users/${encodeURIComponent(userId)}`)
.then(response => {
if (!response.ok) {
return Promise.reject(new Error(`${response.status}: ${response.statusText}`));
} else {
return response.json();
}
});
}
function getUserId() {
return document.getElementById("userId").value;
}
function createView(userInfo) {
return escapeHTML`
<h4>${userInfo.name} (@${userInfo.login})</h4>
<img src="${userInfo.avatar_url}" alt="${userInfo.login}" height="100">
<dl>
<dt>Location</dt>
<dd>${userInfo.location}</dd>
<dt>Repositories</dt>
<dd>${userInfo.public_repos}</dd>
</dl>
`;
}
function displayView(view) {
const result = document.getElementById("result");
result.innerHTML = view;
}
function escapeSpecialChars(str) {
return str
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function escapeHTML(strings, ...values) {
return strings.reduce((result, str, i) => {
const value = values[i - 1];
if (typeof value === "string") {
return result + escapeSpecialChars(value) + str;
} else {
return result + String(value) + str;
}
});
}
アプリケーションを実行すると、次のようになります。 要件を満たすことができたので、このアプリケーションはこれで完成です。
このセクションのチェックリスト
- HTMLの組み立てと表示の処理を
createView
関数とdisplayView
関数に分離した main
関数を宣言し、fetchUserInfo
関数が返すPromiseのエラーハンドリングを行った- Promiseチェーンを使って
fetchUserInfo
関数をリファクタリングした - Async Function を使って
main
関数をリファクタリングした index.html
に<input>
タグを追加し、getUserId
関数でユーザーIDを取得した
この章で作成したアプリは次のURLで確認できます。