vue.jsとFirebaseのRealtime DatabaseとAuthorization機能を利用したSlackクローンの構築を複数回にわけて行っています。8回目となるファイルのアップロード機能の実装を行っています。ファイルはFirebaseのCloud Storageに保存します。input要素によるファイルの選択ではなくSlock Cloneのメイン画面にファイルをドラッグ&ドロップすることでファイルをアップロードできるようにしていきます。

Cloud Storageの設定

FirebaseのCloud Storageを利用するためには初期設定を行う必要があります。Firebaseの管理コンソールにログインし、左側のメニューの赤丸にあるStorageをクリックします。

クリック後画面上部にある始めるボタンをクリックします。

Cloud Storageの初期画面
Cloud Storageの初期画面

始めるボタンを押すとCloud Storageのセキュリティの保護ルールの画面が表示されるので次へボタンをクリックしてください。デフォルトでは認証ユーザによるファイルの読み込みと書き込みが許可されています。

セキュリティの保護ルール画面
セキュリティの保護ルール画面

Cloud Storageのロケーション設定です。asia-northeast1を選択して完了ボタンを押します。

ファイル保存のロケーション
ファイル保存のロケーション

アップロードしたファイル一覧表示される画面が表示されます。

ファイル一覧表示画面
ファイル一覧表示画面

以上でCloud Storageの初期設定は完了です。

ファイルのアップロード設定

ファイルのアップロードの動作確認を行うためにinput要素をメイン画面のメッセージ送信の枠のタグの下に一時的に追加します。動作確認後削除するのでinput要素を入れる場所はどこでも大丈夫です。input要素を追加すると同時にchangeイベントも追加し、fileUploadメソッドを設定します。


    <div class="bg-gray-100 p-2">
      <button
        class="bg-green-900 text-sm text-white font-bold py-1 px-2 rounded"
        @click="sendMessage"
      >送信</button>
    </div>
  </div>
  <input type="file" @change="fileUpload" />
</div>
changeイベントを設定するとファイルを選択するとイベントが発火します。

iput要素追加後は、ファイル選択のフォームが表示されます。

input要素追加後の画面
input要素追加後の画面

HTMLタグの追加が完了したら、fileUploadメソッドの追加を行いますが、その前にCloud Storageのメソッドが利用できるようにfirebase/storageをimportします。


import firebase from "firebase/app";
import "firebase/auth";
import "firebase/storage"; //追加

import追加後、vue.jsにfileUploadメソッドを追加します。取得したファイルはfirebase.storage().refで保存する場所と名前を指定してputメソッドでファイルを指定した場所に保存します。ここではimagesの下にファイル名で保存します。

input要素で選択したファイルの情報はevent.target.filesから取得することができます。複数のファイルを選択すと配列でファイル情報が取得できますが今回は1つ目のファイル情報のみ取得するためfiles[0]になっています。

fileUpload(event) {
  let file = event.target.files[0];
  const storageRef = firebase.storage().ref("images/" + file.name);
  storageRef.put(file).then(() => {
    console.log('uploaded file')
  });
}
ファイルの保存等についてはCloud Storageの公式ドキュメントにアップされている動画を見ることをおすすめします。ファイルのプログレスバーの設定まで丁寧に説明されています。

ブラウザの画面上からファイルを選択すると同時にchangeイベントによりfileUploadが実行されアップロードが完了します。Chomeのデベロッパーツールのコンソールにuploaded fileが表示されていることを確認し、FirebaseのコンソールでStorageにアクセスし、ファイルがアップされているか確認します。

imageフォルダが表示されるのを確認してください。

imageフォルダの確認
imageフォルダの確認

imagesフォルダをクリックするとアップロードしたファイルが表示されることを確認してください。

アップロードファイルの確認
アップロードファイルの確認

数行のラインを記述するだけで簡単にCloud Storageにファイルをアップできることが確認できました。

ファイルのアップロードが完了したら一時的に作成していたinput要素を削除します。


<input type="file" @change="fileUpload" />

Drag&Dropによるファイルのアップロード

