なになれ

IT系のことを記録していきます

Angular+FirebaseのWebアプリを公開するために実施したこと

過去に記事にしたWebアプリを公開しました。

bookshelf-share.hi1280.com

技術的なことは下記記事を見てください。

hi1280.hatenablog.com

この記事では、アプリの開発以外で実際に公開するために実施したことを紹介します。

目次

テスト環境の作成

公開環境でテストをするわけにはいかないので、テストのための環境を用意する必要があります。

Firebase上で別プロジェクトを作成して、テスト環境を用意しました。
FirebaseのCLIツールでは、一つの作業ディレクトリで複数のプロジェクトを扱うことができます。

参考URL
Firebase CLI リファレンス  |  Firebase

Angular CLIでAngularプロジェクトを作成した場合にenvironmentsで環境毎の設定変更が可能です。
アプリ内での環境変更はこれを活用しました。
/src/environments/にテスト環境用と本番環境用の設定を用意しました。

独自ドメインの設定

お名前.comでドメインを取得しました。
Firebaseの場合、FirebaseのコンソールにあるHostingのメニューから独自ドメインを設定することができます。
SSL証明書も自動的に付いてきます。
Firebaseの情報に従って、お名前.comのDNSサーバにDNSレコードを設定しました。

参考URL
Connect a custom domain  |  Firebase

トップページにアプリの紹介を載せる

トップページには、どんなアプリなのかを理解してもらうための簡潔な説明を書きました。
いらすとやから画像を拝借しています。

かわいいフリー素材集 いらすとや

問い合わせ先の記載

メールアドレスの用意

Webアプリの問い合わせ先を用意します。
問い合わせ先として独自ドメインのメールアドレスを用意しました。
参考にしたサイトが独自ドメインのメールアドレスを用意していたので、そういうものなんだと思っています。
独自ドメインのメールアドレスを用意するのにお名前メールを使いました。

失敗したこと

お名前メール用の共用DNSサーバを使う必要があるようで、Firebaseへのホスティング用に使用したDNSサーバは使えずにドメイン名が異なるメールアドレスになってしまいました。
契約した後に気付いたので、これはそのままになっています。

利用規約とプライバシーポリシーの記載

Webアプリのルールを定めたものとして利用規約とプライバシーポリシーを用意する必要があるようです。

作成にあたっては、こちらを参考にしました。
Webサイトの利用規約(無料テンプレート・商用利用可)

見よう見まねです。

その頃よくチェックしていた技術書典のサイトも大いに参考にしました。
技術書典8

著作権表示の記載

著作権表示を慣習として用意します。
著作権表示には正式な書き方があるようです。
色々な書き方がありますが、下記の内容が表記されていれば十分のようです。

  • Copyright表記
  • 著作物の発行年号
  • 著作権所有者の氏名

こちらを参考にしました。
Copyright(コピーライト:著作権表示)の正しい書き方を知っていますか?:webサイト制作 - webデザイン初心者|sometimes study

こちらも見よう見まねです。

Googleアナリティクスを追加

アプリをどれくらいのユーザが利用しているのかトラッキングしたいので、Googleアナリティクスを導入します。
AngularでGoogleアナリティクスを使うためのポイントは下記の通りです。

  • index.htmlにGoogleアナリティクスが提供するJavaScriptのトラッキングコードを記載する
  • ルーティングの際にページを表示したデータを送る

app.component.ts

...
constructor(private router: Router) {
  this.router.events.subscribe(event => {
    if (event instanceof NavigationEnd) {
      (<any>window).ga('set', 'page', event.urlAfterRedirects);
      (<any>window).ga('send', 'pageview');
    }
  });
}
...

参考URL
Using Google Analytics with Angular - codeburst

JavaScriptのエラートラッキングを追加

今回のアプリの構成だとサーバがないためにエラーが発生してもクライアントでしか分かりません。
クライアントのエラー情報を収集するための仕組みを用意する必要がありました。

このためにSentryというサービスを使いました。
Application Monitoring and Error Tracking Software | Sentry

Angularで使うためのドキュメントが公式で用意されています。
Integrations - Docs

Sentryのコンソールからクライアントで発生したJavaScriptのエラー情報が見えるようになりました。

SNSシェアボタンの設置

Twitterはてブのボタンを設置しました。
Twitterでの表示を意図したものにするためにOGPの設定を行いました。

index.html

<meta property="og:title" content="ページタイトル" />
<meta property="og:site_name" content="サイト名" />
<meta property="og:type" content="website" />
<meta property="og:url" content="ページのURL" />
<meta property="og:description" content="ページの説明" />
<meta property="og:image" content="ページのイメージ画像" />
<meta name="twitter:card" content="summary">

