Monaca + Vue.js + Firebase で匿名認証

スマホアプリ開発でiOS、Androidの両方を行ってみて思うのが、やっぱり2つ開発はシンドイと思います。
で、考えるのはハイブリッド開発でしょうか?
Web、Xamarin、React nativeなどいくつか選択肢がありますが、まずはMonacaというHTML5ベースのプラットフォームを試します。

今回は試しにFirebaseと連携し匿名認証を行います。
ユーザーをこちらから特定する必要はないですが、ユーザーの属性情報だけ欲しいというような使い方です。
試して思ったのですが、FirebaseではSNSアカウント、メールアドレスなどで認証するのも簡単そうです。

Monacaの開発環境はクラウド上ではなくローカルで行いました。
MonacaでVue.jsのテンプレートから作成しています。
また、Firebaseで匿名認証を許可する設定まで完了してる前提でプログラムのみ紹介。

src/public/index.html.ejs

まずはFirebaseの設定です。最初 www/index.html に書いたのですが、何かのタイミングでファイルが戻ってしまう?現象があったので、index.html.ejs に書きました。

<body>
  <div id="app"></div>
  <script src="https://www.gstatic.com/firebasejs/5.5.5/firebase.js"></script>
  <script>
    // Initialize Firebase
    var config = {
      apiKey: "{Firebaseから}",
      authDomain: "{Firebaseから}",
      databaseURL: "{Firebaseから}",
      projectId: "{Firebaseから}",
      storageBucket: "{Firebaseから}",
      messagingSenderId: "{Firebaseから}"
    };
    firebase.initializeApp(config);
  </script>
</body>

ここの設定は Firebase console の “ウェブアプリに Firebase を追加” から取得できます。

src/Home.vue

Monacaのテンプレートに入っているHome.vueを変更しています。
イメージは起動時に匿名認証を行っているか判定し、行っていなければダイアログでユーザー属性を入力させ、Firebaseで匿名認証させます。
認証時の属性情報は Firebase の Cloud Firestore に保存します。
あとで気づいたのですが Cloud Firestore は現在ベータ版で、Webで”Firebase database”で検索すると Realtime Database の情報も出てくるので混乱しました。

<template id="main">
  <v-ons-page>
    <div style="display: table; height: 100%; width: 100%;">
      <div style="display: table-cell;text-align: center;vertical-align: middle;">
        <p>
          Hi!
        </p>
        <p>
          <v-ons-button modifier="quiet" style="background-color: transparent;" @click="signOut">
            登録解除
          </v-ons-button>
        </p>
      </div>
    </div>

    <v-ons-modal style="background-color: #FFFFFF;" :visible="showSignInModal">
      <div style="text-align: center; color: #333333;">
        <p>
          あなたのことを教えてください。
        </p>
        <p>
          <v-ons-select class="signInSelect" v-model="gender">
            <option disabled="disabled" value="">性別</option>
            <option v-for="(item, index) in genders" :key="index" :value="item.value">
              {{ item.text }}
            </option>
          </v-ons-select>
        </p>
        <p>
          <v-ons-select class="signInSelect" v-model="generation">
            <option disabled="disabled" value="">年齢</option>
            <option v-for="(item, index) in generations" :key="index" :value="item.value">
              {{ item.text }}
            </option>
          </v-ons-select>
        </p>
        <v-ons-button modifier="outline" class="signInBtn" @click="signIn">次へ</v-ons-button>
      </div>
    </v-ons-modal>

    <v-ons-modal :visible="showLoading">
      <p style="text-align: center">
        Loading <v-ons-icon icon="fa-spinner" spin></v-ons-icon>
      </p>
    </v-ons-modal>
  </v-ons-page>
</template>

