中安拓也のブログ

プログラミングについて書くブログ

【JavaScript】window.open()で開かれるウィンドウがタブではなく新規ウィンドウで開かれるようにしたい(IE11)

はじめに

アプリでリンク先の画像を開くとタブで開かれてしまうから、プログラム(JavaScript)で制御して常に新しいウィンドウで開かれるようにしてほしいとの依頼があったので、やり方を調べた

なお、リンク先の画像を開く処理はJavaScriptのwindow.openを利用している

環境

  • ブラウザはInternet Explorer 11(11.590.17134.0)
  • アプリはBtoBのwebアプリケーション
  • OSはWindows10

調査

ネットサーフィンした

window.open - Web API | MDN

  • 上記のMDNのドキュメントを読んだが新しいウィンドウを開く際に、タブで開くかウィンドウで開くかを設定するオプションはなさそう

どのようにリンクを開くかは、常に、完全にユーザの管理下にあるべきです

ブラウザの表示方法についてはユーザー自身が設定するべきであって、開発者が強制するべきではないみたいな思想を感じる

javascript - window.open opening new tab instead of new window in IE 11 - Stack Overflow

  • ブラウザの方で設定するしかないんじゃない?みたいな回答

javascriptのwindow.open()で別窓が開かれない|teratail

  • window.open()のオプション引数であるwindowFeaturestoolbarなりmenubarの設定を工夫すれば新規タブではなく新規ウィンドウで開いたとのこと
    • windowFeaturesに開かれるウィンドウをタブかウィンドウか直接制御するプロパティがない以上、上記サイトで紹介されている方法が不安定なものである(ブラウザの設定、マイナーバージョンによって左右されうる)認識

調査結果

JavaScript(window.open())で制御するのではなく、ユーザ(クライアント)にブラウザの設定を変更していただいたほうが確実ではないか

ブラウザ設定方法

  1. IE11の設定ボタンをクリック

  2. インターネットオプションを選択

  3. 全般タブを選択

  4. [タブ]ボタンをクリック

  5. ポップアップの発生時:で「常に新しいウィンドウでポップアップを開く」を選択

上記の設定でwindow.open()で開かれるウィンドウがタブではなく新規のウィンドウになる

Angular公式ドキュメント(翻訳されている部分は)全部読む

はじめに

日本語翻訳していただいた全ての人々に感謝.......

angular.jp

バージョン

v.7.2.0時点のドキュメント

学習メモ

勉強になった点や単なる感想についてまとめました。

今までは、共通のHTTPサービスを作成することによってスピナーの表示とかHTTPヘッダーの登録を行なっていたんですが、Angularの非同期バリデーションやインターセプターを利用する方法でも同じことができることを知ったのが一番の収穫だったと思います。

ライフサイクル

  • constructor()の後にngOnInit()が呼ばれる
  • Angular公式チームとしてはDIとローカル変数の初期化(つまり単純な処理)以外はngOnInit()でやってほしいとのこと
  • コストが高い(呼び出し回数の多い)ライフサイクルメソッドの処理に注意...!
    • ngOnChanges()とか...特にngDoCheck()はかなりコストが高いので軽い処理しかやらせないこと

パイプ

  • パイプには純粋パイプと不純パイプがあって、純粋パイプは値が変更された時にしか呼び出されない(オブジェクトの変更は検知できない)のに対して、不純パイプはコンポーネントの変更検知サイクル(ユーザのマウス移動など)の度に毎回呼び出される
  • 不純パイプは純粋パイプと違ってオブジェクトに対して使える代わりに呼び出されまくるのでコストが高い。純粋パイプと不純パイプはngOnChanges()に対するngDoCheck()みたいな関係だと思っておけばよし
  • asyncパイプは不純なパイプなので扱いに注意
    • 以前、全 stateの値をObservable型で管理していたことがあってほぼ全ての項目にasyncパイプを使ってたら処理が激重になったことがあったけどasyncパイプの使い方に問題があったのかもしれない
  • asyncパイプはObservable型だけじゃなくてPromiseにも実は使えるぞ