ボタンの配置は公式で提供されている方法を使いました。

Twitter Publish

はてなブックマークボタンの作成・設置について

ボタンを配置するためのコードにscriptタグが含まれていますが、
Angularではscriptタグをhtmlに記載しても無効化されてしまうので、
コンポーネントの初期処理時にscriptタグを生成するコードを記載しました。

...
constructor(
  private elementRef: ElementRef
) {}

ngOnInit() {
  const twitter = document.createElement('script');
  twitter.src = 'https://platform.twitter.com/widgets.js';
  twitter.async = true;
  twitter.charset = 'utf-8';
  this.elementRef.nativeElement.appendChild(twitter);
}
...

参考URL
javascript - script tag in angular2 template / hook when template dom is loaded - Stack Overflow

まとめ

友達から気軽に本を借りたいという個人的な動機から生まれたアプリです。
今後も使い勝手を向上するために改善して行く予定です。

https://bookshelf-share.site/bookshelf-share.site

AngularFireを使うと遭遇するRxJSでのハマりどころ

Angularを使うと漏れなくRxJSを使うことになります。
非同期処理を命令的に書くことがなくなるメリットがある反面、
宣言的プログラミングに慣れていない人にとってはRxJSを理解することが難しいです。
AngularFireを使ってWebアプリを作成する中で、遭遇したハマりどころを紹介します。
この記事では、RxJSの基本的なことは説明しません。

意図せずに処理が呼ばれる

AngularFireを使った場合にストリームがどういったタイミングで流れるかを意識しないと意図しないタイミングで処理が実行されてしまいます。
例えば、DBからデータを取得して、そのデータを条件にしてデータを削除する場合などです。

悪い例

// getUserはユーザの情報を取得して、レスポンスをvalueChangesメソッドによるObservableで返す関数です
// deleteItemは項目を削除する疑似関数です

getUser(id).subscribe((user) => {
    deleteItem(user.itemId);
});

getUser(id: string): Observable<{}> {
    return this.db.doc(`users/${id}`).valueChanges();
}

この場合、ユーザの情報を常に変更検知しているため、ユーザを作成したタイミングで、またこの処理が実行されてしまう可能性があります。

良い例

getUser(id).take(1).subscribe((user) => {
    deleteItem(user.itemId);
});

takeで処理を実行する回数を1回と指定することで、予期しない処理の実行を防止することができます。

コードのネストが深くなる

Observableによる連続的な非同期処理をsubscribeで処理すると非同期処理ごとにネストが深くなってしまいます。

悪い例

// getUserはユーザの情報、getUserDetailはユーザの詳細情報を取得し、レスポンスをObservableで返す疑似関数です。

getUser(id).subscribe(user => {
    getUserDetail(user.id).subscribe(detail => {
        // 処理内容
    });
});

良い例

getUser(id)
    .mergeMap(user => getUserDetail(user.id))
    .subscribe(detail => {
        // 処理内容
    });

mergeMapを使用することで元のストリームから異なるストリームに切り替えることができます。
イメージ的にはPromiseのthenを使って非同期処理を変えるのに似ていると思います。
このようにストリームを切り替えるオペレータにはmergeMapの他にconcatMap、switchMap、exhaustMapなどがあります。

mergeMapでデータが存在しない場合の条件分岐

mergeMapではデータが存在しない場合でもObservableを返す必要があります。

問題

// getUserはユーザの情報、getUserDetailはユーザの詳細情報を取得し、レスポンスをObservableで返す疑似関数です。

getUser(id)
    .mergeMap(user => {
        // userが存在しなかった場合にuser.idでundefinedで落ちる
        return getUserDetail(user.id);
    });

解決策

getUser(id)
    .mergeMap(user => {
        if(!user) {
            return Observable.empty();
        }
        return getUserDetail(user.id);
    });

emptyで何も値を持たないObservableを返します。

DBから取得したリストそれぞれの項目にデータを付与したい

リスト内の各データを条件に他のデータを参照して、リスト内に参照したデータを追記したいというケースがあります。
例えば、ユーザの一覧とユーザの詳細を結合して、一つの一覧で表示したい場合です。

解決策

// getUsersはユーザの一覧、getUserDetailはユーザの詳細情報を取得し、レスポンスをObservableで返す疑似関数です。

