概要
「サーバーのAPIを叩くHTTPクライアントでも書くか」
(※Angularなのでfunctionが要らない・TypeScriptなので型を指定できることに注意)
// Angularを知らない人向けの説明:
// AngularにはHttpClientクラスがあり、それを使うことでGETなどのHTTPリクエストが実行できる。
// また、コンストラクタの引数にするだけでDI(依存性の注入)できる
import { HttpClient } from '@angular/common/http';
constructor(private http: HttpClient) {}
private async getHoge(): Promise<string> {
return await this.http.get<string>(url).toPromise();
}
「でもこれじゃ2回目以降のgetHoge()でもHTTPリクエストしてしまう……キャッシュ化しよう」
(※GETリクエストが冪等なのはRESTの常識なので)
// 簡単なキャッシュ
cache: {[key: string]: string} = {};
private async getHoge(): Promise<string> {
// キャッシュにあればそちらを返す
if ("getHoge" in this.cache){
return this.cache["getHoge"];
}
// キャッシュに無いのでHTTPリクエストし、その結果をキャッシュする
const result = await this.http.get<string>(url).toPromise();
this.cache["getHoge"] = result;
return result;
}
「……あれ、キャッシュしたはずなのに何度もHTTPリクエストしてしまうぞ!?」
原因
上記処理はasync/awaitを用いた非同期処理です。
このことから、getHoge()の1回目の処理(当然キャッシュ化されていない)の途中で別のgetHoge()が走ると、「その段階ではキャッシュにデータが無い」ことから、そちらについても律儀にHTTPリクエストしてしまいます。
具体的には、「Angularで複数配置した同コンポーネントが、配置時に一斉にリクエストする場合」などにこういったミスが起きてしまいます。
対策
セマフォでHTTPリクエストを待たせます。
本質的にJavaScriptの処理はシングルスレッドであり、非同期処理によって別スレッドと並行処理しているように見えるだけです。なので変数に値を入れる際も、変数に対してロックだのなんだのと考える必要はありません。
// 簡単なキャッシュ
cache: {[key: string]: string} = {};
// セマフォ
semaphore: boolean = false;
private async getHoge(): Promise<string> {
// キャッシュにあればそちらを返す
if ("getHoge" in this.cache){
return this.cache["getHoge"];
}
// セマフォが立っている際は通信中なので、読み込みを待機する
if (this.semaphore) {
// 適当な時間(今回は100ミリ秒)待機する
await new Promise(resolve => setTimeout(resolve, 100));
// 再帰的に処理を呼び出す
return this.getHoge();
}
// キャッシュに無いのでHTTPリクエストし、その結果をキャッシュする
this.semaphore = true;
const result = await this.http.get<string>(url).toPromise();
this.cache["getHoge"] = result;
this.semaphore = false;
return result;
}