Angular+Firebaseでcanvasを描くWEBアプリを作る

# はじめに

今、Angular+Firebaseでプロダクト開発をしていますが、何分初めてのことばかりで何度もつまづきながら進んでいます。
最近ようやく、canvasとFirebase RealtimeDBとの連携ができるようになったので、そこに至るまでの制作過程を簡単なWEBアプリで紹介します。

1. [angular-cliでプロジェクトを作る](http://qiita.com/Yamamoto0525/items/5d4508a06effd4d6e40e#angular-cli%E3%81%A7%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E4%BD%9C%E3%82%8B)
2. [angularfire2をインストールする](http://qiita.com/Yamamoto0525/items/5d4508a06effd4d6e40e#angularfire2%E3%82%92%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%81%99%E3%82%8B)
3. [firebaseのアカウントを作成する](http://qiita.com/Yamamoto0525/items/5d4508a06effd4d6e40e#firebase%E3%81%AE%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B)
4. [coreモジュールを作成する](http://qiita.com/Yamamoto0525/items/5d4508a06effd4d6e40e#core%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B)
5. [canvasコンポーネントを作成する](http://qiita.com/Yamamoto0525/items/5d4508a06effd4d6e40e#canvas%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B)
6. [canvasテンプレートを更新する](http://qiita.com/Yamamoto0525/items/5d4508a06effd4d6e40e#canvas%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88%E3%82%92%E6%9B%B4%E6%96%B0%E3%81%99%E3%82%8B)
7. [canvasに描きこむロジックを作る](http://qiita.com/Yamamoto0525/items/5d4508a06effd4d6e40e#canvas%E3%81%AB%E6%8F%8F%E3%81%8D%E3%81%93%E3%82%80%E3%83%AD%E3%82%B8%E3%83%83%E3%82%AF%E3%82%92%E4%BD%9C%E3%82%8B)
8. [ロジックをサービスに分離する](http://qiita.com/Yamamoto0525/items/5d4508a06effd4d6e40e#%E3%83%AD%E3%82%B8%E3%83%83%E3%82%AF%E3%82%92%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%AB%E5%88%86%E9%9B%A2%E3%81%99%E3%82%8B)
9. [RealtimeDBを設定する](http://qiita.com/Yamamoto0525/items/5d4508a06effd4d6e40e#realtimedb%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B)
10. [canvasのデータをRealtimeDBと連携させる](http://qiita.com/Yamamoto0525/items/5d4508a06effd4d6e40e#canvas%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92realtimedb%E3%81%A8%E9%80%A3%E6%90%BA%E3%81%95%E3%81%9B%E3%82%8B)

## 開発環境

Mac: OSX
angular-cli: v1.0.0-beta.19-3
node: v6.6.0

## レポジトリ

https://github.com/Yamamoto0525/canvas-app

## アプリ動作イメージ

![image1.gif](https://qiita-image-store.s3.amazonaws.com/0/112566/0d792154-8de5-ab96-a94d-f04b87ab87af.gif)

# 制作過程

## angular-cliでプロジェクトを作る

npmライブラリであるangular-cliを使って、新しいプロジェクトを作成します。

```bash
npm install -g angular-cli --save
```
```bash
cd <プロジェクトの設置パス>
ng new canvas-app
```
angular-cliの初期設定については、[Angular2の環境構築（Angular CLIで構築）
](http://qiita.com/Yamamoto0525/items/65d5a0b36eb4dbd8079b)を参照してください。

## angularfire2をインストールする

AngularとFirebaseの連携を容易にしてくれるangularfire2をアプリケーションにインストールします。ターミナルを起動し、プロジェクト直下で下記コマンドを実行してください。

```bash
npm install firebase angularfire2 --save
```

## firebaseのアカウントを作成する

firebaseのアカウントを作り、新しいプロジェクトを作成します。
このあたりの操作はすでに詳しく書いている方かいるので、そちらを参照してください。
参考：[Firebaseを用いて5分でセキュアなWebサイトを公開する](http://qiita.com/potato4d/items/95eaf2e41404711e621e)

## coreモジュールを作成する

次にcoreモジュールを作成します。[Angularのスタイルガイド](http://qiita.com/Yamamoto0525/items/e59551faf69775a7ad12#%E3%82%B3%E3%82%A2%E6%A9%9F%E8%83%BD%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB)では、全体で共有するモジュール（CommonModuleやFormsModuleなど）は、coreモジュールに導入した方がよいとされていますので、angularfire2のモジュールはcoreモジュールから読み込むようにします。

```bash
ng g module core
```

上記コマンドを実行すると、`/app`フォルダ以下に`core`フォルダが作られ、その中に`core.module.ts`とそれ以外のファイルが作成されますが、今回使用するのは`core.module.ts`なので、それ以外のファイルは削除します。このとき、`app.module.ts`の`imports`に`CoreModule`を追加しておきます。

```ts:app.module.ts
import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {CoreModule} from './core/core.module'; // 追加

import {AppComponent} from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    CoreModule // 追加
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {
}
```

`core.module.ts`を更新し、`imports`に`AngularFireModule`を加えます。
また、全体で一度しか読み込まないというこのモジュールの特性を活かし、このファイル内にangularfire2の設定を書き込みます。
firebaseのプロジェクトで確認できる設定を、`firebaseConfig`のyour～に入れてください。

```ts:core.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AngularFireModule } from 'angularfire2';

export const firebaseConfig = {
  apiKey: '<your-key>',
  authDomain: '<your-project-authdomain>',
  databaseURL: '<your-database-URL>',
  storageBucket: '<your-storage-bucket>'// 必要に応じて
};

@NgModule({
  imports: [
    CommonModule, // ngForとかngIFを使うときに使用するmodule
    AngularFireModule.initializeApp(firebaseConfig)
  ],
  exports: [],
  declarations: [],
  providers: []
})
export class CoreModule { }
```

## canvasコンポーネントを作成する

`/app`配下にcanvasコンポーネントを追加します。コンポーネントをどこに配置するか、ということで最初色々と悩みましたが、[スタイルガイドの指摘](http://qiita.com/Yamamoto0525/items/e59551faf69775a7ad12#%E5%85%A8%E4%BD%93%E7%9A%84%E3%81%AA%E6%A7%8B%E6%88%90%E3%81%AE%E3%82%AC%E3%82%A4%E3%83%89%E3%83%A9%E3%82%A4%E3%83%B3)を受け、`/app`配下に配置することにします。

```bash
ng g component canvas
```

コンポーネントが作成され、`app.module.ts`の`declarations`にも自動的に`CanvasComponent`が追加されます。

```ts:canvas.component.ts
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-canvas',
  templateUrl: './canvas.component.html',
  styleUrls: ['./canvas.component.css']
})
export class CanvasComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }

}
```

## canvasテンプレートを更新する

canvasコンポーネントのテンプレートを更新します。
canvas要素には、idプロパティの代わりにテンプレート参照変数（template reference variable）`#myCanvas`を追加しておきます。これによって要素の特定をすることができます。
また、RealtimeDBの変化を確認するため、`<button>`要素も加えておきます。

```html:canvas.component.html
<section>
  <div class="canvas">
    <canvas #myCanvas width="300" height="300"></canvas>
  </div>
  <div class="btn">
    <button (click)="drawLine()">線を追加</button>
    <button (click)="removeLine()">線を削除</button>
  </div>
</section>
```

最低限のCSSを実装しておきます。

```css:canvas.component.css
.canvas {
  text-align: center;
  padding: 20px;
}
.canvas canvas {
  border: 1px #ccc solid;
}
.btn {
  text-align: center;
}
.btn button {
  border: 1px #ccc solid;
}
```

`app.component.html`に、canvasコンポーネントを読み込ませます。

```html:app.component.html
<app-canvas></app-canvas>
```

## canvasに描きこむロジックを作る

`app.module.ts`の`CanvasComponent`クラスに、canvasへ描きこむためのロジックを追加します。今回はボタンを押すと、少しづつずれながらひし形状に線を描きこんでいくメソッドを作成します。
まず`#myCanvas`を取得するために、`@ViewChild`デコレータをインポートし、`myCanvas`変数を宣言します。
次に`AfterViewInit`クラスをインポートして`CanvasComponent`に継承させ、`ngAfterViewInit()`の中で`myCanvas`を参照します。ここで気を付けないといけないのは、`@ViewChild`デコレータは描画後でないと値を参照できないため、`ngOnInit()`ではなく、`ngAfterViewInit()`メソッドの中で参照する必要があります。

```ts:canvas.component.ts
import {Component, ViewChild, OnInit, AfterViewInit} from '@angular/core'; // クラスを追加

@Component({
  selector: 'app-canvas',
  templateUrl: 'canvas.component.html',
  styleUrls: ['canvas.component.css']
})
export class CanvasComponent implements OnInit, AfterViewInit { // AfterViewInitを継承
  @ViewChild('myCanvas') myCanvas; // myCanvasを宣言
  public canvas: HTMLCanvasElement = null; // canvasを宣言
  public ctx: CanvasRenderingContext2D = null; // contextを宣言
  
  constructor() {
  }
  
  ngOnInit() {
  }
  
  ngAfterViewInit() {
    this.canvas = this.myCanvas.nativeElement;
    this.ctx = this.canvas.getContext( '2d' );
  }
}
```

`myCanvas`の参照が完了したら、次はメソッドを作成します。
ロジックの詳細については省略しますが、`drawLine()`、`addLine()`、`removeLine()`の3つのメソッドを実装します。その際、初期値は[コンストラクタではなく、ngOnInit](http://qiita.com/armorik83/items/90b60fae2622f7c1f1a2#component%E3%81%AE%E5%88%9D%E6%9C%9F%E5%8C%96%E5%87%A6%E7%90%86%E3%82%92constructor%E3%81%AB%E6%9B%B8%E3%81%8F)で入れておきます。

```ts:canvas.component.ts（抜粋）
export class CanvasComponent implements OnInit, AfterViewInit { 
  @ViewChild("myCanvas") myCanvas;
  public canvas: HTMLCanvasElement = null;
  public ctx: CanvasRenderingContext2D = null;
  Lines: {x:number, y:number, rx:number, ry:number}[]; // 型を宣言

  constructor() {
  }

  ngOnInit() { // 初期値を入れる
    this.Lines = [{x:0, y:150, rx:300, ry:150 }];
  }

  ngAfterViewInit() { // myCanvasを参照
    this.canvas = this.myCanvas.nativeElement;
    this.ctx = canvas.getContext("2d");
  }

  // 線を描く
  drawLine(): void {
    let len = this.Lines.length;
    for(let i=0;i<len;i++){
      this.ctx.beginPath();
      this.ctx.moveTo(this.Lines[i].x,this.Lines[i].y);
      this.ctx.lineTo(this.Lines[i].rx,this.Lines[i].ry);
      this.ctx.closePath();
      this.ctx.stroke();    
    }
    this.addLine(this.Lines[len-1]);
  };

  // 起点の位置により、次の配列の値を変化させつつ追加する
  addLine(el): void{
    // 左から上へ
    if(el.x>=0 && el.x<150 && el.y>0 && el.y<=150){
      Lines.push({x:el.x+5,y:el.y-5,rx:el.rx-5, ry:el.ry+5 });
    // 上から右へ
    }else if(el.x>=150 && el.x<300 && el.y>=0 && el.y<150){
      Lines.push({x:el.x+5,y:el.y+5,rx:el.rx-5, ry:el.ry-5 });
    // 右から下へ
    }else if(el.x>150 && el.x<=300 && el.y>=150 && el.y<300){
      Lines.push({x:el.x-5,y:el.y+5,rx:el.rx+5, ry:el.ry-5 });
    // 下から左へ
    }else if(el.x>0 && el.x<=150 && el.y>150 && el.y<=300){
      Lines.push({x:el.x-5,y:el.y-5,rx:el.rx+5, ry:el.ry+5 });
    }
  }

  // 線を削除し、データを初期化する
  removeLine(): void{
    this.ctx.clearRect(0, 0, 300, 300);
    this.Lines = [{x:0, y:150, rx:300, ry:150 }];
  }
}
```

この段階で一度実行してみましょう。
ターミナルで`ng serve`を実行し、`http://localhost:4200`をブラウザで確認します。
ボタンをクリックした時の動作は、次のようになります。

![image1.gif](https://qiita-image-store.s3.amazonaws.com/0/112566/0d792154-8de5-ab96-a94d-f04b87ab87af.gif)

## ロジックをサービスに分離する

`CanvasComponent`クラスが肥大化してしまったので、[ロジックをサービスに分離](http://qiita.com/Yamamoto0525/items/e59551faf69775a7ad12#%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%AB%E3%83%AD%E3%82%B8%E3%83%83%E3%82%AF%E3%82%92%E5%85%A5%E3%82%8C%E3%82%8B)します。

まず、`app/canvas`内にサービスを作成します。

```bash
ng g service canvas/drawline
```

`drawline.service.ts`が作成されるので、その中に`CanvasComponent`クラスで作成したメソッドを移動させます。

```ts:drawline.service.ts
import { Injectable } from '@angular/core';

@Injectable()
export class DrawlineService {

  constructor() { }

  // 線を描く
  drawLine(lines, ctx): void {
    let len = lines.length;

    for(let i=0;i<len;i++){
      ctx.beginPath();
      ctx.moveTo(lines[i].x,lines[i].y);
      ctx.lineTo(lines[i].rx,lines[i].ry);
      ctx.closePath();
      ctx.stroke();
    }
  };

  // 起点の位置により、次の配列の値を変化させつつ追加する
  addLine(lines): void{
    let el = lines[lines.length-1];
    // 左から上へ
    if(el.x>=0 && el.x<150 && el.y>0 && el.y<=150){
      lines.push({x:el.x+5,y:el.y-5,rx:el.rx-5, ry:el.ry+5 });
      // 上から右へ
    }else if(el.x>=150 && el.x<300 && el.y>=0 && el.y<150){
      lines.push({x:el.x+5,y:el.y+5,rx:el.rx-5, ry:el.ry-5 });
      // 右から下へ
    }else if(el.x>150 && el.x<=300 && el.y>=150 && el.y<300){
      lines.push({x:el.x-5,y:el.y+5,rx:el.rx+5, ry:el.ry-5 });
      // 下から左へ
    }else if(el.x>0 && el.x<=150 && el.y>150 && el.y<=300){
      lines.push({x:el.x-5,y:el.y-5,rx:el.rx+5, ry:el.ry+5 });
    }
  }

  // 線を削除し、データを初期化する
  removeLine(ctx): void {
    ctx.clearRect(0, 0, 300, 300);
  }
}
```

移動した分のロジックを`drawline.service.ts`から削除し、`DrawlineService`クラスをDIします。

```ts:canvas.component.ts
import {Component, ViewChild, OnInit, AfterViewInit} from '@angular/core';
import {DrawlineService} from './drawline.service.ts'; // DrawlineServiceをimportします。

@Component({
  selector: 'app-canvas',
  templateUrl: 'canvas.component.html',
  styleUrls: ['canvas.component.css'],
  providers: [DrawlineService] // providersにDrawlineServiceを加えます。
})
export class CanvasComponent implements OnInit, AfterViewInit {
  @ViewChild('myCanvas') myCanvas;
  public canvas: HTMLCanvasElement = null;
  public ctx: CanvasRenderingContext2D = null;
  public Lines: {x: number, y: number, rx: number, ry: number}[];

  constructor(private drawlineService: DrawlineService) { // DrawlineServiceをDIする
  }

  ngOnInit() {
    this.Lines = [{x: 0, y: 150, rx: 300, ry: 150}];
  }

  ngAfterViewInit() {
    this.canvas = this.myCanvas.nativeElement;
    this.ctx = canvas.getContext("2d");
  }

  // 線を描く
  drawLine(): void {
    this.drawlineService.drawLine(this.Lines, this.ctx);
    this.drawlineService.addLine(this.Lines);
  };

  // 線を削除し、データを初期化する
  removeLine(): void {
    this.drawlineService.removeLine(this.ctx);
    this.Lines = [{x: 0, y: 150, rx: 300, ry: 150}];
  }
}
```

## RealtimeDBを設定する

firabaseの「RealtimeDB」にあるルールを開き、認証条件を変更して公開します。

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

次にfirabaseの「RealtimeDB」にあるデータを開き、Linesの初期値を設定します。

![image3.png](https://qiita-image-store.s3.amazonaws.com/0/112566/4d4c2447-7033-1ef0-ff26-3808df9d962f.png)

## canvasのデータをRealtimeDBと連携させる

`canvas.component.ts`にangularfire2のクラスをインストールし、初期値を設定します。
angularfire2にはオブジェクトと配列を取得するメソッドがありますが、今回はオブジェクトとしてデータを取得します。`ngAfterViewInit`に`drawLine()`メソッドを追加しておけば、ブラウザ更新時にリアルタイムでデータが同期されます。

最後に変更後のデータをRealtimeDBに保存するため、`set()`をメソッドに加えます。

```ts:canvas.component.ts
import {Component, ViewChild, OnInit, AfterViewInit} from '@angular/core';
import {AngularFire, FirebaseObjectObservable} from 'angularfire2'; // angularfire2のクラスをインストール
import {DrawlineService} from './drawline.service.ts';

@Component({
  selector: 'app-canvas',
  templateUrl: 'canvas.component.html',
  styleUrls: ['canvas.component.css'],
  providers: [DrawlineService]
})
export class CanvasComponent implements OnInit, AfterViewInit {
  @ViewChild('myCanvas') myCanvas;
  public canvas: HTMLCanvasElement = null;
  public ctx: CanvasRenderingContext2D = null;
  public baseLines: FirebaseObjectObservable<any>; // firebaseの型を設定
  public Lines: {x: number, y: number, rx: number, ry: number}[];

  constructor(private drawlineService: DrawlineService,
              private af: AngularFire) { // AngularFireをDI
  }

  ngOnInit() {
    this.baseLines = this.af.database.object('/lines', {preserveSnapshot: true}); // オブジェクトを取得
  }

  ngAfterViewInit() {
    this.canvas = this.myCanvas.nativeElement;
    this.ctx = canvas.getContext("2d");
    this.baseLines.subscribe(snapshot => {  // subscribeでデータを取得
      this.Lines = snapshot.val();
      this.drawlineService.drawLine(this.Lines, this.ctx);
    })
  }

  // 線を描く
  drawLine(): void {
    this.drawlineService.drawLine(this.Lines, this.ctx);
    this.drawlineService.addLine(this.Lines);
    this.baseLines.set(this.Lines); // setで保存する
  };

  // 線を削除し、データを初期化する
  removeLine(): void {
    this.drawlineService.removeLine(this.ctx);
    this.Lines = [{x: 0, y: 150, rx: 300, ry: 150}];
    this.baseLines.set(this.Lines); // setで保存する
  }
}

```

# まとめ

急ピッチではありましたが、canvasを使ったWEBアプリの実装を紹介しました。
まだまだベストプラクティスとは言い難いところもあるかもしれませんので、何かお気付きの点がありましたらご指摘ください。