VueとFirebaseの基本機能全部使ってぬるぬる動くポートフォリオサイトを作ったのでソースと解説

絵描きとかUXとかやりつつフロントもやってる「ゆき」です。ポートフォリオサイトは10年くらい前にMoveableTypeで作ったきり。最近流石に「これでフロントやってますとか言ったら絶対次転職できなくね？」と危機を感じたので0から作り直しました。

サイト： https://pf.nekobooks.com/
ソース： https://github.com/yuneco/portfolio
![pf_overview.gif](https://qiita-image-store.s3.amazonaws.com/0/245740/9414dbe0-2c3b-d225-46f8-c94ac17bb139.gif)

機能・性能・運用を考えて作った結果、VueとFirebase(Web)の機能を一通り使ったサイトが出来上がりました。これからちょっと凝ったポートフォリオサイトを作りたい方向けに、どういう目的でどの機能を使ったのか、その時のポイントはなんだったのかを共有します。

## 2019.4.18追記
春なので期間限定[^add418]で桜が咲くアニメーションを追加してみました。単体のアニメーションは[テストページ](https://pf.nekobooks.com/test2)で試せます。複雑に見えるかもしれませんがやっていることは基本的にこの記事で書いていることの範囲なので、気になった方はぜひ[該当のコミット](https://github.com/yuneco/portfolio/commit/e30219166fad7c21fe03703481e28233a9a86eaa)を読んでみてください。[ぐねぐねした枝をSVGで動的に作る部分](https://github.com/yuneco/portfolio/blob/04646fe1c45aeb8338e09ecd31d7bf907f663837/src/components/anime/TreeBranch.vue#L96)で苦労していますが、他は大したことはやっていません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245740/54306b4e-35e5-c798-3e29-b913df1d6bdc.png)

[^add418]: なんだかんだでちょっと重くなってしまったので暫くしたら元に戻します

## 今回の要件（＝ぼくのかんがえたさいきょうのポートフォリオ）

### デザイン

* レスポンシブ！スマホ・PC両対応
* 全画面でぬるぬる動くアニメーション

Webデザイン自体は専門ではないのですが、絵やUX系もやっているので見た目はがんばりたい。。

### 機能

* かわいい・動くトップページ
* 絵を載せるギャラリーページ
* アプリ紹介とスキルセット書くページ
* コンタクトフォーム（メールで飛ばす）
* Webから新しい絵を追加できる管理ページ
* ギャラリーページは作品ごとにOGP対応すること

基本的には絵とアプリの紹介を載せるサイトです。
以前のポートフォリオサイトはFlickrに載せた絵をAPIで引っ張って来ていたのですが、そもそもFlickrに絵をアップするのが面倒になってしまった（というかパスワードを毎回忘れる）ので今回はサイト自体に管理者ページを作ります。

### 性能

* iPhone6でもぬるぬる動く。目標60fps
* とにかく軽く。目標ロード時間 < 1s

私見ですが、エンジニアのポートフォリオサイトに軽さは超重要だと思ってます。
プラグイン盛り盛りとか背景に動画使ったりとかは作る身としても避けたい。

### やらないこと

* IE対応

**:innocent:だってそういう仕事したくないじゃないですか:innocent:**

## Firebaseの機能と作ったものの対応

| 使ったFirebaseの機能 | 作ったポートフォリオサイトの機能 |
|:---|:---|
|Authentication|サイト管理者機能のログイン|
|Database|ギャラリー画像の一覧・メタデータ管理|
|Storage|ギャラリー画像とサムネイルのアップロード先|
|Hosting|デプロイ先・独自ドメインの接続|
|Functions|サムネイルの自動生成・OGP生成・メール送信|

## 作ったものと解説

ここから、作ったものの機能ベースでVueやFirebaseのどんな機能を使ったのか紹介します。GitHubで公開したソースのリンクもちょこちょこ貼るので、詳細はソースをご参照くださいませ :bow:  

### Vue.jsのみでトップページ・アニメーション

トップページはSVGを使った全画面のアニメーションになっています（というか元々はこれが一番やりたかった）。固定のアニメーションではなく、女の子のキャラクターが時々アクロバティックに飛んだり跳ねたり、あとギャラリーページで絵を選ぶと、絵の配色に合わせてアニメーションの配色も変わるようになってます。
自己満って言ってしまえばそれまでなのですが、ポートフォリオサイトは仕事ではなかなかできない技術や表現をこだわれる場所なので、こういうのも大切だと思うのです。

#### アニメーションの種類とライブラリは何を使うか？

この手のアニメーションを作りたいと思った時の選択肢は色々あるので私見でまとめました。

|アニメーションの種別|ライブラリ例|Pros（つよみ）|Cons（つらみ）|
|:---|:---|:---|:---|
|Canvasアニメーション|[Pixi.js](http://www.pixijs.com/)とか[Create.js](https://createjs.com/)とか|WebGLを使えばぬるぬる動かせる。要素が増えても強い。エフェクトやフィルタが豊富|描画面積が負荷に直結するので全画面になると辛い。レスポンシブとRetinaの対応もちょっと辛い。|
|SVGアニメーション|[Snap.js](http://snapsvg.io/)とか[SVG.js](https://svgjs.com/)とか|リソースが軽い（ただしこれはCanvasでもできる）。流行りの流体シェイプみたいにぬるぬる動くデザインができる|流体シェイプ含めSVG固有のアニメーションは重くなりがち（多分GPUレンダリングが効かない）|
|DOM/CSSアニメーション|[jQuery](https://jquery.com/)とか[Anime.js](https://animejs.com/)[^1]とか|普通のWebのテクニックが使える。アニメーション以外の機能・要素と相性がいい。GPUが効けば速い|要素が増えると重くなりやすい。GPUレンダリングの効くアニメーションは限定的|

[^1]: SVGも使えるらしい

今回は熟慮の結果

**「SVG・CSSアニメーションを🐱Vue.jsだけで🐱作る」**

にしました。つまり、Vue.js以外のアニメーションライブラリは使っていません。理由は「圧倒的な軽さ」。今回のアニメーションはサイトの中ではあくまで背景なので、このアニメーションのためにロード待ちになったりスマホがカイロになる事態は避けないといけません。

#### どうやったか

実装の解説はここではしませんが、面倒なCSSトランジションをプロパティとして扱えるコンテナコンポーネントを作ってアニメーションを構成していきます。

コンテナコンポーネント：
[/src/components/anime/core/ECont.vue](https://github.com/yuneco/portfolio/blob/master/src/components/anime/core/ECont.vue)
要約すると↓こういうコンポーネントです。シンプル :cat: :star: 

```vue
<template>
  <div @click="clicked" :style="{
      transformOrigin: `${ox}px ${oy}px`,
      transform: `translate3d(${x}px, ${y}px, ${z}px) scale(${s}) rotate(${r}deg)`,
      transition: `
        transform ${dur}ms 0s ${easing},
        transform-origin ${dur}ms 0s ${easing}`
    }"> 
    <slot></slot>
  </div>
</template>
```

これを組み合わせてキャラクターのコンポーネントを作ります。
（ https://pf.nekobooks.com/cnfy でデバッグ画面を実際にさわれます）
![20180103.gif](https://qiita-image-store.s3.amazonaws.com/0/245740/b39daa62-d4bc-49bf-7948-a95807a6ed63.gif)


もうAnimateCCつかえよ...って話もあるのですが。それでも今回はVueの力試しというのと、やっぱりコンポーネントになって

```vue
<!-- 首の角度15度・お辞儀の角度30度（※cnfyはこの子の名前です） -->
<cnfy :headAngle="15" :bowAngle="30" />
```

みたいに宣言的にかけるのは楽しい。しかもリアクティブに動かせる！Vueたのしい！😸

....と、この辺りは書き始めると長くなるので、ニーズがあれば別な記事にまとめようと思います。最終的に、全てのアニメーションがCSSの`transform: translate3d(x, y, z) scale(s) rotate(r)`と`opacity`プロパティのトランジションとしてレンダリングされるようにすることで、そこそこややこしいアニメーションでも60fpsが実現できました。

### 管理（絵のアップロード）ページとギャラリーページ

![image.png](https://qiita-image-store.s3.amazonaws.com/0/245740/1c6824d5-d7ed-a50e-fd3c-aff7a92322d8.png)

楽しいアニメーションができたのでここからは真面目に管理画面を作ります。

#### Firebase Authでログイン

最初にFirebaseコンソールのAuth機能でgoogleログインを有効化します。
TwitterやFacebookと違って、googleログインであれば「有効にする」をポチッとするだけでおしまい。超楽。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/245740/88c9caf5-0557-6823-3743-c181aa5b5a91.png)

管理者用のページはfirebase Authを使ってgoogle認証します。
[/src/admin/pages/ImgUploader.vue](https://github.com/yuneco/portfolio/blob/master/src/admin/pages/ImgUploader.vue)

```javascript
async mounted () {
  this.user = await AdminApis.Auth.loginWithGoogle()
}
```

mountedで問答無用でログインに飛ばします。
ログインの実装はこのあたり参照してください。
[/src/admin/api/AdminAuth.js](https://github.com/yuneco/portfolio/blob/master/src/admin/api/AdminAuth.js)

ちなみに、FirebaseAuthを使ったログインの詳しい解説は↓こちらのページあたりが素敵です
[Vue.js + Firebase を使って爆速でユーザ認証を実装する](https://qiita.com/sin_tanaka/items/ea149a33bd9e4b388241)

ここでは単にgoogleで認証してもらうことだけが目的で「ログインしたユーザーが管理者かどうか」は判定しません。（クライアントサイドで動いている以上、画面側で判定をしたところでセキュリティ的には大した意味はないので）

正しい管理者かどうかは、DB(firestore)/ストレージ(Firebase storage)のセキュリティルールでチェックして弾きます。（もちろん、通常の利用者が使う画面であれば画面側でもちゃんと判定してあげてください）

```
allow read;
allow write: if request.auth.token.email == '管理者のgmail';
```

#### Firebase Storageにファイルをアップロード

画像のアップロードにはFirebase Storageを使います。
ソースはこのあたり↓

[/src/admin/api/ImageUploaderApi.js](https://github.com/yuneco/portfolio/blob/master/src/admin/api/ImageUploaderApi.js)
[/src/admin/pages/ImgUploader.vue#L111](https://github.com/yuneco/portfolio/blob/master/src/admin/pages/ImgUploader.vue#L111)

だいたいチュートリアル通りな感じなので問題ないかと。

#### Firebase Functionsでサムネイルとメタ情報を作成

Storageへのアップロードをトリガーに、サムネイルとメタデータ（タイトル・縦横サイズ・配色情報...etc）を生成する処理をサーバ側で走らせます。このあたりもクライアントで生成してアップロードする方法もあるのですが、将来的にTwitterの自分の投稿から絵を拾ってギャラリーに追加したい、という野望があったので、今回はFunctionに登場してもらいました。
（そうでなくても、不整合を避けたいトランザクション的な処理はサーバ側にまとめておいた方が安全ではあります）

ソースはこの辺り↓
[/functions/index.js#L13](https://github.com/yuneco/portfolio/blob/master/functions/index.js#L13)

`functions.storage.object().onFinalize(callback)`の形でコールバックを登録します。

サムネイル作成処理の本体はここ↓
[/functions/src/generateThumbnail.js#L98](https://github.com/yuneco/portfolio/blob/master/functions/src/generateThumbnail.js#L98)
元画像・サムネ共にこのタイミングでキャッシュを有効にしておきます。~~忘れるとバズった時にあっさり無料枠を使い果たすはずなので注意。~~ 2019/2/13訂正：FirebaseのStorageで使われる[GCP エッジキャッシュ](https://qiita.com/sinmetal/items/37c105a098174fb6bf77)はキャッシュにヒットしてもStorage転送量としてカウントされるとのこと（@k2wankoさんに教えていただきました）。実際今回トレンドに乗ったことで、1日あたり2000近いアクセスがあり、無料枠を振り切って僅かですが課金しました。。


[公式のサンプル(Automatically Generate Thumbnails)](https://github.com/firebase/functions-samples/tree/Node-8/generate-thumbnail)とほぼ同じですが、ImageMagickを起動する代わりにCanvasに画像を読み込んでJavaScriptで縮小・jpeg生成まで全部やっています。このあたりはお好みで（こっちの方が軽いかなぁ...って思ったけどそんなに変わらない？）。

サムネ生成に続けて、DBにメタ情報を書き込みます。
Cloud Firestoreにサムネと元画像両方のパス・サイズ・画像のメインカラーを保存します。画像を扱うアプリであれば、画像ロード前にレイアウトを確定させるために画像のサイズは是非とも保存しておくべきです。今回は色情報も保存しておいて色付きのプレースホルダーを表示できるようにしています。一枚高々数十KBのサムネですが、モバイルでのUX改善のためには有効な方法です。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/245740/44a47a6a-cb67-d675-6445-b014dba76f66.png)

#### Cloud Firestoreでメタ情報の更新と画像URLの書き込み

DB書き込み後、管理画面側でDB更新をトリガにメタ情報の更新画面を表示します。タイトルや画像の説明（今の所画面には表示していませんが）を編集するのと、画像のダウンロードURLを生成するのが目的です。

本来であればStorageにアップした画像のダウンロードURLは、一つ前のFunctionsの中で生成してDBに保存すべきなのですが、これが今の所これが簡単にはできません。クライアント用のFirebase SDKでは一行ですむURL取得が、Functionsで使えるAdmin SDKだとサービスアカウントを作って面倒なプロセスを踏まないといけないようです。。

詳細は下のStackOverFlowの回答が詳しいです。一応簡単に生成するための逃げ道はあるようですが、正攻法ではないので今回はパスしました。

[Get Download URL from file uploaded with Cloud Functions for Firebase](https://stackoverflow.com/questions/42956250/get-download-url-from-file-uploaded-with-cloud-functions-for-firebase)

#### Cloud Firestoreからギャラリー画像のメタデータを取得

公開（非管理者）ページに戻ってギャラリーページを作ります。
ギャラリーはこのサイトのメイン機能の一つなので、トップページを表示した時点でCloud Firestoreからデータを取得します。
[/src/api/ImgListApi.js#L14](https://github.com/yuneco/portfolio/blob/master/src/api/ImgListApi.js#L14)

今の所数十件なので全件まとめて取ってきていますが、もし数百になるならページングを考えるべきかもしれません。
また、DBのロードが終わったら順次サムネイルのプリロードも走らせています。

#### VueRouterによる個別のギャラリー画像へのリンク対応

やっぱり新作をUPしたらその絵に直リンクしたいですよね。GAでログを取るためにも、絵一つ一つにURLでアクセスできる必要があります。
こんな感じ↓のURLで個別の絵にURLでアクセスする仕組みを作ります。
https://pf.nekobooks.com/gallery/1547483353992_933

アクセスの制御はVueRouterで画像のIDをパラメータとして受け取って、そのIDと画像リストの選択項目を連動させます。ただ、それだけではアプリ内のリンクは動作しますが、URL直リンクで飛んできた時にはうまく動きません。VueRouterから画像のIDを受け取った時点ではまだDBのロードが終わっていないからです。

今回はDBロードをwatchsで監視して、ロードされたデータがセットされたタイミングで連動を走らせることで対処します。
[/src/pages/GalleryView.vue](https://github.com/yuneco/portfolio/blob/master/src/pages/GalleryView.vue)

#### 画像のグリッドレイアウトをVueでがんばる

色々プラグインはあるようですが、今回は普通にVueコンポーネントで手書きします。普通に座標を計算して`position: absolute`でdivを並べているだけです。（CSS的にはグリッドレイアウトを使うべきなのかもですが、今回は選択やリサイズ時にぬるぬる動かしたかったのでパス）
[/src/components/PhotoList.vue](https://github.com/yuneco/portfolio/blob/master/src/components/PhotoList.vue)

仕事だとなかなか難しい場面も多いですが、この程度のものであればプラグインやたらと組み込むよりも手書きした方が細かい調整利くし勉強にもなりますね。

### Firebase Functionsでメールフォームを作る

今日日メールとかいらなくない？という話もあるのですが、まあお約束ということで。
画面は手抜き感満載ですが許してください。。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/245740/bb25ad64-4e72-fe2f-457c-56f04c318d1a.png)

#### FirebaseFunctionsでメールを送信

メール送信にはFunctionsを使ってWebAPIを作ります。

```javascript
exports.contactmail = functions.https.onRequest(callback)
```

のような形でFunctionを作るとWebからアクセスできる`contactmail`という名前のFunctionができます。実際にアクセスするURLはFirebaseのコントールから確認できます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/245740/cc845bc9-ff8c-3e15-2f2b-8c201a587f1a.png)

サーバ側のメール発信処理にはnodemailerを使います。こちらの記事が超絶丁寧です↓
[VuejsとFirebaseでメール送信機能を実装する](https://qiita.com/Hiroyuki1993/items/1ab9266ca6fc422113e3)

この記事にだいたい書かれているので注意点だけ箇条書きすると、

* Firebaseの無料プランではgoogleの外へ通信ができないため、無料プランで頑張る場合はgmailを使う
* 発信に利用するgmailのアカウントはセキュリティレベルを落とす必要あり。Firebaseのログインやサイトの管理者アカウントとしてセキュリティルールに設定したものとは別のアカウントを利用すべきです（gmailで転送設定をすれば好きなアカウントで受信できます）
* ID/パスワードはソースに直書きせずに環境変数を利用すること

環境変数は

```bash
firebase functions:config:set admin.contact.mail="メアド" admin.contact.pass="パス"
```
を（ローカルの）コマンドラインで叩けば、Functions側から

```javascript
const config = functions.config()
const mail = config.admin.contact.mail
const pass = config.admin.contact.pass
```

のように簡単に取得できます。

あと今回はメールの送信先が完全固定なので大丈夫ですが、送信先もなんらかの条件で動的に変更する合、Functionsの呼び出しパラメータをいじって任意のアドレスにメールを飛ばせてしまうことのないよう、厳重にチェックが必要です。スパムメールの踏み台やなりすまし詐欺に悪用されてしまったら目も当てられないので気をつけましょう（こわい）。

### Firebase Hosting で独自ドメイン公開

Firebase Hostingで公開するサイトはデフォルトでは`https://プロジェクトID.firebaseapp.com`のドメインで公開されます。これだけでも十分ありがたいのですが、やはりポートフォリオサイトなので独自ドメインを使いたいですよね。

FirebaseコンソールのHosting画面から「ドメインを接続」を押すと、自分の所有するドメイン・サブドメインをFirebaseのサイトにつなぐことができます。ドメインをどこでとったかにもよるのですが、大体同じような流れです。↓こちらの記事はお名前.comの場合のやり方を解説してくれています。

[Gatsby+firebaseで独自ドメインのHTTPSサイトを作る（その２ Firebaseの設定）](https://qiita.com/kei0425/items/a7da8d263e8833f7e837)

## ギャラリーページのOGP対応

ギャラリーの絵ひとつひとつにURLでアクセスできるようにしたのでTwitterで新作を宣伝できるようにはなったのですが、やっぱりそこまでやったらOGPしたいですよね。

今回はギャラリーページは作品の画像とタイトルを使い、その他のページは一律のOGPとしました。

トップページ
![image.png](https://qiita-image-store.s3.amazonaws.com/0/245740/8809951d-5d28-ce85-8fb4-b955ba101446.png)
ギャラリーページ（の個々の作品）
![image.png](https://qiita-image-store.s3.amazonaws.com/0/245740/fcaba0e7-eece-f7e6-edd7-726a365f09c1.png)
この部分のやり方は年末にアドベントカレンダーでかなり詳しく書いたので、手前味噌ですが↓こちらをご参照くださいませ。
[SNS映えするWebアプリを...！FirebaseとVue.jsでSPAのOGP画像の動的生成をやってみたら案外楽だった](https://qiita.com/yuneco/items/5e526464939082862f5d)
今回は手抜きのリダイレクト方式で、かつ画像も動的生成ではなくStorageにアップロード済みの画像のURLをそのまま返しています。

## 性能評価と最適化

キャッシュなしでLoadまで1秒（1.6MB|1.1minってあるのはLoad後にギャラリーのサムネを裏でゆっくり読んでるから）。アニメーション用のSVGは全てapp.jsの中に含まれています。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/245740/cf1b29d2-c956-8e46-1465-1cce4c60005b.png)
管理者機能部分をチャンク分割していますが、他の最適化はそれほどやっていません（この辺りは勉強不足なのであまり語れない...どなたかこれでいいのか教えてください）。
[VueRouter-遅延ローディングルート](https://router.vuejs.org/ja/guide/advanced/lazy-loading.html#%E9%81%85%E5%BB%B6%E3%83%AD%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0%E3%83%AB%E3%83%BC%E3%83%88)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/245740/f8fdd485-aa41-36ae-8f70-387f00ffb360.png)
Auditsでもかなりいいスコアが出てます。ただ、正直な実感としてはアクセシビリティやSEOにはあまり配慮していないのでこの点数は高過ぎ気がします。この辺りはVueのテンプレートが形だけよしなにやってくれてしまっているせいなのかもしれません。

## 今後のタスク（備忘+自戒）

* AtomicDesign?なにそれ?みたいなコンポーネント分割をなんとかしたい
* ファビコンつくる
* ギャラリー画像のタイトル再編集・並べ順変更
* キャラの表情・衣装のバリエーションを増やす（趣味全開）

## まとめ

最近はWebページを作るツールやサービスもどんどん進化していて、ちょっとしたサイトならノンコーディングでもそれなりにできる時代になってしまいました。そんな中でも最近の技術をキャッチアップしつつ自分で組んでみると中々勉強になるものです。特にデザイナさんやエンジニアさんであれば、トレンドを取り入れつつ自力でしっかり実装しているサイトは良いPRになるはず。この記事を見てポートフォリオサイト作ってみようかなー、って思ってくれる人がいたらとても嬉しいです。

* :fire: ポートフォリオサイトをがっつり作るとVueとFirebaseの基本機能を一通り習得できるよ 
* :relaxed: 全画面アニメーションするならSVGが軽くて最強！専用のライブラリを使わなくても、Vueだけで結構作れちゃう
* :cat: エンジニアやデザイナーはみんなもっとオリジナルのポートフォリオサイト作ろう 
* :innocent: ポートフォリオ作ったらブログとかQiitaとかに記事も書こう