ファイルのDrag&Dropによるファイルアップロードの機能追加は下記の文章を参考にして行っています。

DragEnter、Leaveイベントの確認

メイン画面にファイルをドラッグするとイベントが発火されるかどうか確認するためにdragenterイベントの設定を行います。

mainタグにdragenterイベントを追加し、dragEnterメソッドを設定します。


<main class="h-full overflow-y-scroll relative" @dragenter="dragEnter">   

vue.js側のメソッドにdragEnterメソッドを追加します。dragenterイベントが発火されるとコンソールにメッセージを表示させます。


dragEnter() {
  console.log("enter");
}

ブラウザをからメイン画面にファイルをドラッグしてください。Chromeの開発ツールであればConsoleにenterのメッセージが表示されます。

drageenterイベントの動作確認ができたのでドラッグしたファイルがその領域から離れた時に発火するDragLeaveイベントも追加し動作確認します。


<main class="h-full overflow-y-scroll relative" @dragenter="dragEnter" @dragleave="dragLeave">   

dragleaveイベントに設定したdragLeaveメソッドを追加します。


dragEnter() {
  console.log("enter");
},
dragLeave() {
  console.log("leave");
}

設定後、メイン画面の領域にファイルをドラッグするとコンソール画面にenterが表示されその領域から外にでるとleaveが表示されることを確認してください。

ファイルアップロード画面の表示

ファイルをドラッグするとメイン画面の上にファイルアップロード用のオーバーレイを表示できるようにmainタグの下に下記のタグを追加します。


<div class="h-full bg-white z-10 absolute w-full flex justify-center items-center">
  <p class="font-bold text-4xl">Slack Cloneへアップロードする</p>
</div> 

ブラウザで確認するとメイン画面にのみ白い幕が表示されその幕の上に”Slack Cloneへアップロードする”の文字列が表示されます。

ファイルアップロード
ファイルアップロード

このオーバーレイが表示されるタイミングをdragenter, dragleaveイベントを利用して制御します。新たにvue.jsにデータプロパティのfile_uplod_overlayを追加します。

追加したデータプロパティとv-showを組み合わせてオーバーレイの表示・非表示を切り替えます。


