僕が適当に考えたAngularの連打防止制御

超お久しぶりです。 たーせるです。

どういうわけか疲労極まっており、いろいろ端折っております。 それでもなんとか書き留めておきたいので、ここに残しておくことにします。

ナイスな導入

今日は、Angular で Submit ボタンを連打されないようにするにはどうすればよいか考えてみたいと思います。

……というのが、基本的な思想になります。

本日の材料

材料は以下の通りです。

それではさっそくパッケージを導入しましょう。

パッケージのインストール

ターミナルで、package.json があるフォルダに $cd して、以下のコマンドを入力します。

$npm i safe-json-stringify @types/safe-json-stringify xxhash-wasm

残念ながら xxhash-wasm には TypeScript の型定義が無いので、ライブラリ本体だけを入れることになります。

もう一つ残念なのが、xxhash-wasm のハッシュ化メソッドは非同期処理であるということ。

これをそのまま HttpInterceptor の中で使おうとすると、軽くはまります

ソースファイルの作成

とりあえず、以下のが付いているフォルダとファイルを作りましょう。

  • <project root>
    • node-modules
    • src
      • app
        • http-interceptors
          • index.ts
          • single-access-interceptor.ts
          • single-access-management.service.ts
        • app.component.ts | html | css | spec.ts
        • app.module.ts
        • app-routing.module.ts
      • main.ts
      • index.html

single-access-management.service.ts
import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class SingleAccessManagementService {
  private readonly requests = new Set<string>();

  add(hash: string) {
    if (this.requests.has(hash)) { return false; }
    this.requests.add(hash);
    return true;
  }

  delete(hash: string) {
    if (!this.requests.has(hash)) { return false; }
    this.requests.delete(hash);
    return true;
  }
}

single-access-management.service.ts は、中に Set<string> を隠し持ったシングルトンクラスです。 要素を新規追加する add() と、削除する delete() メソッドを持っています。

もし、追加しようとしている要素が既に存在する場合、add()メソッドは false (= 追加失敗)を返します。これは、この後登場する single-access-interceptor.ts の実装をラクにするための工夫です。

single-access-interceptor.ts
import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
import { Observable, Subject, EMPTY, from } from 'rxjs';
import { flatMap, finalize } from 'rxjs/operators';
import * as stringify from 'safe-json-stringify';
import xxhash from 'xxhash-wasm';
import { SingleAccessManagementService } from './single-access-management.service';

@Injectable()
export class SingleAccessInterceptor implements HttpInterceptor {
  constructor(private manager: SingleAccessManagementService) { }

  private getRequestHash(req: HttpRequest<any>) {
    const ret = new Subject<string>();
    from(xxhash()).subscribe((hasher: any) => ret.next(hasher.h64(stringify(req))));
    return ret.asObservable();
  }

  intercept(req: HttpRequest<any>, next: HttpHandler) {
    return this.getRequestHash(req).pipe(
      flatMap(hash => this.manager.add(hash) ?
        next.handle(req).pipe(finalize(() => this.manager.delete(hash))) : EMPTY));
  }
}

ここがポイントです。

何を言っているかわからないと思いますが、解説する余力がないのでこの話はまたいつか……。

index.ts
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { SingleAccessInterceptor } from './single-access-interceptor';

export const httpInterceptorProviders = [
  { provide: HTTP_INTERCEPTORS, useClass: SingleAccessInterceptor, multi: true }
];

最後にバレルを作り、これを app.module.ts に登録します。

app.module.ts
import { AppComponent } from './app.component';
import { httpInterceptorProviders } from './http-interceptors';
/* 略 */

@NgModule({
  declarations: [ /* 略 */ ],
  imports: [ /* 略 */ ],
  providers: [ httpInterceptorProviders ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

これで、とりあえず最低限の連打防止制御ができるようになりました。めでたし。

まとめ

こういうこと。

Copyright (c) 2012 @tercel_s, @iTercel, @pi_cro_s.