ねこ限定のフォト共有サイトをつくったよ(Vue + SAM)
猫の写真だけをアップロードできて、かつその表示は毎回ランダムで一期一会、というフォト共有サービスをつくってみたという話。
技術要素は
- Vue3 (Vue Router、Vuex)
- AWS SAM (一部Terraform)
- GitHub Actions
あたりです。
よければ一枚、あなたも置き土産していってくださいませ。
概要
こんなサイトです。
👉 YourCat
特徴は以下。
- 投稿できる写真は猫が写っているものだけ(機械学習でフィルタリングします)
- アカウント登録も写真へのコメントもなーんもいらないです
- 毎回サイト上に表示される猫は全登録写真からランダム→数が増えれば増えるほど二度目以降の再会は難しい…!
本当に一瞬で使えるので、何か 🐱 写真をお持ちの方はとりあえず一枚だけでもお試しでアップロードしていってくださると嬉しいです~~~
技術要素と所感
使用したもの、および雑感です。
以下はリポジトリのリンク。
Vue
Web で見かける小さな UI 部品の中で自分が好きだなーと思うものをちゃんとリリースできる成果物に含めてみたいなーとずっと思っていて、それを今回は Vue でたくさん書けたのが楽しかったです。
いくつか紹介させてください。
blur なモーダルバック
しょっぱなから Vue 全然関係ないんですが、モーダルのバックを暗くするんじゃなくてぼかすやつをやりたかったんですよ~。
Apple の UI っぽくなってモダンになるのと、一応ギャラリーサイトの側面があるので暗くしちゃうのはなんか悲しいかなーと。ぼやかすとカラフルな写真タイル感が上品に残る気がします。
スケルトンローディング
最も Vue の恩恵を受けられた部分だったと思います。
表示のたびにスケルトンブロックのサイズや個数をランダム変動させる仕組みにしてみました。
<template>
<div class="loading-block-wrap" :style="{'width': size.width * 133 / size.height + 'px', 'flex-grow': size.width * 133 / size.height}">
<i :style="{'padding-bottom': size.height / size.width * 100 + '%'}"></i>
<div class="loading-block" :class="{'stop-loading': isStopAnimation}" :height="size.height"></div>
</div>
</template>
表示まで含めても大した実装ではないこと、写真表示と同様のインターフェースにしたかったことなどからライブラリの採用は見送りました。
進捗アニメーション
ユーザーからの操作1つにつきアプリケーションからの応答1つ、というのは最もよくあるインタラクションですが、それらが連なって一連の動きを取るものを作ってみたいと考えていました。
これも細かいところでかなり時間を要しました。フロントエンドの実装って本当に工数を必要としますよね。でも「見た目やデザインだから」って言われたりしやすいわけで、実務でやっている方たちの苦労が少し想像できた感じでした…。
フォトタイル CSS
これも Vue とは直接的には関係ないのですが、フロントエンド側の話題ということでここに書きます。
このようなデザインは比較的よく見かけるので「簡単な CSS で書けそうやな?」って思うでしょう?
ところがどっこい、全然そんなことはありませんでした。
最初は「横幅を統一して縦に並んでいくデザイン」にしたのですが(下図参照)、これだと画像の配列要素が横向きに進んでいかないためプログラム的にかなり扱いづらいということでやめました。
出典:https://www.mdn.co.jp/di/contents/3265/39761/
そこでこの「Google フォト」もしくは「Adobe ストック」風のデザインを目指したわけなのですが、自分で書こうと思ってもすぐに筆が止まりました。止まるのはタイピングか。
というわけでこのやたら長い GitHub issue スレッド に全面的に助けられました。
最終的には JS 含めこんな感じになります。
<div class="photo-wrap">
<div class="photo" v-for="photo, index in photoArray" :key="photo" :style="{'width': photo.size.width * 200 / photo.size.height + 'px', 'flex-grow': photo.size.width * 200 / photo.size.height}">
<i :style="{'padding-bottom': photo.size.height / photo.size.width * 100 + '%'}"></i>
<img :src="photo.src" :alt="photo.title" @click="onClickPhoto(index)" @load="loaded(index)" crossorigin="Anonymous"/>
</div>
おかげで美しいフォトギャラリーになったと思ってます!
ちなみに画像ビューアには spotlight.js を使いました。
マークアップで反応させられるようにする形式と、API 的に使える方法とで使い分けられるのが気に入りました。シンプルなデザインもよいところ。
ただちょっとだけ bug があって、CSS などで暫定対応せざるを得ないシーンがいくつかありました。issue を継続監視しているのでいつか直るかもです。
onClickPhoto(index) {
Spotlight.show(this.photos, {
index: index,
animation: "slide, fade", // "play"
control: "zoom, close",
title: false,
// play: 3, bug (http://github.com/nextapps-de/spotlight/issues/49)
onshow: (index) => {
Spotlight.addControl("dl-btn", (event) => {
execDownload(this.photos[index].src);
});
},
onclose: () => {
Spotlight.removeControl("dl-btn");
},
});
},
AWS SAM
特に目新しいことはないので書くことはありません。localhost 用の開発環境とリリース用の本番環境とで区分して運用しています。
それと、このアプリケーションに限らないインフラ管理のためのリソース類は Terraform で書いています。
AWS Rekognition
強いて言うなら AWS Rekognition は初めて使いました。
今回のような物体ラベルづけはもとより、顔認証や顔画像の一致度比較程度なら API 1つでラクに使えるのは驚きでした。そんでもってめっちゃ安いです。
例えばラベリングである detect_labels()
は下記のように使います。
from boto3 import Session
session = Session(region_name="ap-northeast-1")
client = session.client("rekognition")
res = client.detect_labels(Image={
"Bytes": file
})
レスポンスのデータを使って画像内にバウンディングボックスを表示することができます。
API リファレンスは下記。
ただし実際はランタイムごとの SDK のリファレンスを見た方がわかりやすかったです。少なくとも Python(boto3) はそうでした。
今回はサービスの軸として使いましたが、「要らんものを弾く」という簡易なフィルタリングとしても十分実用に耐えうるサービスだと思います。レスポンスはだいたい 200~300ms くらいでした。
CI/CD
今回は CodeBuild などは使わず GitHub Actions のみの運用としました。
理由は
- フロントエンドとバックエンドでリポジトリを分ける体制を取っていたため、両者で使うツールは統一したかった
- フロントエンド側は AWS ではなくホスティングサーバーへのデプロイのため、AWS 側のサービスを使う必要性がなかった
- 単にお手軽
という感じ。
SAM は sam deploy
するだけなのでいいとしても、ホスティングサーバーへのデプロイというのは初めてなのでちょっとだけ苦戦しました。
scp
したいわけなのですが、定義済みの GitHub Actions である scp-action を使えば一発かなーと思いきや、ディレクトリ構成がうまく制御できず結局セルフベタ書きすることになりました。
その結果クソゴリラコードになってしまったのですが、まあよい勉強になりました。
SAM 側はいつもどおり samconfig.toml
で環境区分しています。本番環境へのフックはプルリクのマージのみ許容です(これは Vue 側も同様)。
- run: sam build --use-container
- if: ${{ github.ref == 'refs/heads/main' }}
run: sam deploy --no-confirm-changeset --no-fail-on-empty-changeset --config-env dev
- if: ${{ github.ref == 'refs/heads/release/prd' }}
run: sam deploy --no-confirm-changeset --no-fail-on-empty-changeset --config-env prd
Twitter API
新しい 🐱 が投稿されたらその写真をツイートしてくれる Twitter アカウントもつくりました。
サーバーは Python で書いたのですが、Twitter の API には TwitterAPI というダイレクトな名前のパッケージが公式で紹介されていたのを発見したのでそれを使いました。
from TwitterAPI import TwitterAPI
api = TwitterAPI(
consumer_key,
consumer_secret,
access_token_key,
access_token_secret
)
api.request('statuses/update', {'status':'This is a tweet!'})
見てわかる通り、OAuth 認証に使う例の4つのキー類もそのまま扱えて、次の行ではもうツイートのリクエストができちゃいます。
インストールは pip
で。
pip install TwitterAPI
今度から毎回これ使おう。
おわりに
個人開発あるあるだと思うのですが、「利用者さえ揃えば素敵なものになる」ってやつあるじゃないですか。
ユーザーコンテンツがないと成立しないのはビジネス観点で言えば当然なんですけど、でもそこにマーケティングしてお金かけて…というモチベーションはやっぱり湧かないと。お金使ったからといって素人が簡単に人を集められる保証もないですしね。
そんなわけで、みなさんが にゃんこ🐱写真 を提供してくれないと悲しいことになってしまいます。
ぜひ!!!
なにとぞ!!!!
…お願いします。
P.S.
わんこ🐶 の写真をお持ちの方も一度お試しあれ。
Discussion
うちの猫の写真もあげておきました。とってもユーザー体験が良かったです。とくに猫が写真に含まれているかどうかがチェックされるあたりがすごく気持ち良かったです。
ぎゃー!!!!
catnose さんにこのサイトを見ていただけることを叶わぬ願いとして密かな目標にしつつ作っていたのですが、まさかこんなすぐに…。嬉しくて本当に涙出かけました…。。
写真までありがとうございます!!
わんこ🐶 のお遊び機能も実は catnose さんのいつものわんちゃんをイメージしながら作りました…。笑
本当にありがとうございました。これからも頑張ります。
とても素敵なギャラリーで癒されました…!
アップロードアイコンも画像の判定も最高です😆
あ、1匹だけ鹿が混じっていましたよ🤔
悪い人もいるもんですね
ありがとうございます!!!
いや自分も Twitter の自動投稿を見て「これ猫、なのか…?」と思ったのですが、AWS が猫とラベリングしたのでヨシとしようと目をそむけましたw
真面目にやるなら運用面でこの辺りの仕組みづくりを考えないといけなそうです。どうしたものか…🤔
こんにちは!
飼ってはいないものの、可愛い猫いっぱいで癒される記事 + サイトでした...!!
ダメ元で相談なのですが、個人開発の案として「SAM + Vue」「ログイン不要の写真投稿サイト」という部分を参考にさせてもらっても良いでしょうか...??
載せる写真は猫ではなく、自分がギター好きだったりするので楽器系にしようかなと思っています.(My New Gear...的な感じで)
どうぞどうぞ、もちろんです!
というか、アイデアに何か新規性があるわけでは全くないので許可などそもそも不要だとは思いますが…笑