data() {
  return {
    user: "",
//略
    file_uplod_overlay: false,
  };

dragEnterイベントではオーバーレイを表示させるのでfile_upload_overlayをtrueとしdragLeaveイベントではオーバーレイを非表示にするのでfile_upload_overlayをfalseにします。


dragEnter() {
  this.file_uplod_overlay = true;
},
dragLeave() {
  this.file_uplod_overlay = false;
},

オーバーレイのdivタグにv-showを追加します。


<div
  class="h-full bg-white z-10 absolute w-full flex justify-center items-center"
  v-show="file_uplod_overlay"
> 

ブラウザを起動してメイン領域にファイルをドラッグすると一瞬オーバーレイが表示されますがすぐに非表示になり期待した動作にはなりません。

dragEnterメソッドとdragLeaveメソッドの中でコンソールへのメッセージ(Enter, Leave)を表示できるようにしておくとEnterの後ファイルをその領域から出していないにもかかわらずすぐにLeaveが表示されることがわかります。

これはDragイベントを設定したdivを親要素とするとオーバーレイが子要素にあたり、DragEnterイベントで親要素に入ると同時にオーバーレイ子要素の領域にも入ることになるので親要素から出たとみなされすぐにdragleaveイベントが発火されるためです。

このイベントを避けるために子要素にstyle=”pointer-events: none”を設定します。


<div
  class="h-full bg-white z-10 absolute w-full flex justify-center items-center"
  v-show="file_uplod_overlay"
  style="pointer-events: none"
> 

追加後にファイルをメイン領域にドラッグするとオーバーレイが表示され、その領域から離れるとオーバーレイが非表示になることが確認できます。

Dropイベントの動作確認

ファイルをメイン領域にドラッグし、オーバーレイが表示されている状態でファイルをドロップするとドロップした画像が画面全体に表示されます。これが通常の動作です。

ファイルを領域にドロップした時にファイル情報を取得できるようにdropイベントを追加します。dropイベントを利用するためには一緒にdragoverイベントも追加する必要があります。またそれらのイベントにはデフォルトの動作を停止させるためpreventを設定します。

dropイベントにはドロップしたファイル情報を操作するためdropFileメソッドを設定します。


<main
  class="h-full overflow-y-scroll relative"
  @dragleave="dragLeave"
  @dragenter="dragEnter"
  @drop.prevent="dropFile"
  @dragover.prevent
> 

dropFileメソッドのなかでは動作確認ためdropイベントが発火したらコンソールにメッセージを表示させます。


dropFile() {
  console.log("drop file");
}

ブラウザを利用してファイルをドラッグ&ドロップしたら画像が表示されるのではなくコンソールにdrop fileが表示されることを確認してください。

ファイルのアップロード確認

ファイルのアップロードについては本文書の最初に確認したのでドロップしたファイルをCloud Storageにアップロードできるように設定を行います。


dropFile() {
  console.log("drop file");
  const file = event.dataTransfer.files[0];
  const storageRef = firebase.storage().ref("images/" + file.name);
  storageRef.put(file);
}
ドロップしたファイル情報はevent.dataTransfer.filesに保存されています。複数のファイルが一度にドロップできるためfilesは配列になっていますが、本アプリケーションではファイルを1つだけアップデートするためfiles[0]を指定しています。

Firebaseのコンソールでドロップしたファイルが保存されているか確認してください。ここではvue_logo.svgファイルをアップしています。

Firebaseのコンソールで追加したファイル確認
Firebaseのコンソールで追加したファイル確認

ドラッグ&ドロップでファイルがアップロードできることが確認できました。

ファイルメッセージの追加画面(モーダルウィンドウ)

ファイルをアップロードすることができましたが、ファイルと一緒にメッセージも追加できるようにアップロード用のモーダルウィンドウを作成します。

アップロード用のモーダルウィンドウはオーバーレイにファイルをドロップすると表示されるように作成していきます。

オーバーレイのdivタグの下に以下のHTMLを追加します。一見複雑に見えますがclassを利用して装飾を行っているだけで特別なことは行っていません。


<div
  class="z-10 fixed top-0 left-0 h-full w-full flex items-center justify-center"
  style="background-color:rgba(0,0,0,0.5)"
>
  <div class="z-20 bg-white text-gray-900 w-1/3 rounded-md" @click.stop>
    <div class="flex flex-col p-6">
      <div class="flex justify-between items-center">
        <h2 class="text-3xl font-black leading-loose">ファイルをアップロードする</h2>
        <span class="text-4xl">×</span>
      </div>
      <div class="my-3">
        <textarea
          class="w-full rounded border-gray-900 border-solid border p-3"
          placeholder="ファイルに関するメッセージを追加する"
        />
      </div>
      <div class="bg-gray-200 p-3 border border-gray-400 rounded mb-4">
        <div class="bg-white p-3">
          <span class="font-bold text-xl"></span>
        </div>
      </div>
      <div class="flex justify-end items-center">
        <button class="px-8 py-2 rounded bg-green-900 font-bold text-white">アップロード</button>
      </div>
    </div>
  </div>
</div>

ブラウザで確認するとファイルアップロード画面が表示されます。

ファイルアップロード画面表示
ファイルアップロード画面表示

このファイルアップロード画面がオーバーレイにファイルをドロップした後に表示できるようにvue.jsに表示・非表示に利用するデータプロパティのfile_upload_modalを追加します。


data() {
  return {
    user: "",
//略
    file_uplod_overlay: false,
    file_upload_modal: false
  };
},

追加したモーダルウィンドウにv-show=”file_upload_modal”を追加します。

ファイルをオーバーレイにドロップした後に表示されるようにするため、dropFileメソッドを更新します。

ファイルアップロードの処理はコメントアウトし、file_upload_modalをtrueにしオーバーレイのfile_upload_overlayをfalseにします。


dropFile() {
  console.log("drop file");
  // const file = event.dataTransfer.files[0];
  // const storageRef = firebase.storage().ref("images/" + file.name);
  // storageRef.put(file);
  this.file_upload_modal = true;
  this.file_uplod_overlay = false;
}

設定完了後、ファイルをドロップするとファイルアップロードのモーダルウィンドウが表示されることを確認してください。

ファイルアップロード画面表示
ファイルアップロード画面表示

モーダルウィンドウが表示されたままになるためXボタンまたはモーダルウィンドウの外側にクリックするとウィンドウが閉じるようにclosefileUploadModalメソッドをvue.jsに追加します。


closefileUploadModal() {
  this.file_upload_modal = false;
}

モーダルウィンドウの背景の黒い幕に@click=”closefileUploadModal”を設定します。


<div
  class="z-10 fixed top-0 left-0 h-full w-full flex items-center justify-center"
  style="background-color:rgba(0,0,0,0.5)"
  v-show="file_upload_modal"
  @click="closefileUploadModal"
>

画面に表示されているXボタンにも@click=”closefileUploadModal”を設定します。


<span class="text-4xl" @click="closefileUploadModal">×</span>

ファイルをドロップして表示されるモーダルウィンドウが黒い幕とXをどちらを押しても非表示になるか確認してください。

ファイルメッセージの入力設定

textareaに入力した文字列をvue.js側で処理できるようにデータプロパティfile_messageを追加します。


data() {
  return {
    user: "",
//略
    file_uplod_overlay: false,
    file_upload_modal: false,
    file_message: ''
  };

データプロパティfile_messageを追加後textareaタグにv-model=”file_message”を追加します。


<textarea
  class="w-full rounded border-gray-900 border-solid border p-3"
  placeholder="ファイルに関するメッセージを追加する"
  v-model="file_message"
/> 

これでtextareaに入力した文字列をvue.js側で処理できるようになりました。

ファイル情報を保存

アップロードの動作確認をした際はドロップ後即座にアップしましたが、ファイル名などの情報をファイルアップロードのモーダルウィンドウに表示されるためデータプロパティfileを追加して一時的に保存します。


data() {
  return {
    user: "",
//略
    file_uplod_overlay: false,
    file_upload_modal: false,
    file_message: '',
    file: ''
  };

dropFileメソッドを更新し、ファイル情報を追加したデータプロパティfileに保存します。


dropFile() {
  console.log("drop file");
  this.file = event.dataTransfer.files[0];
  this.file_upload_modal = true;
  this.file_uplod_overlay = false;
},

ドロップしたファイルの名前をファイルアップロードのモーダルウィンドウに表示させるためにデータプロパティfileを利用します。

ファイル名を表示させるのはtextareaの下にあるspanタグの中です。v-textを利用しています。


<div class="bg-gray-200 p-3 border border-gray-400 rounded mb-4">
  <div class="bg-white p-3">
    <span class="font-bold text-xl" v-text="file.name"></span>
  </div>
</div>

ブラウザを使ってファイルをドロップするとドロップしたファイルが表示されていることがわかります。

ドロップしたファイル名が表示される
ドロップしたファイル名が表示される

アップロード処理の追加

モーダルウィンドウからファイルをアップロードするための処理を追加します。アップロードボタンにclickイベントを追加し、fileUploadメソッドを設定します。


<button
  class="px-8 py-2 rounded bg-green-900 font-bold text-white"
  @click="fileUpload"
>アップロード</button>

動作確認のためfileUploadボタンを押すとファイル情報とメッセージがコンソールログに表示させるように設定します。


fileUpload() {
  console.log(this.file_message);
  console.log(this.file);
}

ブラウザにファイルをドロップします。メッセージを入力しアップロードボタンを押します。

ファイルに関するメッセージを入力
ファイルに関するメッセージを入力

Chromeの開発ツールのコンソールにメッセージとファイル情報が表示されることを確認してください。

メッセージとファイル情報
メッセージとファイル情報

ファイル情報が取得できたのでファイルのアップロードはこれまで確認した方法を使ってアップロードすることができます。


fileUpload() {
  const storageRef = firebase.storage().ref("images/" + this.file.name);
  storageRef.put(this.file);
}

ファイルをアップロードするだけではなくメッセージとアップロードしたファイルを紐付ける必要があるためこれだけで処理は完了ではありません。

アップロードしたファイルの保存先URL

アップロードしたファイルをメッセージと紐付けるためにはアップロードしたファイルの保存先のURLが必要となります。メッセージの中にファイルのURLを保存します。

ファイルのURLは、アップロード完了後にファイルの保存先の参照先の情報を持つstorageRefのgetDownloadURLメソッドからURLを取得することができます。


storageRef.getDownloadURL().then(url => {
 this.url = url
});

このメソッドを利用するためにはファイルのアップロードが完了していることが必要です。

ファイルを保存するputメソッドを実行するとuploadTaskが戻されアプロードの状態を確認することができます。このuploadTaskを利用してファイルのアップロード完了を確認します。


const storageRef = firebase.storage().ref("images/" + this.file.name);
const uploadTask = storageRef.put(this.file);

uploadTaskのonメソッドによりイベントを監視することができます。onメソッドは3つのコールバック関数を持っています。


var next = function(snapshot) {};
var error = function(error) {};
var complete = function() {};

// The first example.
uploadTask.on(
    "state_changed,
    next,
    error,
    complete);

1つ目のnextではファイルのアップロード状況を監視できます。後ほど行いますがファイルのアップロードの進行状況を確認することができるためプログレスバーに活用することができます。

2つ目はエラーを取得し、3つ目はアップロードが完了した時に実行されます。3つ目のコールバックを利用して完了後の処理を行います。

3つ目の完了だけを知りたい場合は下記のように記述することが可能です。


uploadTask.on(
    firebase.storage.TaskEvent.STATE_CHANGED,
    null,
    null,
    function() {
      console.log('upload complete!');
    });

そのほかの記述方法については公式ドキュメントを確認してください。

本アプリケーションでは最初はerrorとcompleteを利用します。completeの中でurlを取得し、データプロパティurlに保存しsendMessageメソッドでメッセージを送ります。

sendMessageメソッドはダイレクトメッセージやチャネルメッセージで利用しているsendMessageを再利用します。

uploadTask.on(
  "state_changed",
  NULL,
  error => {
    console.log(error);
  },
  () => {
    storageRef.getDownloadURL().then(url => {
      this.url = url;
      this.sendMessage();
      this.fileUploadModal = false;
    });
  }
);

データプロパティurlを追加しておきます。


data() {
  return {
    user: "",
//略
    file_uplod_overlay: false,
    file_upload_modal: false,
    file_message: '',
    file: '',
    url: ''
  };

ファイルの保存先であるURLが取得できたので次はsendMessageを更新します。

sendMessageメソッドを更新する

アップロードしたファイルのURLとfileに関するメッセージをFirebaseのRealtime databaseに保存できるようにsendMessageを更新します。


sendMessage() {
  const newMessage = firebase
    .database()
    .ref("messages")
    .child(this.channel_id)
    .push();

  const key_id = newMessage.key;

  let content = "";

  if (this.url == "") {
    content = this.message;
  } else {
    content = this.file_message;
  }

  newMessage
    .set({
      key: key_id,
      content: content,
      user: this.user.email,
      url: this.url,
      createdAt: firebase.database.ServerValue.TIMESTAMP
    })

  this.url == "" ? this.message = "" : this.file_message = "";
  this.url = "";
},

urlに値があるかどうかで保存するメッセージがファイルのメッセージなのか通常のダイレクト、チャネルメッセージかのかを判別してcontentに保存しています。

メッセージには新たにurlを追加します。urlを追加したことによってデータベース側で何か設定をする必要はありません。

メッセージ作成が行った後、urlに値があるかどうかでfile_messageを空にするかmessageを空にするか判別し処理の最後にurlを空にしています。

アップロードしたファイルをメッセージに表示

メッセージを表示するループの中でメッセージのurlに値がある場合は画像として画面に表示できるように設定を行います。画像の幅は360pxにしています。


<div
  class="mt-2 mb-4 flex"
  v-for="message in messages"
  :key="message.key"
  style="pointer-events: none"
>
  <Avator :user="message.user" />
  <div class="ml-2">
    <div class="font-bold">{{ message.user }}</div>
    <div>{{ message.content }}</div>
    <div v-if="message.url">
      <img :src="message.url" width="360px" />
    </div>
  </div>
</div>
画像以外のファイルをアップロードすると画像は表示されません。画像以外とEXCELなどのファイルを区別する仕組みが必要となります。

ファイルアップロードの動作確認

アップロードするための必要最低限の設定が完了したので実際にファイルをアップロードしてみましょう。

ダイレクトメッセージでファイルを送る相手であるjack@example.co.jpを選択します。

ファイルをメイン画面にドラッグ&ドロップします。

ファイルをドロップ
ファイルをドロップ

メッセージ入力後、アップロードボタンをクリックします。

アップロードした画像がファイル一覧に表示
アップロードした画像がファイル一覧に表示

受け取ったjackからダイレクトメッセージで返事を送信。

ダイレクトメッセージで返信
ダイレクトメッセージで返信

Slack Cloneに新たに画像ファイルのアップロードが実装できました。

プログレスバーを表示する

ファイルのアップロードの進捗具合を確認するためにuploadTaskを利用してプログレスバー機能をつけることができます。

ファイルアップロードのモーダルウィンドウにプログレスバーのタグを追加します。


<div class="z-20 bg-white text-gray-900 w-1/3 rounded-md" @click.stop>
  <div class="w-full h-2">
    <div class="bg-green-900 h-full block" style="width:0%" ></div>
  </div>

デフォルトではwidthは0%にしているため表示されませんが動作確認で20%を設定すると下記のようにプログレスバーが表示されます。

プログレスバーの追加
プログレスバーの追加

プログレスバーのstyleのwidthのパーセンテージを変更することでバーの幅が変更できることがわかりました。

widthの値はファイルのアップロードの完了でuploadTaskを利用した時に1つ目のnullにしていたコールバッグを利用します。


uploadTask.on(
  "state_changed",
  snapshot => {
    let percentage =
      (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
  },
  error => {
    console.log(error);
  },
  () => {
    storageRef.getDownloadURL().then(url => {
      this.url = url;
      this.sendMessage();
      this.file_upload_modal = false;
    });
  }
);

snapshot.totalBytesでファイルのサイズ、bytesTransferredがアップロードされたサイズです。percentageは割り算をして100をかけることで出しています。

その値をstyleのwidthに設定するため、プログレスバーのdivをvue.jsから取得するためにrefを利用します。


<div class="bg-green-900 h-full block" style="width:0%" ref="progress_bar"></div>

vue.jsからはthis.$refs.progress_barで取得できます。取得した値を利用して下記のようにstyle.widthを更新することができます。


snapshot => {
  let percentage =
    (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
  this.$refs.progress_bar.style.width = percentage + "%";
},

ファイルが小さいと一瞬で100%になってしまいますがサイズが大きいと進捗具合をプログレスバーの幅で確認することができます。

その他

メッセージの最下部にスクロールする

メッセージのよりとりが増えているとスクロールしなければ最新のメッセージを見ることはできません。ダイレクトチャネルを選択した時に自動でメッセージの最新の場所までスクロールさせる設定を行います。

メッセージ表示画面のdivにid=”message_bottom”を追加します。


<div class="h-full flex flex-col ml-6">
  <div class="flex-grow overflow-y-scroll" id="message_bottom">
    <div

チャンネルを選択した時にメッセージの最新が表示されるようにdirectMessageメソッドのmessageのchild_addedイベントに$nextTickを追加します。


firebase
  .database()
  .ref("messages")
  .child(this.channel_id)
  .on("child_added", snapshot => {
    this.messages.push(snapshot.val());
    this.$nextTick(() => {
      let message_bottom = document.getElementById("message_bottom");
      message_bottom.scrollTop = message_bottom.scrollHeight;
    });
  });

child_addedイベントでメッセージが追加される度に自動で最新のメッセージの場所まで移動します。