Firebaseをwebで使う(Hosting, Authentication, Realtime Database, Storage)

(2017-04-16)

Firebaseとは

GoogleのmBaaS。Android/iOSアプリの開発に使う認証、データストア、クラッシュレポート、分析、通知、広告などなど全部入りサービス。 今年のGoogleI/Oでも毎時間のように Firebaseのセッションがあって大分推している印象。

基本的にはアプリで使うのだけれど、webで使える機能も結構ある。今回は

  • Hosting
  • Authentication
  • Realtime Database
  • Storage

を使ってみる。

料金

プランは無料のSPARKと25ドル/月のFLAME、従量課金のBLAZEがある。 試す分にはSPARKで十分だけど、Realtime Databaseの同時接続数が100なので注意。

セットアップ

firebase-cliをインストール、ログインして初期化する。

$ npm install -g firebase-tools
$ firebase login
$ mkdir firebase-chat && cd firebase-chat
$ firebase init
...
? What Firebase CLI features do you want to setup for this folder? 
❯◉ Database: Deploy Firebase Realtime Database Rules
 ◉ Functions: Configure and deploy Cloud Functions
 ◉ Hosting: Configure and deploy Firebase Hosting sites

? What Firebase project do you want to associate as default? *****

? What file should be used for Database Rules? database.rules.json

? Do you want to install dependencies with npm now? Yes

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

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

✔  Firebase initialization complete!

$ ls
database.rules.json	firebase.json		functions		public

firebase.jsonはこんな感じ。

