中安拓也のブログ

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

window.open()で開いたウィンドウにデータを渡す

はじめに

window.open()で開いたウィンドウに親ウィンドウからデータを渡す処理を仕事で書いた時に色々と苦労したので、備忘録としてプライベートでも似たような実装をしてみました。

環境

SPAフレームワークのAngular/TypeScriptを使用しています。

  • Angular: 8.2.14
  • Node: 12.13.1
  • TypeScript: 3.5.3

今回やりたいこと

1.あるウィンドウからwindow.open()で別のウィンドウを開く

f:id:l08084:20210130184507p:plain
親ウィンドウからwindow.open()で子ウィンドウを開く

2.親ウィンドウから子ウィンドウにデータを渡す

f:id:l08084:20210130184551p:plain
親ウィンドウから子ウィンドウにデータを渡す

今回の記事では、2. の親ウィンドウから子ウィンドウへの値の渡し方についてフォーカスを当てて説明します。1. のwindow.open()で別ウィンドウを開く実装については、こちらの記事をご参照ください。

ウィンドウにデータを渡す二つの方法

ウィンドウにデータを渡す方法についてですが、本記事では下記の二つの方法について説明します。

1. ローカルストレージを用いる方法

ブラウザにデータを保存する技術の一つであるローカルストレージ(localStorage)を使用してウィンドウ間でデータの受け渡しをします。

f:id:l08084:20210130184639p:plain
localStorageを経由することでウィンドウを跨いでデータを連携する

2. window.postMessage()を用いる方法

Windowオブジェクト間で通信する技術の一つであるwindow.postMessage()を使用して、親ウィンドウから子ウィンドウにデータを送信します。

f:id:l08084:20210130184851p:plain
window.postMessage()でデータを送信する

ウィンドウにデータを渡す実装

ローカルストレージを採用した場合とwindow.postMessage()を採用した場合の二つに分けてwindow.open()で開いたウィンドウにデータを渡す処理の実装について説明していきます。

今回は下記のオブジェクトを親ウィンドウから子ウィンドウに渡します。

export class Message {
  id: number;
  description: string;
}

ローカルストレージを採用した場合

localStorageを使用して、親ウィンドウから子ウィンドウにデータ(オブジェクト)を渡す処理を実装します。

親ウィンドウの実装

親ウィンドウでは、MessageオブジェクトをlocalStorageに格納した後、window.open()で子ウィンドウを開いています。

// 子ウィンドウに渡したいデータを作成
const message = new Message();
message.id = 5;
message.description = 'データ受信成功';

// ローカルストレージにデータを格納します
localStorage.setItem('key', JSON.stringify(message));

// 子ウィンドウを開く
const rootPath = window.location.origin + window.location.pathname;
window.open(rootPath + 'matrix', '_blank', 'location=no,scrollbars=yes');

localStorageにはstring型のデータしか格納できないため、JSON.stringify(message)とすることによってオブジェクトからJSON型文字列に変換してからlocalStorageに格納しています。

子ウィンドウの実装

子ウィンドウでは、親ウィンドウで格納したデータをlocalStorage.getItem()で取り出すことによってデータを受け取っています。

// ローカルストレージ経由でデータを受け取る
const message: Message = JSON.parse(localStorage.getItem('key'));
this.id = message.id;
this.description = message.description;

JSON.parse()を使用することで、JSON型文字列からオブジェクトに変換しています。

window.postMessage()を採用した場合

続いて、window.postMessage()を使用してウィンドウ間でデータを渡す場合の実装について解説していきます。

親ウィンドウの実装

親ウィンドウでは、子ウィンドウを開いた後に、window.postMessage()でデータを送信しています。

// 子ウィンドウに渡したいデータを作成
const message = new Message();
message.id = 5;
message.description = 'データ受信成功';

// 子ウィンドウを開く
const rootPath = window.location.origin + window.location.pathname;
const popup = window.open(
  rootPath + 'matrix',
  '_blank',
  'location=no,scrollbars=yes'
);

// 子ウィンドウにメッセージを送信
popup.onload = () => popup.postMessage(message, window.location.origin);

ポイントとしては、window.onloadイベントハンドラ内でwindow.postMessage()を呼び出すことによって、子ウィンドウが開く前にpostMessageが送信されることを防いている部分です。

注意点:IEブラウザを使う場合

window.postMessage()でデータを送信している処理popup.onload = () => popup.postMessage(message, window.location.origin);ですが、IEブラウザでは正常に動作しません。下記のコードに書き換える必要があります。

