2019/02/03
絵描きとかUXとかやりつつフロントもやってる「ゆき」です。ポートフォリオサイトは10年くらい前にMoveableTypeで作ったきり。最近流石に「これでフロントやってますとか言ったら絶対次転職できなくね?」と危機を感じたので0から作り直しました。
サイト: https://pf.nekobooks.com/機能・性能・運用を考えて作った結果、VueとFirebase(Web)の機能を一通り使ったサイトが出来上がりました。これからちょっと凝ったポートフォリオサイトを作りたい方向けに、どういう目的でどの機能を使ったのか、その時のポイントはなんだったのかを共有します。
Webデザイン自体は専門ではないのですが、絵やUX系もやっているので見た目はがんばりたい。。
基本的には絵とアプリの紹介を載せるサイトです。
以前のポートフォリオサイトはFlickrに載せた絵をAPIで引っ張って来ていたのですが、そもそもFlickrに絵をアップするのが面倒になってしまった(というかパスワードを毎回忘れる)ので今回はサイト自体に管理者ページを作ります。
私見ですが、エンジニアのポートフォリオサイトに軽さは超重要だと思ってます。
プラグイン盛り盛りとか背景に動画使ったりとかは作る身としても避けたい。
だってそういう仕事したくないじゃないですか
使ったFirebaseの機能 | 作ったポートフォリオサイトの機能 |
---|---|
Authentication | サイト管理者機能のログイン |
Database | ギャラリー画像の一覧・メタデータ管理 |
Storage | ギャラリー画像とサムネイルのアップロード先 |
Hosting | デプロイ先・独自ドメインの接続 |
Functions | サムネイルの自動生成・OGP生成・メール送信 |
ここから、作ったものの機能ベースでVueやFirebaseのどんな機能を使ったのか紹介します。GitHubで公開したソースのリンクもちょこちょこ貼るので、詳細はソースをご参照くださいませ
トップページはSVGを使った全画面のアニメーションになっています(というか元々はこれが一番やりたかった)。固定のアニメーションではなく、女の子のキャラクターが時々アクロバティックに飛んだり跳ねたり、あとギャラリーページで絵を選ぶと、絵の配色に合わせてアニメーションの配色も変わるようになってます。
自己満って言ってしまえばそれまでなのですが、ポートフォリオサイトは仕事ではなかなかできない技術や表現をこだわれる場所なので、こういうのも大切だと思うのです。
この手のアニメーションを作りたいと思った時の選択肢は色々あるので私見でまとめました。
アニメーションの種別 | ライブラリ例 | Pros(つよみ) | Cons(つらみ) |
---|---|---|---|
Canvasアニメーション | Pixi.jsとかCreate.jsとか | WebGLを使えばぬるぬる動かせる。要素が増えても強い。エフェクトやフィルタが豊富 | 描画面積が負荷に直結するので全画面になると辛い。レスポンシブとRetinaの対応もちょっと辛い。 |
SVGアニメーション | Snap.jsとかSVG.jsとか | リソースが軽い(ただしこれはCanvasでもできる)。流行りの流体シェイプみたいにぬるぬる動くデザインができる | 流体シェイプ含めSVG固有のアニメーションは重くなりがち(多分GPUレンダリングが効かない) |
DOM/CSSアニメーション | jQueryとかAnime.js2とか | 普通のWebのテクニックが使える。アニメーション以外の機能・要素と相性がいい。GPUが効けば速い | 要素が増えると重くなりやすい。GPUレンダリングの効くアニメーションは限定的 |
今回は熟慮の結果
「SVG・CSSアニメーションを🐱Vue.jsだけで🐱作る」
にしました。つまり、Vue.js以外のアニメーションライブラリは使っていません。理由は「圧倒的な軽さ」。今回のアニメーションはサイトの中ではあくまで背景なので、このアニメーションのためにロード待ちになったりスマホがカイロになる事態は避けないといけません。
実装の解説はここではしませんが、面倒なCSSトランジションをプロパティとして扱えるコンテナコンポーネントを作ってアニメーションを構成していきます。
コンテナコンポーネント:
/src/components/anime/core/ECont.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>
もうAnimateCCつかえよ...って話もあるのですが。それでも今回はVueの力試しというのと、やっぱりコンポーネントになって
<!-- 首の角度15度・お辞儀の角度30度(※cnfyはこの子の名前です) -->
<cnfy :headAngle="15" :bowAngle="30" />
みたいに宣言的にかけるのは楽しい。しかもリアクティブに動かせる!Vueたのしい!😸
....と、この辺りは書き始めると長くなるので、ニーズがあれば別な記事にまとめようと思います。最終的に、全てのアニメーションがCSSのtransform: translate3d(x, y, z) scale(s) rotate(r)
とopacity
プロパティのトランジションとしてレンダリングされるようにすることで、そこそこややこしいアニメーションでも60fpsが実現できました。
楽しいアニメーションができたのでここからは真面目に管理画面を作ります。
最初にFirebaseコンソールのAuth機能でgoogleログインを有効化します。
TwitterやFacebookと違って、googleログインであれば「有効にする」をポチッとするだけでおしまい。超楽。
管理者用のページはfirebase Authを使ってgoogle認証します。
/src/admin/pages/ImgUploader.vue
async mounted () {
this.user = await AdminApis.Auth.loginWithGoogle()
}
mountedで問答無用でログインに飛ばします。
ログインの実装はこのあたり参照してください。
/src/admin/api/AdminAuth.js
ちなみに、FirebaseAuthを使ったログインの詳しい解説は↓こちらのページあたりが素敵です
Vue.js + Firebase を使って爆速でユーザ認証を実装する
ここでは単にgoogleで認証してもらうことだけが目的で「ログインしたユーザーが管理者かどうか」は判定しません。(クライアントサイドで動いている以上、画面側で判定をしたところでセキュリティ的には大した意味はないので)
正しい管理者かどうかは、DB(firestore)/ストレージ(Firebase storage)のセキュリティルールでチェックして弾きます。(もちろん、通常の利用者が使う画面であれば画面側でもちゃんと判定してあげてください)
allow read;
allow write: if request.auth.token.email == '管理者のgmail';
画像のアップロードにはFirebase Storageを使います。
ソースはこのあたり↓
/src/admin/api/ImageUploaderApi.js
/src/admin/pages/ImgUploader.vue#L111
だいたいチュートリアル通りな感じなので問題ないかと。
Storageへのアップロードをトリガーに、サムネイルとメタデータ(タイトル・縦横サイズ・配色情報...etc)を生成する処理をサーバ側で走らせます。このあたりもクライアントで生成してアップロードする方法もあるのですが、将来的にTwitterの自分の投稿から絵を拾ってギャラリーに追加したい、という野望があったので、今回はFunctionに登場してもらいました。
(そうでなくても、不整合を避けたいトランザクション的な処理はサーバ側にまとめておいた方が安全ではあります)
ソースはこの辺り↓
/functions/index.js#L13
functions.storage.object().onFinalize(callback)
の形でコールバックを登録します。
サムネイル作成処理の本体はここ↓
/functions/src/generateThumbnail.js#L98
元画像・サムネ共にこのタイミングでキャッシュを有効にしておきます。忘れるとバズった時にあっさり無料枠を使い果たすはずなので注意。 2019/2/13訂正:FirebaseのStorageで使われるGCP エッジキャッシュはキャッシュにヒットしてもStorage転送量としてカウントされるとのこと(@k2wankoさんに教えていただきました)。実際今回トレンドに乗ったことで、1日あたり2000近いアクセスがあり、無料枠を振り切って僅かですが課金しました。。
公式のサンプル(Automatically Generate Thumbnails)とほぼ同じですが、ImageMagicを起動する代わりにCanvasに画像を読み込んでJavaScriptで縮小・jpeg生成まで全部やっています。このあたりはお好みで(こっちの方が軽いかなぁ...って思ったけどそんなに変わらない?)。
サムネ生成に続けて、DBにメタ情報を書き込みます。
Cloud Firestoreにサムネと元画像両方のパス・サイズ・画像のメインカラーを保存します。画像を扱うアプリであれば、画像ロード前にレイアウトを確定させるために画像のサイズは是非とも保存しておくべきです。今回は色情報も保存しておいて色付きのプレースホルダーを表示できるようにしています。一枚高々数十KBのサムネですが、モバイルでのUX改善のためには有効な方法です。
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
公開(非管理者)ページに戻ってギャラリーページを作ります。
ギャラリーはこのサイトのメイン機能の一つなので、トップページを表示した時点でCloud Firestoreからデータを取得します。
/src/api/ImgListApi.js#L14
今の所数十件なので全件まとめて取ってきていますが、もし数百になるならページングを考えるべきかもしれません。
また、DBのロードが終わったら順次サムネイルのプリロードも走らせています。
やっぱり新作を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
色々プラグインはあるようですが、今回は普通にVueコンポーネントで手書きします。普通に座標を計算してposition: absolute
でdivを並べているだけです。(CSS的にはグリッドレイアウトを使うべきなのかもですが、今回は選択やリサイズ時にぬるぬる動かしたかったのでパス)
/src/components/PhotoList.vue
仕事だとなかなか難しい場面も多いですが、この程度のものであればプラグインやたらと組み込むよりも手書きした方が細かい調整利くし勉強にもなりますね。
今日日メールとかいらなくない?という話もあるのですが、まあお約束ということで。
画面は手抜き感満載ですが許してください。。
メール送信にはFunctionsを使ってWebAPIを作ります。
exports.contactmail = functions.https.onRequest(callback)
のような形でFunctionを作るとWebからアクセスできるcontactmail
という名前のFunctionができます。実際にアクセスするURLはFirebaseのコントールから確認できます。
サーバ側のメール発信処理にはnodemailerを使います。こちらの記事が超絶丁寧です↓
VuejsとFirebaseでメール送信機能を実装する
この記事にだいたい書かれているので注意点だけ箇条書きすると、
環境変数は
firebase functions:config:set admin.contact.mail="メアド" admin.contact.pass="パス"
を(ローカルの)コマンドラインで叩けば、Functions側から
const config = functions.config()
const mail = config.admin.contact.mail
const pass = config.admin.contact.pass
のように簡単に取得できます。
あと今回はメールの送信先が完全固定なので大丈夫ですが、送信先もなんらかの条件で動的に変更する合、Functionsの呼び出しパラメータをいじって任意のアドレスにメールを飛ばせてしまうことのないよう、厳重にチェックが必要です。スパムメールの踏み台やなりすまし詐欺に悪用されてしまったら目も当てられないので気をつけましょう(こわい)。
Firebase Hostingで公開するサイトはデフォルトではhttps://プロジェクトID.firebaseapp.com
のドメインで公開されます。これだけでも十分ありがたいのですが、やはりポートフォリオサイトなので独自ドメインを使いたいですよね。
FirebaseコンソールのHosting画面から「ドメインを接続」を押すと、自分の所有するドメイン・サブドメインをFirebaseのサイトにつなぐことができます。ドメインをどこでとったかにもよるのですが、大体同じような流れです。↓こちらの記事はお名前.comの場合のやり方を解説してくれています。
Gatsby+firebaseで独自ドメインのHTTPSサイトを作る(その2 Firebaseの設定)
ギャラリーの絵ひとつひとつにURLでアクセスできるようにしたのでTwitterで新作を宣伝できるようにはなったのですが、やっぱりそこまでやったらOGPしたいですよね。
今回はギャラリーページは作品の画像とタイトルを使い、その他のページは一律のOGPとしました。
トップページ最近はWebページを作るツールやサービスもどんどん進化していて、ちょっとしたサイトならノンコーディングでもそれなりにできる時代になってしまいました。そんな中でも最近の技術をキャッチアップしつつ自分で組んでみると中々勉強になるものです。特にデザイナさんやエンジニアさんであれば、トレンドを取り入れつつ自力でしっかり実装しているサイトは良いPRになるはず。この記事を見てポートフォリオサイト作ってみようかなー、って思ってくれる人がいたらとても嬉しいです。