「IonicとFirebaseを使って3日で作る写真共有アプリ」シリーズ6本目、通信処理の作成編です。
まとめ記事はコチラ。
https://qiita.com/kazuki502/items/585aef0d79ed1235bec0
アプリだからサーバーとデータをやりとりしたい
前回までで、画面遷移と画面内の処理(写真のプレビューなど)を作ってきました。今回は、アプリの核となる機能である、①写真のアップロード機能と②アップロードした写真のダウンロード機能を作ります。
firebaseを使ってサーバーレスの環境を作る。
アプリをみんなに使ってもらうためには、ローカルで動かしているだけではだめで、サーバーにアップロードしてアクセス可能な状態にしていくことが必要です。ただ、自分でサーバーを立ててネットにつなげて設定してとかやるのはめんどい。一回やったことありますけど、出来るならやりたくない作業でした。。。
アプリを作るときって2つ大きなハードルがあって、「環境設定」と「サーバー設置」なんですね。これがめんどくさくて、多くの人がつまづくところになっています。でも今は多くの人たちの努力によってそれもなくなりつつあります。「環境設定」に関しては、以前やったようにコマンド一つで自動でやってくれましたよね。1つ1つ手動でソフトをインストールしていた時があったと思うとかなり便利になりました。では「サーバー設置」はどうでしょうか。それもかなり便利になってきています(この時代に生まれたことを感謝しましょう)。
実は環境設定でアプリのデプロイをやってしまっています。このセクションですね。firebaseを使えば、面倒なサーバー設定をすることなくサーバーを使うことが出来るんです!このとき使ったのは"hosting"というアプリをインターネット上で使えるようにする機能でした。今回の記事ではそれ以外の機能を使っていきます。
使用する機能の説明
firestorage(ファイルストレージ)
今回は写真をアップロードしますが、そのアップロード先を用意する必要があります。イメージとしては、google driveやdropboxを思い浮かべてもらえればいいです。その機能がfirestorageなんです。アプリからアップロードすることが出来るので、今回はこれを使って写真をサーバーに保存していきます。
firestore(データベース)
アプリがアップロードした写真やメッセージの情報を取得するためにはデータベースを用意する必要があります。その機能もfirebaseに備わっています。それがfirestoreです。今回はデータベースとしてこれを使っていきたいと思います。
※firestoreはNoSQLといわれる新しい形のデータベースなので、一般によく使われる関係性データベースとは異なります。firebaseにあるという理由で今回は使っていますが、最適のデータベースという訳ではないので注意として書いておきます。もっと詳しく知りたい方はコチラを参照してください(https://academy.gmocloud.com/qa/20160509/2284 )。
実装していこう!
環境変数の設定
firestoreとfirestorageをアプリから使うためには、アプリ側でちょっとした設定が必要です。まずはfirebaseを使いやすくするためのモジュールをインストールします。今回はAngularfire2を使っていきます。
Angularfire2
https://github.com/angular/angularfire2
Readmeに従って、モジュールをインストールします。アプリのあるローカルフォルダで書きコマンドを実行してください。
$ npm install firebase @angular/fire --save
無事、インストールされたらenvironments
フォルダの中にあるファイルを次のように編集してください。
export const environment = {
production: false,
firebaseConfig: {
apiKey: "Your apikey",
authDomain: "Your authDomain",
databaseURL: "Your databaseURL",
projectId: "Your projectId",
storageBucket: "Your storageBucket",
messagingSenderId: "Your messagingSenderId",
appId: "Your appId"
}
};
"Your ***"となっている部分は、firebaseの構成設定から取得したあなたの値を使ってください。firebaseコンソールからプロジェクトページに入った後、左サイドメニューの「Project Overview」の右側にある歯車アイコンをクリックして「プロジェクトの設定」をクリック、全般タブの下の方に「firebase SDK snippet」があるのでそこで自分の使うべき値を取得することが出来ます。
モジュールの追加
app.module.ts
でangularfire2のモジュールを追加します。このようにしてください。
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
// 追加
import { AngularFireModule } from '@angular/fire';
import { environment } from '../environments/environment';
@NgModule({
declarations: [AppComponent],
entryComponents: [],
imports: [
BrowserModule, IonicModule.forRoot(), AppRoutingModule,
// 追加
AngularFireModule.initializeApp(environment.firebaseConfig),
],
providers: [
StatusBar,
SplashScreen,
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
],
bootstrap: [AppComponent]
})
export class AppModule {}
これでangularfire2を使う準備が完了しました。
firestorageの設定
firestorageのためにAngularFireStorageModule
というモジュールが用意されているので、app.module.ts
に追加しましょう。
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { AngularFireModule } from '@angular/fire';
import { environment } from '../environments/environment';
// 追加
import { AngularFireStorageModule } from '@angular/fire/storage';
@NgModule({
declarations: [AppComponent],
entryComponents: [],
imports: [
BrowserModule, IonicModule.forRoot(), AppRoutingModule,
AngularFireModule.initializeApp(environment.firebaseConfig),
AngularFireStorageModule // 追加
],
providers: [
StatusBar,
SplashScreen,
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
],
bootstrap: [AppComponent]
})
export class AppModule {}
これで、アプリ内でfirestorageとの通信が出来るようになりました。firebase側の設定も忘れないようにしましょう。firebaseのコンソールに入って、左サイドメニューの"Storage"をクリックしてください。するとこんな画面になると思います。
スタートガイドをクリックして、
"OK"を押しましょう。
このままだと、認証していないユーザーはアップロードできないのでルールを変えたいと思います。ルールタブを開いて、ルールを下記に変更してください。
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if true;
}
}
}
これで、サーバーに写真をアップロードする準備ができました。
写真をアップロードする
アプリに戻って、写真をアップロードする処理を組み込みましょう。現時点では、写真を選択してプレビュー表示されるところまではできていると思います。
ここから、送信ボタンを押して画像がアップロードされるようにしましょう。直観的には、upload-photo.component.ts
にアップロード処理機能を実装したくなるところですが、もしかしたら他の画面でもその機能を使いたくなるかもしれません。また、こういった通信処理は切り離しておいた方が後々便利になることが多いです。なので、通信周りの処理をまとめて別に実装できるようにしましょう。
そのためにServiceを使いたいと思います。Serviceは、サーバーとの通信や認証など、共通処理として使いたい機能をまとめておくときに使います。「他の画面でも使いそうだな~」と思ったらServiceだと思ってください。まずは、下記のコマンドを実行してください。
$ ionic g service shared/service/network
すると、sharedフォルダの中にserviceフォルダが作られて、その中にnetwork.service.ts
が作られます。その中をこのように編集してください。
import { Injectable } from '@angular/core';
import { AngularFireStorage } from '@angular/fire/storage';
@Injectable({
providedIn: 'root'
})
export class NetworkService {
constructor(
private storage: AngularFireStorage
) { }
// 写真をアップロードする
public async uploadPhoto(img: string, filename: string) {
// 画像が無い場合は処理を終了します。
if (!img) {
throw new Error('写真がありません');
} else {
const filePath = 'photos/' + filename;
const ref = this.storage.ref(filePath);
const task = ref.putString(img, 'data_url');
return task.then((snapshot) => {
console.log('Uploaded', snapshot.totalBytes, 'bytes.');
console.log('File metadata:', snapshot.metadata);
// Let's get a download URL for the file.
return snapshot.ref.getDownloadURL().then((url) => {
return alert(`${url}`);
});
});
}
}
}
これで、"uploadPhoto"を使って写真をアップロードすると、成功したときに、写真のurlが表示されるようになりました。これをコンポーネントで使えるようにするために、"app.module.ts"に追加してください。
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { AngularFireModule } from '@angular/fire';
import { environment } from '../environments/environment';
import { AngularFireStorageModule } from '@angular/fire/storage';
// 追加
import { NetworkService } from 'src/app/shared/service/network.service'
@NgModule({
declarations: [AppComponent],
entryComponents: [],
imports: [
BrowserModule, IonicModule.forRoot(), AppRoutingModule,
AngularFireModule.initializeApp(environment.firebaseConfig),
AngularFireStorageModule
],
providers: [
StatusBar,
SplashScreen,
// 追加
NetworkService,
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
],
bootstrap: [AppComponent]
})
export class AppModule {}
画面処理に組み込む
serviceが定義できたところで、次はそれをcomponentに組み込んでいきましょう。
import { Component, OnInit, Input } from '@angular/core';
// ModalControllerを追加
import { NavParams, ModalController } from '@ionic/angular';
// 追加
import { NetworkService } from 'src/app/shared/service/network.service';
const RESULT = 'result';
@Component({
selector: 'app-modal-upload',
templateUrl: './modal-upload.component.html',
styleUrls: ['./modal-upload.component.scss'],
})
export class ModalUploadComponent implements OnInit {
// "value" passed in componentProps
public imageSrc: any;
public isSelected: boolean;
@Input() value: number;
private reader = new FileReader();
constructor(
private navParams: NavParams,
private network: NetworkService, // 追加
private modalController: ModalController // 追加
) { }
ngOnInit() {}
public previewPhoto(event) {
const file = event.target.files[0];
this.reader.onload = ((e) => {
this.imageSrc = e.target[RESULT];
this.isSelected = true;
});
this.reader.readAsDataURL(file);
}
// 写真アップロード処理
public async uploadPhoto() {
if (!this.imageSrc) {
alert('写真を選択してください');
} else {
// ファイル名の作成
const now = new Date();
const filename = 'eventname_' + now.getHours() + now.getMinutes() + now.getSeconds() + now.getMilliseconds();
this.network.uploadPhoto(this.imageSrc, filename).then((value) => {
this.modalController.dismiss(); // モーダル画面を閉じる処理
});
}
}
}
ボタンに処理を紐づけるのも忘れずに。
<label class="photo" for="photo-upload">
<div [ngClass]="{'inactive': isSelected}">
<ion-icon name="image"></ion-icon><br>
画像を選択
</div>
<ion-img [ngClass]="{'inactive': !isSelected}" [src]="imageSrc"></ion-img>
</label>
<input type="file" name="photo" id="photo-upload" accept="image/*" (change)="previewPhoto($event)">
<ion-button class="send" (click)="uploadPhoto()">送信</ion-button>
これで、写真をアップロードするとアラートでurlが表示されます。このurlにアクセスすると、アップロードした写真が見られます。また、firebaseのコンソールを見ると、
写真がアップロードされているのが分かります。これで、写真のアップロード機能が出来ました。
アップロードした写真の情報をデータベースに保存する
アップロードした写真をアプリで使えるようにするためには、写真の情報を保存するデータベースを作成して、そこに情報を保存するようにする必要があります。まずはfirebaseでデータベースを使うための設定をしましょう。
firebaseのコンソールに入って、"Database"をクリックします。
「データベースの作成」をクリックしてください。
「テストモードで開始」を選択して「有効にする」をクリックしてください。
これでデータベースが使えるようになりました。
データベース用モジュールをインポートする
アプリ側に戻って、firestore用のモジュールをインポートします。これまで通り"app.module.ts"に追加します。
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { AngularFireModule } from '@angular/fire';
import { environment } from '../environments/environment';
import { AngularFireStorageModule } from '@angular/fire/storage';
// 追加
import { AngularFirestoreModule } from '@angular/fire/firestore';
import { NetworkService } from 'src/app/shared/service/network.service'
@NgModule({
declarations: [AppComponent],
entryComponents: [],
imports: [
BrowserModule, IonicModule.forRoot(), AppRoutingModule,
AngularFireModule.initializeApp(environment.firebaseConfig),
AngularFireStorageModule,
// 追加
AngularFirestoreModule
],
providers: [
StatusBar,
SplashScreen,
NetworkService,
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
],
bootstrap: [AppComponent]
})
export class AppModule {}
Serviceにデータベース通信機能を追加する
先ほどと同じように、データベースとの通信は共通機能として分けておきたいので先ほど作ったserviceにデータベース通信処理を追加しましょう。
import { Injectable } from '@angular/core';
import { AngularFireStorage } from '@angular/fire/storage';
import { AngularFirestore } from '@angular/fire/firestore';
@Injectable({
providedIn: 'root'
})
export class NetworkService {
constructor(
private storage: AngularFireStorage,
private db: AngularFirestore
) { }
// 写真をアップロードする
public async uploadPhoto(img: string, filename: string) {
// 画像が無い場合は処理を終了します。
if (!img) {
throw new Error('写真がありません');
} else {
const filePath = 'photos/' + filename;
const ref = this.storage.ref(filePath);
const task = ref.putString(img, 'data_url');
return task.then((snapshot) => {
console.log('Uploaded', snapshot.totalBytes, 'bytes.');
console.log('File metadata:', snapshot.metadata);
// Let's get a download URL for the file.
return snapshot.ref.getDownloadURL().then((url) => {
return alert(`${url}`);
});
});
}
}
// データベースにアップロードされた写真を追加する
private addPhotoInfo(imgPath: string, photoname: string) {
return this.db.collection('photolist').add({
name: photoname,
url: imgPath,
createdAt: new Date()
});
}
}
addPhotoInfo
という関数は、データベースにアップロードした写真のパス、写真の名前、作成時間を書き込む処理になっています。写真のアップロードが成功したときにだけ、データベースへの書き込みを行いたいので、次はuploadPhoto
を編集します。
// 写真をアップロードする
public async uploadPhoto(img: string, filename: string) {
// 画像が無い場合は処理を終了します。
if (!img) {
throw new Error('写真がありません');
} else {
const filePath = 'photos/' + filename;
const ref = this.storage.ref(filePath);
const task = ref.putString(img, 'data_url');
return task.then((snapshot) => {
console.log('Uploaded', snapshot.totalBytes, 'bytes.');
console.log('File metadata:', snapshot.metadata);
// Let's get a download URL for the file.
return snapshot.ref.getDownloadURL().then((url) => {
// return alert(`${url}`);
// データベース書き込み処理を追加
return this.addPhotoInfo(url, filename);
});
});
}
}
はい、これでオッケーです。では写真をアップロードしたときに、データベースに書き込まれるかやってみましょう。
はい、ちゃんと書き込まれていますね。これで、データベースへの書き込みもできるようになりました。
データベースの読み込み
最後に、アップロードした写真を一覧として取得出来るようにしたいと思います。なんとなく流れが分かっていると思いますが、その通りでserviceに機能を追加していきます。
import { Injectable } from '@angular/core';
import { AngularFireStorage } from '@angular/fire/storage';
import { AngularFirestore } from '@angular/fire/firestore';
import { map } from 'rxjs/operators'; // 追加
@Injectable({
providedIn: 'root'
})
export class NetworkService {
constructor(
private storage: AngularFireStorage,
private db: AngularFirestore
) { }
// 写真をアップロードする
public async uploadPhoto(img: string, filename: string) {
// 画像が無い場合は処理を終了します。
if (!img) {
throw new Error('写真がありません');
} else {
const filePath = 'photos/' + filename;
const ref = this.storage.ref(filePath);
const task = ref.putString(img, 'data_url');
return task.then((snapshot) => {
console.log('Uploaded', snapshot.totalBytes, 'bytes.');
console.log('File metadata:', snapshot.metadata);
// Let's get a download URL for the file.
return snapshot.ref.getDownloadURL().then((url) => {
// return alert(`${url}`);
return this.addPhotoInfo(url, filename);
});
});
}
}
// データベースにアップロードされた写真を追加する
private addPhotoInfo(imgPath: string, photoname: string) {
return this.db.collection('photolist').add({
name: photoname,
url: imgPath,
createdAt: new Date()
});
}
// 写真リストを取得する:追加
public getPhotos() {
return this.db.collection<{createdAt: Date, name: string, url:string}>('photolist', ref => {
return ref.orderBy('createdAt', 'desc');
}).snapshotChanges()
.pipe(map(actions => actions.map(action => {
const data = action.payload.doc.data();
return {createdAt: data.createdAt, name: data.name, url: data.url};
})));
}
}
これで、データベースから写真の情報が取得出来るようになりました。photo-listコンポーネントに写真一覧機能を実装していきましょう。
import { Component, OnInit } from '@angular/core';
import { NetworkService } from '../shared/service/network.service';
import { Observable } from 'rxjs';
@Component({
selector: 'app-photo-list',
templateUrl: './photo-list.page.html',
styleUrls: ['./photo-list.page.scss'],
})
export class PhotoListPage implements OnInit {
public photolist: Observable<{createdAt: Date, name: string, url:string}[]>;
constructor(
private network: NetworkService
) { }
ngOnInit() {
this.photolist = this.network.getPhotos();
}
}
ここでは、photolistという変数を用意して、そこに写真情報の一覧を格納するようにしています。サーバー側でデータが追加されたり、削除されたり変更が合った場合には自動的にその変更を受信するようになっています(これがfirebaseのいいところ)。画面表示の部分も書き換えていきましょう。
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-title>photo-list</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-grid>
<ion-row *ngFor="let photo of photolist | async">
<ion-col>
<ion-img [src]="photo.url"></ion-img>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>
ここについては少し説明しておきたい部分が2つあります。
*ngForとは何なんだ
angularになれていない方は、疑問に感じたと思います。この"*ngFor"はhtml上で繰り返し処理をしたい場合に使う構文です。"*ngFor"が書かれているタグが繰り返しの単位となっています。詳しくは公式のドキュメントを参考にしてください。
今回は、サーバーから取得した写真情報の一覧を表示するために使っています。
asyncとは何なんだ
これもangular特有のものです。pipeと言って、画面に表示する直前に挟む処理でいろんなものがあり、また自分で作成することもできます。
https://angular.jp/guide/pipes
"async"は、非同期処理で取得する値を表示する場合に使うものです。これも公式のドキュメントが詳しいのでそちらを参照してください。
https://angular.jp/guide/observables-in-angular#%E9%9D%9E%E5%90%8C%E6%9C%9F%E3%83%91%E3%82%A4%E3%83%97
簡単に言うと、遅れて取得するデータも表示できるようにする処理ですね。
動作を確認する
では、photo-list画面を開いて、写真が取得できているか確認しましょう。
こんな風に写真が表示されていればオッケーです。