popup[popup.addEventListener ? 'addEventListener' : 'attachEvent'](
  // ビルドエラー防止のため、例外的に文字列によるプロパティアクセスを使用している
  // tslint:disable-next-line:no-string-literal
  (popup['attachEvent'] ? 'on' : '') + 'load',
  () => popup.postMessage(message, window.location.origin),
  false
);
子ウィンドウの実装

子ウィンドウでは、window.addEventListener('message', (event) => {を使用することで、受信したメッセージを受け取っています。

なお、if (event.origin === window.location.originの部分では、メッセージ送信元のオリジンを確認することで、不正なメッセージの受信を防いでいます。今回は同一オリジン間でのメッセージのやり取りなので、送信元オリジンがwindow.location.originと一致していれば、OKとしています。

Angular以外の場合

    window.addEventListener('message', (event) => {
      if (event.origin === window.location.origin && event.data.id) {
        const message: Message = event.data;
        this.id = message.id;
        this.description = message.description;
      }
    });

受信したメッセージは、event.dataの部分になります。

Angularの場合

Angularを使用している場合は、window.addEventListener@HostListenerに書き換えることができます。

  @HostListener('window:message', ['$event'])
  public onPostMessage(event) {
    if (event.origin === window.location.origin && event.data.id) {
      const message: Message = event.data;
      this.id = message.id;
      this.description = message.description;
    }
  }

参考サイト

javascript - Problems with window.postMessage on Chrome - Stack Overflow

https://qrunch.net/@tercel/entries/hazZdGPlAaoAHGWD

javascript - Detecting the onload event of a window opened with window.open - Stack Overflow

Angular(SPA)でwindow.open()を使う

f:id:l08084:20210130180431p:plain

はじめに

仕事でAngularを使ってアプリケーションを作成している時に、window.open()で別ウィンドウを表示する要件があったので、備忘録としてプライデートでも似たような実装をしてみました。

環境

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

  • Angular: 8.2.14
  • Node: 12.13.1

作ったもの

サンプルとして、親ウィンドウの「マトリクス図を開く」ボタンを押下すると新規ウィンドウとしてマトリクス画面(子ウィンドウ)を開くアプリケーションを作りました。

f:id:l08084:20200503182208p:plain
親ウィンドウ

f:id:l08084:20210130180629p:plain
子ウィンドウ

実装

サンプルアプリケーションの実装について順を追って説明していきます。

ルーティング設定

まずルーティングの設定から説明します。親(top)、子(matrix)のルーティングを下記の通り設定しました。

app-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { TopComponent } from './component/top/top.component';
import { MatrixComponent } from './component/matrix/matrix.component';

const routes: Routes = [
  { path: '', component: TopComponent, pathMatch: 'full' },
  { path: 'top', redirectTo: '' },
  { path: 'matrix', component: MatrixComponent },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}

親ウィンドウの実装

親ウィンドウの実装について説明します。ボタンを押下した時に呼び出されるopenMatrix()メソッドで、window.open()に子ウィンドウのルーティングURLを設定して別ウィンドウとして子ウィンドウであるマトリクス画面を表示しています。

top.component.html

<div class="wrapper">
  <button (click)="openMatrix()">マトリクス図を開く</button>
</div>

top.component.ts

import { Component, OnInit } from '@angular/core';

/**
 * 親ウィンドウコンポーネント
 *
 * @export
 * @class TopComponent
 * @implements {OnInit}
 */
@Component({
  selector: 'app-top',
  templateUrl: './top.component.html',
  styleUrls: ['./top.component.scss'],
})
export class TopComponent implements OnInit {
  constructor() {}

  ngOnInit() {}

  /**
   * 別ウィンドウでマトリックス画面を開く
   *
   * @memberof TopComponent
   */
  public openMatrix() {
    const rootPath = window.location.origin + window.location.pathname;
    window.open(rootPath + 'matrix', '_blank', 'location=no,scrollbars=yes');
  }
}

子ウィンドウの実装

子ウィンドウ(マトリクス画面)については、特別な実装が必要ないので説明を省略します。

データの受け渡しについて

本サンプルアプリケーションでは、親ウィンドウから子ウィンドウへのデータの受け渡しは実施していませんが、仕事で作ったアプリケーションではlocalStorageを使って、データの受け渡しを実現しました。

実装方法としては、親ウィンドウ側でlocalStorage.setItem()でデータを格納し、子ウィンドウ側でlocalStorage.getItem()でデータを取得することで親ウィンドウからデータを受け取るといった感じで実装しています。

参考サイト

Location - Web API | MDN

https://qrunch.net/@tercel/entries/hazZdGPlAaoAHGWD

HTMLとCSSでマトリックス図を描く

f:id:l08084:20210130175314p:plain
作成したマトリクス図

はじめに

仕事でCSSフレームワークを使わずにテーブルを作成する機会があったので、備忘録としてプライベートでもマトリクス図を作ってみました。

環境

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

  • Angular: 8.2.14
  • Node: 12.13.1

マトリクス図の作成

サンプルとして、年収と年齢から、その人がお客さんになってくれそうかどうかを判定するマトリクス図を作ってみます(トップの画像が完成図)。

CSSリセット

デフォルトで<table>タグを使うと、セルとセルの間に余白ができてしまうので、下記のCSSを追加してセルの隙間を消します。

table {
  border-collapse: collapse;
  border-spacing: 0;
}

マトリクス図の全コード

完成したマトリクス図の全コードになります。Angualrフレームワークを使用しない場合は、matrix.component.tsは必要ないので無視してください。 レイアウトの設定はSCSSを使用しています。

matrix.component.html

<div class="wrapper">
  <!-- 顧客分類マトリクス -->
  <div class="title-wrapper">
    <!-- テーブルのタイトル -->
    <div class="group-title">顧客分類マトリクス</div>
    <!-- 凡例 -->
    <div class="usage-guide">
      <div class="item space">
        <div class="square origin"></div>
        <div class="text"> : 初回入力値</div>
      </div>
      <div class="item">
        <div class="square adjust"></div>
        <div class="text"> : 再入力値</div>
      </div>
    </div>
  </div>
  <!-- マトリクス図 -->
  <table class="matrix" border="1">
    <thead>
      <tr>
        <th colspan="2" rowspan="2"></th>
        <th class="repayment-ratio" colspan="5">年齢</th>
      </tr>
      <tr>
        <th class="percent">〜30</th>
        <th class="percent">〜35</th>
        <th class="percent">〜40</th>
        <th class="percent">〜45</th>
        <th class="percent">45超</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th class="total-income" rowspan="3">年収
        </th>
        <th class="income">250万円以上<br>400万円未満</th>
        <td>A</td>
        <td>A</td>
        <td class="not-lend adjust">X</td>
        <td class="not-lend">X</td>
        <td class="right-edge not-lend">X</td>
      </tr>
      <tr>
        <th class="income">400万円以上<br>500万円未満</th>
        <td>A</td>
        <td>A</td>
        <td>A</td>
        <td class="not-lend">X</td>
        <td class="right-edge not-lend">X</td>
      </tr>
      <tr>
        <th class="income">500万以上</th>
        <td class="bottom">
          A</td>
        <td class="bottom origin">
          B</td>
        <td class="bottom">
          B</td>
        <td class="bottom">
          B</td>
        <td class="bottom right-edge not-lend">
          X</td>
      </tr>
    </tbody>
  </table>
  <!-- 見込みテーブル -->
  <table class="example" border="1">
    <tbody>
      <tr>
        <th>見込み</th>
        <td>
          <div>
            A
            <span class="text">
              :一般客
            </span>
          </div>
          <div>
            B
            <span class="text">
              お得意様
            </span>
          </div>
          <div>
            <span class="not-lend">X</span>
            <span class="text">
              :マーケティング対象外
            </span>
          </div>
        </td>
      </tr>
    </tbody>
  </table>
</div>

matrix.component.scss

.wrapper {
  align-items: center;
  display: flex;
  flex-direction: column;
  padding: 48px;

  .title-wrapper {
    display: flex;
    width: 364px;

    .group-title {
      font-size: 16px;
      font-weight: bold;
      letter-spacing: .2px;
    }

    .usage-guide {
      align-items: center;
      display: flex;
      font-weight: normal;
      justify-content: flex-end;
      width: 184px;

      &.valuation {
        width: 265px;
      }

      .item {
        display: flex;

        .square {
          margin: 2px;

          &.origin {
            height: 12px;
            width: 12px;
            background-color: rgba(84, 141, 255, .25);
            border: solid 1px #a3a3a3;
          }

          &.adjust {
            height: 10px;
            width: 10px;
            border: solid 3px #548DFF;
          }
        }

        .text {
          font-size: 11px;
          margin-left: 2px;
        }
      }

      .space {
        margin-right: 8px;
      }
    }
  }

  table {

    &.matrix,
    &.valuation {
      border: solid 1px #545454;
      font-size: 10px;
      height: 160px;
      margin: 16px 0;

      th {
        background: #e6e6e6;
        border: solid 1px #545454;
        text-align: center;

        &.total-income {
          padding: 5px;
          width: 30px;
        }

        &.income {
          height: 36px;
          width: 80px;
        }

        &.repayment-ratio,
        &.percent {
          height: 28px;
        }
      }

      td {
        border: solid 1px #a3a3a3;
        height: 36px;
        text-align: center;
        width: 52px;

        &.bottom {
          border-bottom: solid 1px #545454;
        }

        &.right-edge {
          border-right: solid 1px #545454;
        }

        &.origin {
          background-color: rgba(84, 141, 255, .25);
        }

        &.adjust {
          border: solid 3px #548DFF;
        }
      }
    }

    &.example {
      border: solid 1px #545454;
      font-size: 10px;
      height: 22px;
      margin-bottom: 40px;

      th {
        text-align: center;
        width: 69px;
      }

      td {
        align-items: center;
        border: 0;
        display: flex;
        font-size: 11px;
        height: 100%;
        justify-content: space-around;
        padding: 0 12px;
        width: 299.8px;

        .text {
          font-weight: normal;
        }
      }

      &.interest-rate {
        margin-top: 16px;

        th {
          width: 109px;
        }

        td {
          padding: 0 26px;
          width: 259.8px;
        }
      }
    }

    &.valuation {
      margin: 4px 0;
    }
  }

  .not-lend {
    color: #a3a3a3;
  }

  .table-title {
    font-size: 14px;
    margin-top: 16px;
    text-align: left;
    width: 364px;
  }
}

matrix.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-matrix',
  templateUrl: './matrix.component.html',
  styleUrls: ['./matrix.component.scss'],
})
export class MatrixComponent {
  constructor() {}
}

参考サイト

flexboxを使った中央寄せについて - Qiita

CSSで作図する - Qiita

Angular + Firebase でFacebook認証

はじめに

AngularとFirebaseを使用して、Facebook認証機能を実装します。

なお、AngularとFirebaseによるメールアドレス/パスワードの認証とTwitter認証は実装ずみで、下記の記事で説明もしています。

  • メールアドレス/パスワードの認証

AngularでFirebase認証(その1) Firebaseのセットアップ - 中安拓也のブログ

AngularでFirebase認証(その2) Angular Materialを使ったログイン画面の作成 - 中安拓也のブログ

AngularでFirebase認証(その3) Firebase Authentication の呼び出し - 中安拓也のブログ

Angular + Firebase でアカウント登録画面の作成 - 中安拓也のブログ

  • Twitter認証

Angular + Firebase でTwitter認証 - 中安拓也のブログ

環境

フロントエンドのフレームワークにはAngular/TypeScriptを、CSSフレームワークにはAngular Materialを使用しています。

Firebase関連のライブラリのバージョンは下記となります。

  • Angular CLI@8.3.20
  • Node@12.13.1
  • OS: darwin x64
  • Angular@8.2.14
  • firebase@6.3.4
  • angular/fire@5.2.1

Facebook for Developersアカウントを作成

Facebook認証を実装するにはFacebookのアプリケーション IDとアプリ シークレットを取得する必要があるため、Facebookの開発者向けサイトに移動して、Facebookの開発者向けアカウントを作成します。

f:id:l08084:20200422214931p:plain

アプリの登録

Developersアカウントの作成が完了すると、そのままアプリの作成画面が表示されるのでアプリを作成します。

f:id:l08084:20200422215027p:plain

f:id:l08084:20200422220712p:plain

アプリを作成すると作成したアプリの[設定] > [ベーシック]からアプリケーションIDとアプリシークレットを確認できるようになります。

FirebaseコンソールでFacebook認証を有効にする

Firebaseコンソールに移動してFacebookを認証方法として有効にします。なお、有効にする際には先ほど取得したアプリケーション IDとアプリ シークレットを入力する必要があります。

f:id:l08084:20200422005212p:plain
Facebookを認証方法として有効にする

また、Firebaseコンソールに表示されているコールバックURLもFacebookの開発者向けサイトに登録したアプリに設定する必要があります。

f:id:l08084:20200422221509p:plain
コールバックURLをコピーする

[製品を追加] > [Facebookログイン] > [設定] を選択して、有効なOAuthリダイレクトURI欄にコピーしたコールバックURLを設定します。

f:id:l08084:20200422222651p:plain
コールバックURLを設定する

これで「Firebase構成オブジェクトの転記」が完了してる場合は、Firebase側の設定はすべて完了です。「Firebase構成オブジェクトの転記」が完了していない場合は、下記の記事を参考にしてください。

AngularでFirebase認証(その1) Firebaseのセットアップ - 中安拓也のブログ

「Facebookでログイン」ボタンを作成する

続いてFacebookログイン用のボタンを作成していきます。FacebookのアイコンはFont AwesomeのFacebookアイコンを使用します。

f:id:l08084:20200423005855p:plain
「Facebookでログイン」ボタン

Angular Materialを使用しているので、<mat-icon class="sns-icon fab fa-facebook-f"></mat-icon>を使用することでFacebookアイコンをボタンに埋め込むことができます。

下記のようにHTMLとSCSSを記載すればFacebookボタンが完成します。

  • login.component.html
      <button (click)="signInWithFacebook()" class="facebook" mat-raised-button>
        <mat-icon class="sns-icon fab fa-facebook-f"></mat-icon>Facebookでログイン
      </button>
  • login.component.scss
    .sns-icon {
      font-size: 20px;
      position: absolute;
      left: 15px;
      top: 25%;
    }

    .facebook {
      color: #FFF;
      background-color: #3b5998;
      border-color: #3b5998;
      font-weight: bold;
      width: 100%;
      margin-bottom: 10px;
    }

Facebook認証機能の実装

それではFirebaseと@angular/fireライブラリを使用して、Facebook認証機能の実装をやっていきます。@angular/fireライブラリのインストールが完了していない場合は、下記の記事を参考に実施してください。

AngularでFirebase認証(その1) Firebaseのセットアップ - 中安拓也のブログ

  • login.component.html
      <button (click)="signInWithFacebook()" class="facebook" mat-raised-button>
        <mat-icon class="sns-icon fab fa-facebook-f"></mat-icon>Facebookでログイン
      </button>
  • login.component.ts
import { Router } from '@angular/router';
import { AuthenticationService } from '../services/authentication.service';

export class LoginComponent implements OnInit {
  constructor(
    private router: Router,
    private authenticationService: AuthenticationService
  ) {}

  /**
   * Facebook認証でログイン
   *
   * @memberof LoginComponent
   */
  public async signInWithFacebook() {
    try {
      await this.authenticationService.signInWithFacebook();
      // ログインに成功したらホーム画面に遷移する
      this.router.navigate(['/home']);
    } catch (error) {
      console.log(error);
    }
  }

}
  • authentication.service.ts
import { AngularFireAuth } from '@angular/fire/auth';
import * as firebase from 'firebase/app';

export class AuthenticationService {
  constructor(public afAuth: AngularFireAuth) {}

  public signInWithFacebook(): Promise<auth.UserCredential> {
    return this.afAuth.auth.signInWithPopup(
      new firebase.auth.FacebookAuthProvider()
    );
  }
}

Facebookの認証APIを呼び出しているのは、下記の部分のコードとなります。

this.afAuth.auth.signInWithPopup(new firebase.auth.FacebookAuthProvider());

上記のソースコードを実装して「Facebookでログイン」ボタンを押下すると、下記のようにポップアップでFacebook認証のページが表示され、Facebook認証に成功するとログインが完了し、ホーム画面に遷移します。

f:id:l08084:20200426165135p:plain
「Facebookでログイン」ボタンを押下するとポップアップが表示される

補足: Facebook認証を利用したアカウント登録機能の実装

f:id:l08084:20200426180116p:plain
アカウント登録画面

Facebook認証を使用して、アカウント登録機能を実装する場合もFacebook認証でログインを実施する場合と全く同じ実装で実現できます(ログインと同様にthis.afAuth.auth.signInWithPopup(new firebase.auth.FacebookAuthProvider());を呼ぶことでアカウント登録も実装できる)。

というのも、Firebaseを使ったFacebook認証を実施した場合、Facebook認証を実施した時にそのユーザーが存在しなければ、Firebaseにアカウントが新規登録されるため、ログインもアカウント登録も同様のFirebase APIを呼び出すことで実現できるからです。

参考サイト

JavaScript で Facebook ログインを使用して認証する  |  Firebase

Angular + Firebase でTwitter認証

はじめに

AngularとFirebaseを使用して、Twitter認証機能を実装します。

なお、AngularとFirebaseによるメールアドレスとパスワードの認証については実装ずみで、下記の記事で説明もしています。

AngularでFirebase認証(その1) Firebaseのセットアップ - 中安拓也のブログ

AngularでFirebase認証(その2) Angular Materialを使ったログイン画面の作成 - 中安拓也のブログ

AngularでFirebase認証(その3) Firebase Authentication の呼び出し - 中安拓也のブログ

Angular + Firebase でアカウント登録画面の作成 - 中安拓也のブログ

環境

フロントエンドのフレームワークにはAngular/TypeScriptを、CSSフレームワークにはAngular Materialを使用しています。

Firebase関連のライブラリのバージョンは下記となります。

  • firebase@6.3.4
  • angular/fire@5.2.1

また、$ ng --versionの実行結果は下記の通りです。

$ ng --version

Angular CLI: 8.3.20
Node: 12.13.1
OS: darwin x64
Angular: 8.2.14
... animations, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... router

Package                           Version
-----------------------------------------------------------
@angular-devkit/architect         0.803.20
@angular-devkit/build-angular     0.803.20
@angular-devkit/build-optimizer   0.803.20
@angular-devkit/build-webpack     0.803.20
@angular-devkit/core              8.3.20
@angular-devkit/schematics        8.3.20
@angular/cdk                      7.3.7
@angular/cli                      8.3.20
@angular/fire                     5.2.3
@angular/material                 7.3.7
@ngtools/webpack                  8.3.20
@schematics/angular               8.3.20
@schematics/update                0.803.20
rxjs                              6.5.3
typescript                        3.5.3
webpack                           4.39.2

TwitterのAPIキーとAPIシークレットを取得する

f:id:l08084:20200409011846p:plain
Create App をクリック

Twitterの開発者向けサイトに移動して、Create an appボタンをクリックします。

Twitter Developer アカウントの作成

Twitter Developer Accountを持っていない場合は、Twitter Developer Accountの作成ページに自動で遷移します。

どのような目的でTwitter APIを使用するのか?などの質問をされるので英語で回答します。

すべての質問に回答すると下記のような画面が表示された後、本人確認用リンクが添付されたメールが送信されるので、本人確認用リンクをクリックするとTwitter Developer Accountの登録が完了します。

f:id:l08084:20200411225849p:plain

アプリの登録

再度Twitterの開発者向けサイトに移動して、Create an appボタンをクリックします。すると、下記のような画面が表示されるので回答していきます。

f:id:l08084:20200411230341p:plain

Website URLもCallback URLsも現時点では入力できないので、適用にやり過ごしてあとで正しい値を設定するようにします。

回答が完了すると下記の画面を閲覧できるようになり、APIキーとAPIシークレットを取得できます。

f:id:l08084:20200411234429p:plain

FirebaseコンソールでTwitter認証を有効にする

Firebaseコンソールに移動してTwitterを認証方法として有効にします。なお、有効にする際には先ほど取得したAPIキーとAPIシークレットを入力する必要があります。

f:id:l08084:20200407224024p:plain
Twitterを認証方法として有効にする

また、Firebaseコンソールに表示されているコールバックURLもTwitterの開発者向けサイトに登録したアプリに設定する必要があります。

f:id:l08084:20200412001400p:plain
コールバックURLをコピーする

f:id:l08084:20200412001733p:plain
コールバックURLを設定する

これで「Firebase構成オブジェクトの転記」が完了してる場合は、Firebase側の設定はすべて完了です。「Firebase構成オブジェクトの転記」が完了していない場合は、下記の記事を参考にしてやってみてください。

AngularでFirebase認証(その1) Firebaseのセットアップ - 中安拓也のブログ

「Twitterでログイン」ボタンを作成する

続いてTwitterログイン用のボタンを作成していきます。TwitterのアイコンはFont AwesomeのTwitterアイコンを使用します。

f:id:l08084:20200419145552p:plain
「Twitterでログイン」ボタン

Angular Materialを使用しているので、<mat-icon class="sns-icon fab fa-twitter"></mat-icon>を使用することでTwiiterアイコンをボタンに埋め込むことができます。

下記のようにHTMLとSCSSを記載すればTwitterボタンが完成します。

  • login.component.html
      <button (click)="signInWithTwitter()" class="twitter" mat-raised-button>
        <mat-icon class="sns-icon fab fa-twitter"></mat-icon>Twitterでログイン
      </button>
  • login.component.scss
    .sns-icon {
      font-size: 20px;
      position: absolute;
      left: 15px;
      top: 25%;
    }

    .twitter {
      color: #FFF;
      background-color: #00acee;
      border-color: #00acee;
      font-weight: bold;
      width: 100%;
      margin-bottom: 10px;
    }

Twitter認証機能の実装

それではFirebaseと@angular/fireライブラリを使用して、Twitter認証機能の実装をやっていきます。@angular/fireライブラリのインストールが完了していない場合は、下記の記事を参考に実施してください。

AngularでFirebase認証(その1) Firebaseのセットアップ - 中安拓也のブログ

  • login.component.html
      <button (click)="signInWithTwitter()" class="twitter" mat-raised-button>
        <mat-icon class="sns-icon fab fa-twitter"></mat-icon>Twitterでログイン
      </button>
  • login.component.ts
import { Router } from '@angular/router';
import { AuthenticationService } from '../services/authentication.service';

export class LoginComponent implements OnInit {
  constructor(
    private router: Router,
    private authenticationService: AuthenticationService
  ) {}

  /**
   * Twitter認証でログイン
   *
   * @memberof LoginComponent
   */
  public async signInWithTwitter() {
    try {
      await this.authenticationService.signInWithTwitter();
      // ログインに成功したらホーム画面に遷移する
      this.router.navigate(['/home']);
    } catch (error) {
      console.log(error);
    }
  }

}
  • authentication.service.ts
import { AngularFireAuth } from '@angular/fire/auth';
import * as firebase from 'firebase/app';

export class AuthenticationService {
  constructor(public afAuth: AngularFireAuth) {}

  public signInWithTwitter(): Promise<auth.UserCredential> {
    return this.afAuth.auth.signInWithPopup(
      new firebase.auth.TwitterAuthProvider()
    );
  }
}

Twitterの認証APIを呼び出しているのは、下記の部分のコードとなります。

this.afAuth.auth.signInWithPopup(new firebase.auth.TwitterAuthProvider());

上記のソースコードを実装して「Twitterでログイン」ボタンを押下すると、下記のようにポップアップでTwitter認証のページが表示され、Twitter認証に成功するとログインが完了し、ホーム画面に遷移します。

f:id:l08084:20200419153728p:plain

補足: Twitter認証を利用したアカウント登録機能の実装

f:id:l08084:20200419154152p:plain
アカウント登録画面

Twitter認証を使用して、アカウント登録機能を実装する場合もTwitter認証でログインを実施する場合と全く同じ実装で実現できます(ログインと同様にthis.afAuth.auth.signInWithPopup(new firebase.auth.TwitterAuthProvider());を呼ぶことでアカウント登録も実装できる)。

というのも、Firebaseを使ったTwitter認証を実施した場合、Twitter認証を実施した時にそのユーザーが存在しなければ、Firebaseにアカウントが新規登録されるため、ログインもアカウント登録も同様のFirebase APIを呼び出すことで実現できるからです。

参考サイト

JavaScript による Twitter を使用した認証  |  Firebase

Build Firebase Login with Twitter in Angular 7|8|9 - Positronx.io

【2020年】TwitterのAPIに登録し、アクセスキー・トークンを取得する具体的な方法 | Rabbishar-ラビシャー

Angular7でFont AwesomeをAngular Materialに統合 - Qiita

Firebase を JavaScript プロジェクトに追加する

【IE11】PDFファイルを保存せずにそのまま開けるようにする

はじめに

SPAアプリ上でIE11ブラウザを使ってPDFファイルを参照する場合に、PDFファイルをダウンロードせずにそのまま開いて参照したいとの要望をクライアントからいただいたため、ソースコードを修正した。

環境

Angularを使用して作成されたSPAアプリ。利用ブラウザはIE11

  • Angular@6.1.2
  • typescript@2.8.4
  • Internet Explorer11

msSaveBlobからmsSaveOrOpenBlobに変更

修正内容だが、ファイルをローカルに保存する時に呼び出しているAPIをwindow.navigator.msSaveBlobからwindow.navigator.msSaveOrOpenBlob変更して、ポップアップに保存ボタンだけでなくファイルを開くボタンも表示されるようにした。

こうすることによってPDFファイルを保存してから閲覧するのではなくて、ファイルを保存せずに閲覧することもできるようにした。

// 修正前のコード
window.navigator.msSaveBlob(blob, fileName);

// 修正後のコード
window.navigator.msSaveOrOpenBlob(blob, fileName);

下記のように、msSaveBlobではなくmsSaveOrOpenBlobを使用することでポップアップにファイルを開くボタンを表示することができる。

f:id:l08084:20200406224401p:plain
msSaveBlobのポップアップ

f:id:l08084:20200406223933p:plain
msSaveOrOpenBlobのポップアップ

参考サイト

File API で作成した blob をダウンロードする | Hebikuzure's Tech Memo

IE11にはファイルをローカルに保存するJavaScriptのAPIが2種類用意されている。 - Qiita

msSaveBlob - Web APIs | MDN

msSaveOrOpenBlob - Web APIs | MDN

webpackで「JavaScript heap out of memory」エラー

はじめに

npm scriptsを使ってIonic(Angular/TypeScript)のAndroidビルドをしている時にFATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memoryエラーが発生して処理が落ちてしまった。

環境

  • Windows 10
  • Angular@5.0.1
  • ionic@3.9.5
  • webpack@3.8.1
  • node v8.11.3
  • npm@6.1.0

発生したエラー

発生しているエラーはヒープサイズ不足によるエラーとなるFATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory

詳細なエラーメッセージは下記となる(省略部分あり)。

$ npm run build:android --env=dev4 --releasetype=android

<中略>

> webpack --progress --config ./config/webpack.config.js --hide-modules

69% building modules 1427/1437 modules 10 active ...odules\core-js\modules\_math-log1p 
69% building modules 1428/1437 modules 9 active ...odules\core-js\modules\_math-log1p. 
69% building modules 1429/1437 modules 8 active ...odules\core-js\modules\_math-log1p. 
69% building modules 1430/1437 modules 7 active ...odules\core-js\modules\_math-log1p. 

<中略>

91% additional asset processing       
<--- Last few GCs --->

[23324:000001DB86E99F80]  1136311 ms: Mark-sweep 1345.4 (1446.3) -> 1345.4 (1446.3) MB, 1025.9 / 0.0 ms  allocation failure GC in old space requested
[23324:000001DB86E99F80]  1137592 ms: Mark-sweep 1345.4 (1446.3) -> 1345.4 (1435.8) MB, 1154.1 / 0.0 ms  last resort GC in old space requested
[23324:000001DB86E99F80]  1138612 ms: Mark-sweep 1345.4 (1435.8) -> 1345.4 (1435.8) MB, 1019.8 / 0.0 ms  last resort GC in old space requested


<--- JS stacktrace --->

==== JS stack trace =========================================

Security context: 000003CA08025879 <JSObject>
    1: /* anonymous */(aka /* anonymous */) [000002FAFA6022D1 <undefined>:~3227] [pc=000001AC8582FFF7](this=000002FAFA6022D1 <undefined>,self=0000035515E06269 <AST_Array map 
= 000000C2D106EC59>,tw=00000317A7E51B79 <Compressor map = 000000C2D10709E9>)
    2: before [000002FAFA6022D1 <undefined>:~5479] [pc=000001AC8559B096](this=00000317A7E51B79 <Compressor map = 000000C2D10709E9>,node=000003551...

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
 1: node_module_register
 2: v8::internal::FatalProcessOutOfMemory
 3: v8::internal::FatalProcessOutOfMemory
 4: v8::internal::Factory::NewCodeRaw
 5: v8::internal::Factory::NewCode
 6: v8::internal::modulo
 7: v8::internal::compiler::ControlFlowOptimizer::TryBuildSwitch
 8: v8::internal::interpreter::HandlerTableBuilder::HandlerTableBuilder
 9: v8::internal::compiler::Pipeline::AllocateRegistersForTesting
10: v8::internal::compiler::ValueNumberingReducer::operator=
11: v8::internal::CompilationJob::FinalizeJob
12: v8::internal::CompilationJob::isolate
13: v8::internal::Compiler::FinalizeCompilationJob
14: v8::internal::OptimizingCompileDispatcher::InstallOptimizedFunctions
15: v8::internal::StackGuard::HandleInterrupts
16: v8::internal::wasm::WasmOpcodes::TrapReasonMessage
17: 000001AC847043C1
npm ERR! code ELIFECYCLE
npm ERR! errno 3

<省略>

上記のエラーメッセージを見ていくとwebpackのコマンド(webpack --progress --config ./config/webpack.config.js --hide-modules)を実行しているタイミングでヒープサイズ不足のエラーが発生していることがわかる。

対処法

エラーが発生しているnpm scriptsのコマンド(build:ts)にオプションを追加して、--max_old_space_sizeのサイズを大きくしてから再実行すると直った。

  • package.json
// 修正前
"build:ts": "webpack --progress --config ./config/webpack.config.js --hide-modules"

// 修正後
"build:ts": "cross-env NODE_OPTIONS=--max_old_space_size=2048 webpack --progress --config ./config/webpack.config.js --hide-modules"

上記の通り、npm scriptsのwebpackのコマンドにcross-env NODE_OPTIONS=--max_old_space_size=2048を追記して、--max_old_space_sizeのサイズを拡大している。

なお、cross-envがインストールされていなくてエラーが発生する場合は、下記の通りcross-envをインストールする必要がある。

npm i cross-env

参考サイト

Webpack でビルドが稀に落ちる現象の回避 - Qiita

JavaScript heap out of memory が発生したときに試したこと ++ Gaji-Laboブログ

npm searchの「JavaScript heap out of memory」エラー対応 - Qiita