フォーム

  • 非同期バリデーション(AsyncValidator)というものがあり、通常のバリデーションが完了したタイミングで呼び出される。スピナーの表示とかに利用するのが一般的とのこと
  • formの状態をリセットできるreset()メソッドが便利そう

HTTPClient

  • RxJSにretry()メソッドというものがあるから、HTTPリクエストのリトライ処理が簡単に書けるとのこと
  • インターセプター(HttpInterceptor)というリクエスト直前とレスポンス直後に呼び出される仕組みがあって、共通ヘッダーの設定などに使える
  • ファイルのアップロードなどの重い処理で、進捗状況を表示できるようにする処理があるとのこと

Service WorkerとPWA

  • Service WorkerもPWAも初めて知った。ページをキャッシュしてくれるからパフォーマンス上がるのね〜〜なるなる
  • 素振りしてから本番投入してみたい

バージョニング

https://angular.jp/guide/releases#angularのバージョニング

メジャーバージョンを別のメジャーバージョンに更新する場合は、メジャーバージョンをスキップしないことをお勧めします。手順にしたがって、次のメジャーバージョンに段階的に更新し、各ステップでテストし、検証します。たとえば、バージョン4.x.xからバージョン6.x.xに更新する場合は、まず最新の5.x.xリリースに更新することをお勧めします。5.x.xに正常に更新した後には、6.x.xに更新できます

ほえ〜〜そうなん?めんどいから一気に上げてた....

チートシート

  • ngSwitchって初めて知った。ngIfElse書きまくるよりも綺麗にまとまりそう

あとがき

最近Vue.jsの公式ドキュメントを基礎部分だけ読んだんですけど、Angularのそれよりも圧倒的にボリュームが少なかったのでここら辺がAngularは学習コストが高いと言われるゆえんなのかな...と感じました。基礎部分で学習することが多いのが悪いことなのかは知りませんが

【Angular】シャイニングブルートウキョウでは、プログラムがあなたを条件分岐する!!

f:id:l08084:20181224205343p:plain

はじめに

Vue.jsの公式ドキュメントに、質問するとユーザーの代わりにYES or NOの意思決定を行ってくれるWebAPI(YesNo.WTF)が紹介されていたのをきっかけに、AngularでYesNo.WTFを呼び出して結果を表示するアプリを作ってみました。

タイトルはお馴染みのロシア的倒置法をもじったもので、シャイニングブルーというのは2020東京オリンピックのボランティア名の候補の一つだそうですよ。

引用元の記事

算出プロパティとウォッチャ — Vue.js

今回の記事では、上記のVue.jsのサンプルコードをAngularに書き換えてみます。

試しに触ってみる

完成版をGitHub Pagesに上げているので、実際に触って遊ぶこともできます!

YES or NO形式の質問を入力して500ミリ秒待つと質問に対する適当な回答(YES or NO)と愉快なGIF画像が表示されるぞ!