<script>
export default {
  data() {
    return {
      showLoading: false,
      showSignInModal: false,
      gender: "",
      genders: [
        { text: "男性", value: "MALE" },
        { text: "女性", value: "FEMALE" },
        { text: "その他", value: "OTHER" }
      ],
      generation: "",
      generations: [
        { text: "10歳未満", value: "_10" },
        { text: "10-19歳", value: "10_19" },
        { text: "20-29歳", value: "20_29" },
        { text: "30-39歳", value: "30_39" },
        { text: "40-49歳", value: "40_49" },
        { text: "50-59歳", value: "50_59" },
        { text: "60-69歳", value: "60_69" },
        { text: "70-79歳", value: "70_79" },
        { text: "80歳以上", value: "80_" }
      ]
    };
  },
  methods: {
    signIn(event) {
      this.showLoading = true;
      // 匿名ユーザサインイン
      firebase
        .auth()
        .signInAnonymously()
        .catch(function(error) {
          alert(error.message);
        });
    },
    signOut(event) {
      firebase.auth().signOut();
    }
  created() {
    var self = this;
    firebase.auth().onAuthStateChanged(function(user) {
      if (user) {
        const firestore = firebase.firestore();
        firestore.settings({ timestampsInSnapshots: true });
        firestore
          .collection("users")
          .doc(user.uid)
          .set({
            gender: self.gender,
            generation: self.generation
          })
          .then(function() {
            self.showLoading = false;
            self.showSignInModal = false;
          })
          .catch(function(error) {
            console.error("Error writing document: ", error);
            alert("登録に失敗しました");
          });
      } else {
        self.showSignInModal = true;
      }
    });
  }
};
</script>

こんな感じでユーザー情報がFirebase の Authentication と Database(Cloud Firestore) に登録されます。

user.uid をキーにアプリデータをFirebaseのDatabaseに保存すれば、アンケートみたいなものは簡単に作れそうです。
もちろん要件によってはiOS、Androidを別々に開発すべきですが、要件を満たせるならハイブリッドで開発するのは十分にアリだと思います。
ただ、バックグラウンド処理やBLE周りとかどうなんでしょう?

Vue.js と jQuery を共存させる

DOM操作、イベント処理などは Vue.js に任せるとして、
アニメーションなど jQuery で簡単に実装出来ることは jQuery でやってしまおうと思い、
Vue.js で jQuery を使う。

思想的には共存させない方が良いのかな?と思いますが一旦効率重視で。

まずはインストール。

$ npm install -D jquery
$ npm install -D @types/jquery

使う時は下記のような感じです。

import $ from "jquery";

$("html,body").animate({scrollTop: 0}, 500, "swing");

型定義が合わないのか、
$(“.xxx”).offset().top
の offset() が見つからないとかエラーが出てしまい、
// @ts-ignore
で抑制してしまったり、少しもどかしい感じです。

Vue.js(TypeScript) でグローバル変数を使う

Vue.js で開発していてアプリケーション内でデータを共有したくなりました。
いくつか方法があると思います。
・Vuex
・Cookie
・LocalStrage
・グローバル変数

Vuex とかが正しいのかもしれないですが、少し冗長な気がしたし、Cookie だとデータの保存期間の制御など要件に合わすのが大変そうだった。
アプリが起動しているときだけ保持して、且つ複数のウィンドウでアプリが起動されウィンドウごとに別のデータとして管理する想定です。
もっとも簡単そうですし要件にも合うのでグローバル変数を使う方針にしました。

今回はプラグインとして実現することにしました。
まずはプラグインと、データを保持するクラスの定義です。

import _Vue from "vue";

export default function AppDataPlugin<AppDataPluginOptions>(Vue: typeof _Vue, options?: AppDataPluginOptions): void {
    Vue.prototype.$appData = new AppData();
}

export class AppDataPluginOptions {
}

export class AppData {
    public dataAaa: string = "";
    public dataBbb: string = "";
}

VisualStudioCode などのためにコード型定義ファイルも用意。
AppDataPlugin.d.ts とかのファイル名で src 直下に保存

import AppDataPlugin, { AppData } from "./components/AppDataPlugin";
declare module 'vue/types/vue' {
  interface Vue {
    $appData: AppData
  }
}

使う側は下記のような感じです。

<script lang="ts">
import Vue from "vue";
import AppDataPlugin from "./AppDataPlugin";

Vue.use(AppDataPlugin);

export default Vue.extend({
    ・・・
    methods: {
        xxx() {
            console.log(this.$appData.dataAaa);
        }
    }
    ・・・
});

Vue.js(TypeScript)でvalidation

Vue.js(TypeScript) のプロジェクトで validation を行いたい。
表示はcss フレームワーク Bulma を拡張したUIコンポーネントの Buefy を使う。
validation には VeeValidation を使う。

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

$ npm install -D buefy node-sass sass-loader vee-validate

buefy を使う

buefyのアイコンを使うので、index.htmlに下記追加。

 <link rel="stylesheet" href="//cdn.materialdesignicons.com/2.0.46/css/materialdesignicons.min.css">

VeeValidate を使う

<template>
  <div>
    <b-field label="メールアドレス"
      :type="errors.has('email') ? 'is-danger': ''"
      :message="errors.has('email') ? errors.first('email') : ''">
        <b-input
            name="email"
            v-model="email"
            v-validate="'required|email'"
            placeholder="メールアドレス">
        </b-input>
    </b-field>
  </div>
</template>
・・・
<script lang="ts">
import Vue from "vue";
import Buefy from "buefy";
import VeeValidate from "vee-validate";

Vue.use(Buefy);
Vue.use(VeeValidate);
・・・

VeeValidate を日本語化

エラーメッセージが英語なので日本語化する。
ローカライズ用のメッセージファイルの読込と、フィールド名の日本語化を行う。

・・・
        <b-input
            name="email"
            v-model="email"
            v-validate="'required'"
            data-vv-as="メールアドレス"
            placeholder="メールアドレス">
・・・
<script lang="ts">
import Vue from "vue";
import Buefy from "buefy";
import VeeValidate, { Validator } from "vee-validate";
// @ts-ignore: implicitly has an 'any' type.
import VeeValidateJa from "vee-validate/dist/locale/ja";

Vue.use(Buefy);
Validator.localize("ja", VeeValidateJa);
Vue.use(VeeValidate);
・・・

フィールド名の日本語化は、data-vv-as を追加してフィールドごとに対応。
attributesファイルを用意しても出来るようです。
https://baianat.github.io/vee-validate/guide/localization.html#aliases

メッセージファイルの読込でエラーが出たので、ts-ignoreで対応、、、

入力チェックのタイミングを変更

メールアドレスのチェックなどで入力を始めるといきなりエラーとなるので、入力チェックを行うタイミングをフォーカスが外れた時にする。

Vue.use(VeeValidate, {
  events: "blur"
});

ボタンクリックなどで入力チェック

ログイン処理など実際にはボタンクリックなどで全体の入力チェックを行うと思うので、その方法。

・・・
<a class="button is-primary is-fullwidth" @click="login()">ログイン</a>
・・・
  methods: {
    login() {
      this.$validator
        .validateAll()
        .then(result => {
          if (!result) {
            console.log(this.$validator.errors.all());
            return;
          }
        })
        .catch(() => {
          console.log(this.$validator.errors.all());
        });
    }
  }

入力チェックでエラーがあってもcatchではなくthenが呼ばれる。入力エラーがあるかはresultで判定する。

vue-cli で作成したプロジェクトの起動時のポートを変更

vue-cli 作成したVue.jsのプロジェクトをnpm run devで起動すると、ポート:8080で起動する。
8080が使われてると、8081で起動したりと賢いのですが、別のポートで起動したい時に下記のように設定する。

config/index.js

module.exports = {
  dev: {
・・・
    host: 'localhost',
    port: 8079, ←コレ

Vue.js + TypeScript で外部ライブラリを使用したいが型定義がない

型定義があるライブラリはここを参照。

ただ、そもそもない時の回避方法を考える。

  • 無視する
    該当の処理の前に、
    // @ts-ignore:

  • 型定義を作る
    ただしく定義するのは大変なので、
    declare module “{パッケージ名}” を記載した .d.ts を作成

  • declare
    declare var xxx: any;
    でany型として宣言してしまう。

AWSにアップロード

Vue.js + TypeScript で開発していたアプリケーションをサーバーにアップロードするシェルを作る。
AWSは、CloudFront、S3 を使用する。
クライアントPCにはAWS-CLIのインストールと、アカウントのプロファイルを作成しておく。

#!/bin/sh
npm run build
aws s3 sync ./dist/ s3://{バケット名}/sample/ --include "*" --profile={プロファイル名}
aws cloudfront create-invalidation --distribution-id {CloudfrontのID} --paths "/sample/*" --profile={プロファイル名}

aws cloudfront create-invalidation でCloudFrontのキャッシュを削除すると確認が早くできる。