getUsers().map(users => {
    return users.map(user => {
        return getUserDetail(user.id).map(detail => {
            return {...user, ...detail};
        });
    });
}).mergeMap(users => {
    return Observable.combineLatest(users);
});

mapをネストさせることで、ユーザの一覧の各データに対して、ユーザの詳細情報を追記しています。
mapのネストによってObservableの中にObservableがある状態になりますが、最終的にcombineLatestでObservableに変えてます。
combineLatestはストリーム内の値を結合することができるので、結果的に中身のObservableだけを取り出しています。

おわりに

最後の例はこれで問題ないのか結構怪しいです。
もっと良い方法があれば知りたいです。

AngularとFirebaseでWebアプリを作った

AngularとFirebaseでWebアプリを作ったので、その記録です。

作ったものがこちらです。
友達同士で本を共有できるみんなの本棚というコンセプトで、本を登録し、持っている本を友達同士で共有できるサービスです。
bookshelf-share.hi1280.com

この記事では、このサービスを作るにあたって活用している技術について紹介します。

Webアプリのクライアント部分をカバーしているAngularですが、
さらにFirebaseを活用することでバックエンドのアプリなしでWebアプリを作成できます。

Angular

画面を作るためのベースのフレームワークとして利用しました。
Angular

Angular Material

Angularにマテリアルデザインコンポーネントを提供するライブラリです。
Angular Material

Paginator
ページ数をつけるコンポーネントを活用しました。
こんな感じのコンポーネントを簡単に配置できます。 f:id:hi1280:20180310181123p:plain pageEventでページ数を変えた時の処理を記述できます。

paginator.html

<mat-paginator [length]="length"
              [pageSize]="pageSize"
              [pageSizeOptions]="pageSizeOptions"
              (page)="pageEvent = $event">
</mat-paginator>

AngularFire

AngularからFirebaseを利用するためのライブラリです。
GitHub - angular/angularfire2: The official Angular library for Firebase.

Firebase

バックエンドの機能を備えたサービスです。
認証、データベース、ホスティングの機能などがあります。
Firebase

認証

Firebaseのデータベース機能を使うために認証が実用上必須になります。

Googleログインによる認証
認証をGoogleログインに任せる形で実装できます。

app.component.ts

export class AppComponent {
  user: firebase.User;
  constructor(private afAuth: AngularFireAuth, private router: Router) {
    afAuth.authState.subscribe(user => this.user = user);
  }

  login() {
    this.afAuth.authState.subscribe((user) => {
      if (user && user.uid) {
        this.router.navigate(['main']);
      } else {
        this.afAuth.auth.signInWithPopup(new firebase.auth.GoogleAuthProvider());
      }
    });
  }

  logout() {
    this.router.navigate(['home']);
    this.afAuth.auth.signOut();
  }
}

Googleだけでなく、FacebookTwitterGitHubのアカウントを利用することもできます。

データベース

クライアントから直接データベースを操作することができます。

Realtime Database
本の一覧を取得するのにクエリを活用して以下のようなコードを書きました。
データ作成の降順でソートしつつ、ページングに対応しています。

books.component.ts

private getBooks(pageSize: number, page: number) {
  this.db.list('books', ref => ref.orderByKey()).snapshotChanges().subscribe((books) => {
    const book = books[pageSize * page];
    // orderByKeyはkeyで昇順にする。昇順によるソートしか対応していない。
    // limitToFirstはデータの取得するに制限をかける。
    // startAtは取得するデータ開始位置を表す。ここではページング対応のために利用している。
    this.booksRef = this.db.list('books', ref => ref.orderByKey().limitToFirst(this.pageSize).startAt(book.key));
    // keyを使うためのコード
    this.books = this.booksRef.snapshotChanges().map(changes => {
      return changes.map(c => ({ key: c.payload.key, ...c.payload.val() }));
    // 降順にする
    }).map((changes) => changes.reverse());
    this.length = books.length;
  });
}

ホスティング

Angularで作成したWebアプリをFirebaseでホスティングできます。

Angularのアプリをビルドします。

$ ng build --prod

FirebaseのCLIツールをインストールします。

$ npm install -g firebase-tools

Firebaseのプロジェクトとして初期設定をします。

$ firebase init

以下のように質問に答えます。

? What do you want to use as your public directory?
dist

? Configure as a single-page app (rewrite all urls to /index.html)?
y

? File dist/index.html already exists. Overwrite?
N

デプロイします。

$ firebase deploy

注意点

Realtime Databaseはクエリ操作が特殊な感じで、機能も充実していないので使う時には注意が必要です。
少なくとも複雑な検索はできません。