$ cat firebase.json
{
  "database": {
    "rules": "database.rules.json"
  },
  "hosting": {
    "public": "public",
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}

ローカルでサーバーを立ち上げて確認する。

$ firebase serve
Server listening at: http://localhost:5000

$ curl http://localhost:5000 # Firebase SDK loaded with auth, database, messaging, storage

Hosting

静的サイトのホスティング。もちろん独自ドメインも使える。

$ firebase deploy --only hosting
...
✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/*****/overview
Hosting URL: https://*****.firebaseapp.com

Authentication

ユーザー認証。Googleだけではなく、TwitterやFacebook、Githubといったプロバイダや、メールとパスワードでの認証が用意されていて、 コンソールから有効にする必要がある。

実装はFirebase SDKで自分でやるか、

const provider = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithPopup(provider).then((result) => {
    console.log(`sign in successfully. ${result.user.displayName}`)
}).catch((error) => {
    console.log(`fail to sign in. ${error.message}`)
});

FirebaseUI Authを使う。

<script defer src="https://cdn.firebase.com/libs/firebaseui/1.0.1/firebaseui.js"></script>
<link type="text/css" rel="stylesheet" href="https://cdn.firebase.com/libs/firebaseui/1.0.1/firebaseui.css" />

<div id="firebaseui-auth-container"></div>
// FirebaseUI config.
var uiConfig = {
  signInOptions: [
    // Leave the lines as is for the providers you want to offer your users.
    firebase.auth.GoogleAuthProvider.PROVIDER_ID
  ],
  callbacks: {
    signInSuccess: function(currentUser, credential, redirectUrl) {
      // リダイレクトさせない
      return false;
    }
  },
  // Terms of service url.
  tosUrl: '<your-tos-url>'
};

// Initialize the FirebaseUI Widget using Firebase.
var ui = new firebaseui.auth.AuthUI(firebase.auth());
// The start method will wait until the DOM is loaded.
ui.start('#firebaseui-auth-container', uiConfig);

こんな感じにボタンが並ぶ。

Firebase authentication

onAuthStateChanged()でsign in/outをハンドリングでき、

firebase.auth().onAuthStateChanged((user) => {
    if (user) {
      console.log(`${user.displayName} sign in`);
    } else {
      console.log('sign out');
    }
  }, (error) => {
    console.log(error);
  }
);

currentUserでsign inしてるユーザーを取得できる。

if(firebase.auth().currentUser){
  console.log(firebase.auth().currentUser.displayName);
}else{
  // need to sign in
}

Realtime Database

NoSQLなデータベース。直接読み書きするのではなく、 ローカルにデータを保存してリアルタイムに同期するため一時的にオフライン状態になっても読み書きできる。

読み書き

データベースへの書き込みと更新。refでusers/1のように参照を取って操作する。 push()hoge/-Khp36CCygw5AI6G8L1Bのようなユニークなキーを発行することができ、これは時系列にソートされるようになっている。

const database = firebase.database();

for(let i = 0; i < 10; i++){
    
    const newHogeRef = database.ref('hoge').push();
    console.log(`newHogeRef: ${newHogeRef.toString()}`);
    
    newHogeRef.set({
        idx: i,
        aaa: "bbb123",
    });

    newHogeRef.update({
        aaa: "bbb456",
        eee: "fff"
    });
}
newHogeRef: https://test-3a363.firebaseio.com/hoge/-Khp36CCygw5AI6G8L1B
newHogeRef: https://test-3a363.firebaseio.com/hoge/-Khp36CLyJ9BQVefW-l5
newHogeRef: https://test-3a363.firebaseio.com/hoge/-Khp36CMs9-jJoKUUgr0

on()で value eventを拾うと、 呼んだときとデータに変更があったときにsnapshotが取得できる。

database.ref("hoge").orderByKey().limitToLast(3).on("value", (snapshot) => {
    snapshot.forEach((data) => console.log(data.val()));
});
...
Object {aaa: "bbb456", eee: "fff", idx: 7}
Object {aaa: "bbb456", eee: "fff", idx: 8}
Object {aaa: "bbb456", eee: "fff", idx: 9}

アクセス権限・バリデーション

firebase.jsonで指定しているdatabase ruleファイル(database.rules.json)でルールを設定する。 デフォルトで認証していれば読み書きできる設定になっている。

{
  "rules": {
    ".read": "auth != null",
    ".write": "auth != null"
  }
}

read/writeだけではなくバリデーションの設定もこんな感じでできる。 これはusers/${ユーザーのuid}への読み書きを本人のみができるようにするもの。

{
  "rules": {
    "users": {
      "$uid": {
        ".read": "$uid === auth.uid",
        ".write": "$uid === auth.uid",
        ".validate": "newData.hasChildren(['age', 'name']) && newData.child('age').isNumber() && newData.child('age').val() >= 0 && newData.child('age').val() < 200 && newData.child('name').isString() && newData.child('name').val().length < 50"
      }
    }
  }
}
$ firebase deploy --only database

上の設定を適用したデータベースに読み書きしてみる。

const uid = firebase.auth().currentUser.uid;

database.ref(`users/${uid}`).set({
    age: 20,
    name: "taro"
});

// ok: Object {age: 20, name: "taro"}
database.ref(`users/${uid}`).on("value", (snapshot) => {
    console.log(snapshot.val()); // Object {age: 20, name: "taro"}
});

以下のような不正なデータや不正なキーに書き込もうとすると PERMISSION_DENIED: Permission deniedになる。

// ageがおかしい
database.ref(`users/${uid}`).set({
    age: "aaaa",
    name: "jiro"
});

// 本人じゃない
database.ref(`users/hogehoge`).set({
    age: 20,
    name: "jiro"
});

ちなみに、さっきまでアクセスできていたhogeにもアクセスできなくなっている。

// PERMISSION_DENIED: Permission denied
database.ref("hoge").orderByKey().limitToLast(3).on("value", (snapshot) => {
    snapshot.forEach((data) => console.log(data.val()));
});

これを解決するためにrulesのルートにauth != nullの設定を入れてしまうと、 浅い階層のルールが深い階層のルールより優先される ためusersのread/writeの設定が無効になってしまうので注意。ただしvalidateはそれぞれの階層でチェックされる。

{
  "rules": {
    ".read": "auth != null",
    ".write": "auth != null",
    "users": {
      "$uid": {
        ".read": "$uid === auth.uid",
        ".write": "$uid === auth.uid",
        ".validate": "newData.hasChildren(['age', 'name']) && newData.child('age').isNumber() && newData.child('age').val() >= 0 && newData.child('age').val() < 200 && newData.child('name').isString() && newData.child('name').val().length < 50"
      }
    }
  }
}

Storage

画像などを保存しておくために使う。 Realtime Databaseと同様、 ネットワーク品質が良くない環境でも使えるように、処理が中断されても途中から処理を再開するようになっている。 裏側ではGoogle Cloud Storageが使われている。

アップロード

<input type="file" id="file-upload">
const storage = firebase.storage();

const inputFile = document.getElementById('file-upload');

inputFile.addEventListener('change', (e) => {
  const files = e.target.files;
  const user = firebase.auth().currentUser;
  if(user){
    const ref = storage.ref(`images/${user.uid}`);
    const uploadTask = ref.put(files[0]);
  }
}, false);

中断、再開、キャンセル。

uploadTask.pause();
uploadTask.resume();
uploadTask.cancel();

アップロードの状態はstage_changed eventで確認し、完了するとダウンロードURLを取得できる。

uploadTask.on('state_changed', (snapshot) => {

  const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
  console.log('Upload is ' + progress + '% done');

  switch (snapshot.state) {
    case firebase.storage.TaskState.PAUSED: // or 'paused'
      console.log('Upload is paused');
      break;
    case firebase.storage.TaskState.RUNNING: // or 'running'
      console.log('Upload is running');
      break;
  }
}, (error) => {
  // Handle unsuccessful uploads
  console.log(error);
}, () => {
  // Handle successful uploads on complete
  // For instance, get the download URL: https://firebasestorage.googleapis.com/...
  console.log(uploadTask.snapshot.downloadURL);
});

アクセス制限・バリデーション

Realtime Databaseと同様にアクセス制限やバリデーションをかけることができる。 設定はコンソールから。

Storageのルール

service firebase.storage {
  match /b/*****.appspot.com/o {
    match /images/{imageId} {
      // Only allow uploads of any image file that's less than 5MB
      allow write: if request.resource.size < 5 * 1024 * 1024
                   && request.resource.contentType.matches('image/.*')
                   && request.auth != null;
      allow read: if request.auth != null;             
    }
  }
}

ダウンロード

getDownloadURL()でURLを取得する。

<img id="myimg">
const ref = storage.ref(`images/${user.uid}`).getDownloadURL().then((url) => {
  
  const img = document.getElementById('myimg');
  img.src = url;

}).catch((error) => {

  switch (error.code) {
    case 'storage/object-not-found':
      console.log("not found");
      break;

    default:
      console.log(error);
  }
});;