完成版のデモのリンク(https://l08084.github.io/ng-yes-or-no/)

全体のコードに興味がある人はGitHubに掲載されている下記コードを参照のこと

github.com

実装

バージョン情報

JavaScriptのフレームワークとしてAngularを、CSSのフレームワークとしてAngular Materialを使いました。

  • angular@7.0.7
  • angular/material@7.2.0
  • angular/cli@7.0.7
  • rxjs@6.3.3

質問フォーム作成

まず(YesNo.WTF)に投げかける質問を入力する質問フォームを作成します。

質問フォームのコンポーネントクラス

Angularのモデル駆動型フォーム(Reactive駆動型)を使用していて、全角か半角のを文末に入力しないといけないバリデーションもかけています。

質問フォームのテンプレート

// 入力フォームの値が変更された場合、500ミリ秒間隔で値を取得する
this.questionForm.get('question').valueChanges.pipe(debounceTime(500))
    .subscribe((searchText) => {

Vueのサンプルコードでは、lodash_.debounce(func, [wait=0], [options={}])を使用してyesno.wtf/apiへのリクエスト回数を制限していましたが、本コードではAngularらしく(?)RxJSdebounceTimeを利用してAPIへのアクセスを500ミリ秒間隔に制限しています。

あと全然知らなかったんですが、RxJS 6系だとpipe()メソッドを使わないとオペレーター(この例だとdebounceTime)使えないんですね......

回答を表示するコンポーネントを作成する

続いて、APIの回答結果(YES or NOとGIF画像)を表示するコンポーネントを作成していきます。

回答を表示するコンポーネントクラス

回答を表示するコンポーネントのテンプレート

作っててGIF画像って<img>タグで表示するんだ〜って思いました。まあそりゃそうでしょって感じですが

動作確認

実際に質問を入力してみましょう。ちゃんと動いていますね

f:id:l08084:20181224194508p:plain
実際に質問している様子

あとがき

サンプルコードの写経以上、クソアプリ未満といった感じでした。明日からはもうちょいがんばろう。ね、ハム太郎

【Angular】 ボタンの連打を防止するディレクティブを作る(連続クリック・二重送信の禁止)

はじめに

アプリケーションを作る上で避けられない、ボタンの連続クリック防止機能を今回は作成します。この機能がないと同じメールが2通飛んだりデータベースにレコードが重複してINSERTされてしまうかも......

バージョン情報

JavaScriptのフレームワークとしてAngularを使用しています

  • Angular@7.0.6
  • typescript@3.1.6
  • webpack@4.19.1

実装

今回のテーマである連続クリックの防止ですが、ディレクティブを使用して実現します。
AngularのディレクティブにはNgForとNgIfに代表される構造ディレクティブとテンプレート要素の属性を変更する属性ディレクティブがあり、今回作成するのは後者の属性ディレクティブになります。

ボタンの連打を防止するAngularのディレクティブ

このディレクティブの機能としては、HTTP通信(非同期処理)が発生するボタンでの使用を想定していて、押下したときにHTTPの通信が終わるまで、ボタンを連打されないように非活性化するといったものになっています。

今回作成したディレクティブの処理の流れ

  1. ElementRefを通してボタンのHtmlElementを取得する
  2. @HostListener('click')でボタンがクリックされたイベントを検知し、ボタンを非活性化(disabled)する
  3. HTTP通信が終了したことを@Inputデコレーターを通して検知したら、ボタンの非活性化を解除する

HTMLからディレクティブを呼びだす

引数を受け取るタイプのディレクティブなので、このようにHTMLテンプレート上で引数を渡してあげる必要があります

今回作成したディレクティブの呼び出し例

終わりに

スピナー(くるくる回るやつね)使えばよくねえか???と思った人もいるかもしれませんが、デザイン的にスピナーを使えない事情があったのでボタンを非活性にする方法でやってみました。
あと0.5秒とか1.0秒とか追加で非活性の時間のばしてるけど(エクセルを信頼せずに電卓で計算をやり直す側の人間なので)、完全に無駄な処理だったかもしれない。せっかくHTTP通信中のフラグをInputで渡しているわけだしそのフラグのON・OFFだけでボタンの非活性・活性を制御しても良かった気がしています

【Angular】複数項目にまたがるカスタムバリデーションを作る(パスワード・確認用パスワードなど)

f:id:l08084:20181125171015p:plain
パスワード・確認用パスワード入力フォームの例

はじめに

アカウント登録画面を作っていて出現頻度が著しく高い項目といえば............そう!パスワードの入力フォームですね。というわけで今回は、パスワードと確認用パスワードが一致しているか確認するバリデーションを実装していきます。

そもそもAngularでカスタムバリデーションをどう作っていいかわからねーよという人は下記の記事を!

【JavaScript】文字列の間に空白(スペース)が入力されているか確認する(氏名などのバリデーション) - 中安拓也のブログ

Angular Materialのインストール方法と使い方については、下記の記事を参照してください。

Angular Material の Tableを使う - 中安拓也のブログ

Angular Materialでログインフォームを作る - 中安拓也のブログ

バリデーション内容

パスワードについてのバリデーションを設定します。チェックする項目は下記とします。

  • パスワード欄と確認用パスワード欄で入力された内容が一致する

バージョン情報

JavaScriptのフレームワークとしてはAngularを、CSSのフレームワークとしてはAngular Materialを使用しています。

  • Angular@7.0.6
  • typescript@3.1.6
  • webpack@4.19.1
  • Angular Material@7.0.4

実装

パスワード入力フォーム作成

前回の記事で作成した入力フォームにコードを足していく感じで実装していきます。
コードが多くなってきて前回からの更新部分がわかりづらいですが、<!-- add this! -->とコメントされている行が追加された部分になります。

f:id:l08084:20181124201537p:plain
前回の記事で作成した入力フォーム

まずテンプレートに、パスワード入力フォームと確認用パスワードの入力フォーム、そしてパスワードが表示しないときに表示するエラーメッセージを表示します。

app.component.html

<div class="container">
  <mat-card class="login-card">
    <mat-card-header>
      <mat-card-title class="login-title">アカウント登録</mat-card-title>
    </mat-card-header>
    <mat-card-content>
      <form [formGroup]="nameRegisterForm" (ngSubmit)="onSubmit()" class="login-form">
        <!-- 氏名の入力フォーム -->
        <mat-form-field class="input-field">
          <input matInput placeholder="氏名(姓と名の間にスペースを入力してください)"
            id="name" name="name" [formControl]="nameRegisterForm.controls.name" required>
          <!-- 必須入力のエラーメッセージ -->
          <mat-error *ngIf="nameRegisterForm.controls.name.errors?.required">氏名は必須項目です</mat-error>
          <!-- スペースがない場合のエラーメッセージ -->
          <mat-error *ngIf="nameRegisterForm.controls.name.errors?.haveBlank">姓と名の間にはスペースを入力してください</mat-error>
        </mat-form-field>
        <!-- add this! -->
        <!-- パスワードの入力フォーム -->
        <mat-form-field class="input-field">
          <input [type]="hide ? 'password' : 'text'" matInput
            placeholder="パスワード" id="password" name="password" [formControl]="nameRegisterForm.controls.password" required>
          <mat-icon matSuffix (click)="hide = !hide">{{hide ? 'visibility' : 'visibility_off'}}</mat-icon>
          <mat-error *ngIf="nameRegisterForm.controls.password.errors?.required">パスワードは必須項目です</mat-error>
        </mat-form-field>
        <!-- 確認用パスワードの入力フォーム -->
        <mat-form-field class="input-field">
          <input [type]="hide ? 'confirmPassword' : 'text'" matInput
            placeholder="確認用パスワード" id="confirmPassword" name="confirmPassword" [formControl]="nameRegisterForm.controls.confirmPassword" required>
          <mat-icon matSuffix (click)="hide = !hide">{{hide ? 'visibility' : 'visibility_off'}}</mat-icon>
          <mat-error *ngIf="nameRegisterForm.controls.confirmPassword.errors?.required">確認用パスワードは必須項目です</mat-error>
          <!-- add this! -->
          <!-- パスワードが一致しないときに表示するエラー -->
          <mat-error *ngIf="nameRegisterForm.controls.confirmPassword.errors?.notMatchPassword">パスワードが一致しません</mat-error>
        </mat-form-field>
        <!-- 登録ボタン -->
        <button type="submit" class="register-button"
          mat-raised-button color="primary" [disabled]="nameRegisterForm.invalid">登録</button>
      </form>
    </mat-card-content>
  </mat-card>
</div>

コンポーネントクラスにもパスワード入力フォームに関するコードを追加します。(テンプレートの時と同じく追加部分には// add this!とコメントしています)

app.component.ts

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms';
import { CustomValidator } from './custom-validator';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {

  public nameRegisterForm: FormGroup;
  public nameControl: FormControl;
  // add this!
  public passwordControl: FormControl;
  public confirmPasswordControl: FormControl;
  public hide = true;

  constructor(private builder: FormBuilder) {
    this.createForm();
  }

  public ngOnInit(): void {
    this.nameControl = this.nameRegisterForm.get('email') as FormControl;
    // add this!
    this.passwordControl = this.nameRegisterForm.get('password') as FormControl;
    this.confirmPasswordControl = this.nameRegisterForm.get('confirmPassword') as FormControl;
  }

  public onSubmit() {
    console.log();
  }

  /**
   * フォームグループの初期化を実行する
   *
   */
  private createForm() {
    // 氏名欄のバリデーションを設定している
    this.nameRegisterForm = this.builder.group({
      name: ['', [Validators.required, CustomValidator.haveBlank]],
      // add this!
      password: ['', [Validators.required]],
      confirmPassword: ['', [Validators.required]]
    }, {
      validator: CustomValidator.matchPassword
    });
  }
}

今回追加するのは複数項目に対するバリデーションなので、他のバリデーションと違ってvalidator: CustomValidator.matchPasswordという風にフォーム全体にバリデーションを定義しています。

最後にパスワード入力フォームに入力された内容と確認用パスワードに入力された内容が一致していることを確認するカスタムバリデーションmatchPasswordメソッドを追加します。

custom-validator.ts

import { ValidationErrors, FormControl, AbstractControl } from '@angular/forms';

export class CustomValidator {

  // 姓と名の間にスペースが入っているか確認する
  static haveBlank(control: FormControl): ValidationErrors | null {
    const value = (control.value || '') + '';
    // 両端のスペースを取り除く
    const name = value.trim();
    // 半角スペース
    const NAME_COLUMN_SPRIT_VALUE = ' ';
    // 全角スペース
    const NAME_COLUMN_SPRIT_VALUE_W = ' ';
    let isError = false;
    if (name.indexOf(NAME_COLUMN_SPRIT_VALUE) < 0
    && name.indexOf(NAME_COLUMN_SPRIT_VALUE_W) < 0) {
        // 半角スペースも全角スペースも含まれていない場合
        isError = true;
    }
    // 姓と名の間にスペースが入力されていない場合は、バリデーションエラーを返す
    return isError ? { haveBlank: true } : null;
  }

  // add this!
  // パスワードと確認用パスワードが一致するかチェック
  static matchPassword(ac: AbstractControl) {
    const password = ac.get('password').value;
    const passwordConfirm = ac.get('confirmPassword').value;
    if (password !== passwordConfirm) {
      ac.get('confirmPassword').setErrors({ notMatchPassword: true });
    }
  }
}

単項目に対するバリデーションであるhaveBlankには引数としてFormControlを渡していますが、複数項目に対するバリデーションであるmatchPasswordにはAbstractControlを渡しています。

動作確認

書いたコードを動かしてみます

f:id:l08084:20181128091521p:plain
初期表示

一致しないパスワードを入力するとエラーが表示されます。

f:id:l08084:20181128091844p:plain
パスワード不一致のエラーメッセージが表示されている

一致するパスワードを入力するとエラーが表示されなくなるので、バリデーションが正しく機能していることがわかります。

f:id:l08084:20181128092002p:plain

【JavaScript】文字列の間に空白(スペース)が入力されているか確認する(氏名などのバリデーション)

はじめに

アカウント登録画面でよく見る姓と名の入力欄について、フォームが二つに別れているパターンと、フォームが一つになっていてスペースで姓と名を区切るパターンの二つがあると思います。

今回は後者を採用した場合に必要になるバリデーションについて書きます(なぜなら今作ってるシステムがそうなので)。

とはいえ、他のWebサービスを見ると基本的に姓と名でフォームを分けてる場合がほとんどですね。なんでフォームを一つにしちゃったんだろう。。。デザインの問題?画面が縦に長すぎるとまずいとか

f:id:l08084:20181118142046p:plain
姓と名が別れている入力フォームの例

バリデーション内容

氏名についてのバリデーションを設定します。チェックする項目は下記とします。

  • 氏名に少なくとも1つのスペース(半角もしくは全角)が入力されていること
  • ただし、スペースが文頭または文末に入力されている場合は、未入力の扱いとする
  • 複数スペースが入力された場合は、最初のスペースを姓名の区切りとして利用する

バージョンなどの情報

JavaScriptのフレームワークとしてはAngularを使用していますが、他フレームワークや生JavaScriptでもバリデーションのロジック部分は変わらないと思います。

  • Angular@7.0.6
  • typescript@3.1.6
  • webpack@4.19.1
  • Angular Material@7.0.4

実装

CSSフレームワークとして、Angular Material を使用しています。Angular Material自体のインストール方法や使い方は下記の記事を参照してください。

Angular Material の Tableを使う - 中安拓也のブログ

Angular Materialでログインフォームを作る - 中安拓也のブログ

Angularのバリデーション機能は、テンプレート(HTML)に検証ルールを記述するテンプレート駆動型と、コンポーネント側に検証ルールを記述するモデル駆動型に分かれていますが、本記事ではモデル駆動型の方を使用しています。

まず姓と名の間にスペースが入力されているかを判定するカスタムバリデーションを作成します。

  • custom-validator.ts
import { ValidationErrors, FormControl } from '@angular/forms';

export class CustomValidator {

  static haveBlank(control: FormControl): ValidationErrors | null {
    const value = (control.value || '') + '';
    // 両端のスペースを取り除く
    const name = value.trim();
    // 半角スペース
    const NAME_COLUMN_SPRIT_VALUE = ' ';
    // 全角スペース
    const NAME_COLUMN_SPRIT_VALUE_W = ' ';
    let isError = false;
    if (name.indexOf(NAME_COLUMN_SPRIT_VALUE) < 0
    && name.indexOf(NAME_COLUMN_SPRIT_VALUE_W) < 0) {
        // 半角スペースも全角スペースも含まれていない場合
        isError = true;
    }
    // 姓と名の間にスペースが入力されていない場合は、バリデーションエラーを返す
    return isError ? { haveBlank: true } : null;
  }
}

実装内容としては、文字列の両端のスペースを取り除いた後に、半角または全角のスペースが入力されているかどうかをindexOf()メソッドを使用してチェックしています。(indexOf()メソッドの代わりにincludes()メソッドを使ってもOKです)

続いて、作成したカスタムバリデーションをフォームの方に設定してあげると、実装が完了します。

  • app.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms';
import { CustomValidator } from './custom-validator';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {

  public nameRegisterForm: FormGroup;
  public nameControl: FormControl;

  constructor(private builder: FormBuilder) {
    this.createForm();
  }

  public ngOnInit(): void {
    this.nameControl = this.nameRegisterForm.get('email') as FormControl;
  }

  public onSubmit() {
    console.log();
  }

  /**
   * フォームグループの初期化を実行する
   *
   * @private
   * @memberof AppComponent
   */
  private createForm() {
    // 氏名欄のバリデーションを設定している
    this.nameRegisterForm = this.builder.group({
      name: ['', [Validators.required, CustomValidator.haveBlank]]
    });
  }
}
  • app.component.html
<div class="container">
  <mat-card class="login-card">
    <mat-card-header>
      <mat-card-title class="login-title">アカウント登録</mat-card-title>
    </mat-card-header>
    <mat-card-content>
      <form [formGroup]="nameRegisterForm" (ngSubmit)="onSubmit()" class="login-form">
        <mat-form-field class="input-field">
          <!-- 氏名の入力フォーム -->
          <input matInput placeholder="氏名(姓と名の間にスペースを入力してください)"
            id="name" name="name" [formControl]="nameRegisterForm.controls.name" required>
          <!-- 必須入力のエラーメッセージ -->
          <mat-error *ngIf="nameRegisterForm.controls.name.errors?.required">氏名は必須入力です</mat-error>
          <!-- スペースがない場合のエラーメッセージ -->
          <mat-error *ngIf="nameRegisterForm.controls.name.errors?.haveBlank">姓と名の間にはスペースを入力してください</mat-error>
        </mat-form-field>
        <!-- 登録ボタン -->
        <button type="submit" class="register-button"
          mat-raised-button color="primary" [disabled]="nameRegisterForm.invalid">登録</button>
      </form>
    </mat-card-content>
  </mat-card>
</div>

スペースが入力されなかった場合のエラーメッセージについては、テンプレート(HTML)の方に記載しています。

動作確認

実際に動かしてみます。

f:id:l08084:20181124201537p:plain
初期表示

姓と名の間にスペースが入力されていない氏名を入力すると、ちゃんとエラーメッセージを表示してくれます。

f:id:l08084:20181124201731p:plain

姓と名の間にちゃんとスペースを入力してあげると、エラーメッセージが消えて登録ボタンもアクティブになりました。成功ですね f:id:l08084:20181124201929p:plain

おわりに

Angularのカスタムバリデーションですが、他にもディレクティブ機能を使う方法などがあり、また今回の例だとそもそもカスタムバリデーションを作らずに正規表現で解決する方法もありました。実際のシステム開発だと姓と名それぞれの文字数制限などのバリデーションが追加されるであろうことを考えると、ディレクティブを使ったり正規表現を使ったほうがキレイなコードが書けたのでは.....みたいな気持ちがあります。でも次回への課題ということでこの記事自体は終了します。

アカウント登録メールの実装をしています

f:id:l08084:20181110163634p:plain
アカウント登録メールの例

はじめに

Webサービスで会員登録の情報を入力した後に、メールアドレス確認のメールが飛んでくるサービスよくありますよね。
あれの実装をいま仕事でしています。よくある機能だと思うので、実装方針を備忘録として記録しました。

作成しているシステムについて

アカウント登録機能のあるWebアプリケーションで、採用している言語・ミドルウェアは下記の一覧を参照

  • Java
  • Angular
  • Ionic
  • PostgreSQL
  • AWS

メールの具体的な仕様

今回実装しているアカウント登録メールの大まかな仕様

  1. ユーザーが仮登録(メールアドレスなどを入力)を完了する
  2. 本登録画面へのURLリンクを含んだメールをユーザーに送信する
  3. URLリンクをクリックしたユーザーを認証して問題なければ、本登録画面に遷移させる

この記事では、URLリンクをクリックしたアカウントの認証(というか特定)方法に絞って解説します。
HTMLメールのレイアウトなどについては、こちらの記事をご参照ください。

アカウント登録のためのURLリンクを作成する

アカウント登録メールでは、アカウント本登録画面に遷移するURLだけではく、クリックしたユーザーを特定する情報が必要になります。

本システムでは、メールを送信する前の画面で入力させる情報がメールアドレスしかなかったため、URLのパラメーターにメールアドレスを含めることでアカウントを特定することにしました。

メールに載せるURLのイメージとしてはこんな感じです。
https://[システムのドメイン]/[アカウント本登録画面のパス]/[暗号化したメールアドレス]

メールアドレス暗号化の手順

URLに生のメールアドレスをそのまま載せるのはセキュリティ的にアレだよね...ってなったので暗号化することにしました。

  1. サーバサイド(Java)でAWS Key Management Service (KMS)を利用してメールアドレスを暗号化する
  2. 暗号化したテキストをBase64エンコードする
  3. 2.のテキストをURLエンコードする

3.URLエンコードを最初忘れていたせいで、暗号化したテキストにURLに使用できない文字が混じって認証に失敗したりしました。

ユーザーがURLリンクをクリックした時の動作としては、URLから本登録画面に遷移した後、WebAPIを呼び出して暗号化したテキストをデコードして元のメールアドレスに戻し、DBを参照してアカウントを特定するという流れにしています。

言いたいこと

完全に霊感で作ったので、その実装ヤバくね?みたいなやつがあったらコソッと教えてください。まだ間に合います