PhpStormと僕

日々周りを巻き込むことをモットーに。気まぐれでJetBrains製のIDEネタとか書いてます。

漫画ビレッジを支える技術とオススメのマンガ

1つ前のエントリで書いた漫画ビレッジっていうサービスをリリースして3週間弱経った。

その後もITmediaの記事でインタビューして頂いたり、 出版社絡みの方と色々話したりなど諸々各所反響があって最近はちょっとバタバタ気味。(何事においても暇なことが嫌いなので忙しいのは嬉しい)

作り始めた当初は、これ作ることで技術的にも人脈的にも本業に活かせれば良いかなぁ、、くらいのゆるい感じだったので、今の状態はとにかくありがたい限りという感じ。

3週間の間に漫画村UIに寄せてリニューアルしたり、Heroku2台体制で運用しているときにYahoo!砲がきたりと色々あって、なんか色々変化だったり進捗もあったりしていて。

Twitter経由だったり問い合わせだったりで割と技術的な構成がどんな感じなのかを興味頂いている方も多かったので、こんな感じで作ってますよ〜っていうのを色々まとめてみた。補足として前エントリをざっくり見ていただくと理解が早いかと。

個人開発の常でとにかくリソースが限られているので、無駄な工数(開発も運用も)は最小限にしないと全て破綻するので、とにかく巨人の肩の上に立つことを意識していて、自前で作らなくてもいいものはとにかくSaaSを使って解決するようにしている。

今現在使っているフレームワークやサービスは大体こんな感じ。

Nuxt

前エントリではNuxtで Framework7 っていうUIフレームワークを使っている旨を書いたが、色々つらみがあったので捨てた。今はCSSフレームワークとしてBulmaを入れている。

Framework7は簡単にネイティブアプリ風のデザインを作れるんでそれはそれで良いんだけど、ただRouter周りをwrapしてしまっているUIフレームーワークは自分で拡張しようと思ったときにかなり制約があってつらみがあることが多いなぁという学びがあった。

あと大きく変えたのはSSRにしたことくらい。SEO的な要件をいくつか満たしたかったのでSSRにしたが、結果的にこれはSPAのままでも満たせたので結果的にそこまで頑張らなくても感はあったかも。

First Meaningful Paint周りの対応(Avoid the fold)がまだうまくできていなくて(めっちゃ運用のつらみを伴えばできる)、Nuxtでここらへんの知見ある方いたらぜひお話聞かせてください。

Firebase

バックエンドではFirebaseで今は FirestoreCloud Functions を使っている。

Firestoreはとにかく便利で、ざっくり言うと Database + WebAPI + Mobile Databaseをくっつけたようなもの。Scalabilityも全く意識しなくていい。すごい。 しかも料金もFirebase Realtime Databaseよりかなり安い。(それでも、漫画ビレッジにおいてはここ一番コスト面で掛かっている)

まだβではあるものの安定性は特に困ってはいないが、ただ、機能周りがいくつか制約があって

  • SubCollectionに対してQueryが使えない(SubCollectionの存在意義って・・・)
  • Webコンソールが絶妙に機能不足な上にカジュアルにTruncate(Drop table)できてしまう
  • バックアップ周りが全くない

と、運用面でのつらみはそこそこある。

これは完全に自分が悪いんだけど、FirestoreのWebコンソールから操作をしていて、あるレコード(Document)を消そうと思って間違ってメインの商品テーブル(Collection)を全消ししてしまったことがあってめっちゃ血の気が引いた。大惨事になる前に即時復旧ができたのでそのときは良かったけど、心臓にはとてもよくない。

恐らく現時点での実務上の運用においては、FirestoreのWebコンソール画面にはアクセスできないように権限設定しちゃうのが一番安全なんだろうなぁ、、っていう気はしている。

Cloud FunctionsはFaaSなもので、ビレッジではクローラーの実行と、あとDB(Firestore)に新刊追加があった際のトリガーでスコアを変更したり、Algoliaのインデックス登録とかをやっている。 とりあえずログ周りはconsole.logしておけばStackdriver Loggingで扱えるのでなにかと便利。

Cloud Functionsは実質ほぼ無料枠で収まる程度で使っているくらい。こっちは現状そこまで不満はないが、cron相当の定期実行が自前ではできなくて、外部からなにかしらトリガーを走らせてやらないと動かないので、そこらへんのつらみがあるくらい。

Heroku

フロントはNuxtで書いているので単純にホスティングだけしたかったので、運用周りで自分が知見があったHerokuを選定した。 ローンチ当初は色々模索してPerformanceプランに上げたりオートスケール入れたりしたけど、 今は一番安いStandard-1X dynos * 2台($25*2/month)という最小限の構成に落ち着いていて Yahoo!砲が来た時も結果的に最後まで2台構成のまま乗り切った。(これはFastlyの功績が大きい)

Fastly

高機能CDN。Herokuのプラグインとして今はPro(TLS)プランの$130/monthを使っている。 基本的な使い方しかしていないが、運用を続ける中で一番このサービスに助けられている。

最初結構設定でハマって(というか反映されなくて)いくつかサポートに問い合わせたりもしたけど中の人が割と手厚いサポートをしてくれる印象。

Algolia

全文検索エンジン as a Service。日本語にも対応している。$59/month。

FirestoreはQueryでの検索周りは弱く、部分一致での検索などはできないので、漫画ビレッジでの検索機能はAlgoliaを使っている。 とにかく高速で平均7msくらいでレスポンスが返ってくる。

SynonymもWeb上から簡単に管理できるのがとても楽。まだ運用に置けるBest practicesは模索中だが、例えば「ワンピース」で検索したときに「ONE PIECE」に紐付けるようなことは全部Algolia管理画面で完結してできる。Synonm生成自動化は精度がかなり落ちそうなのでいまのところ手動でやっている。

検索にHITしなかったワード一覧もAlgolia上から管理できるので(もちろんAnalyticsでも取ってはいるけど)、「ユーザの需要はあるが漫画ビレッジではカバー出来ていないマンガ」の傾向も把握できて便利。

Bugsnag

エラー監視 as a Service。$29/month。 これは全くこだわりはなくて、3年くらい前からこれ使ってる。

細かなバグまでは対応しきれなかったりするのだけれど、どんなバグがどこでどれくらいの頻度で起きているっていうのは割と見落としがちなので、対応可否を判断するためにも入れておいて損はないかなぁ、、というのが自分の中の位置付け。

Librato

HerokuのプラグインとしてMetrics & Monitorig用として入れている。$19/month。

日中はそこまでがっつりメトリクス見ることはできないので、基本的にはヤバイ閾値でのアラート設定入れといて、これの通知がきたらなにか対応する必要がある、くらいの温度感で入れている。

これ飛んできた時大体ヤバイことになっているのでだいぶ役に立ってる。

オススメのマンガ

今年読んだ中だと 7seeds(全35巻+外伝1巻) とかゴールデンカムイ(現在14巻・今まで読まず嫌いしてたことを反省)とかヒナまつり(現在14巻・LINEマンガ12話まで無料)とか。

現時点で5巻以下のものだとあげくの果てのカノン(全5巻・小学館eコミックで1巻無料)とかブルーピリオド(現在2巻)、左利きのエレン(現在2巻)とかが好きです。

有名所ばっか挙げましたが、そんなん全部読んどるわって人はぜひ色々語り合いましょう。

まとめ

βのサービスだったりを本番で使う判断を下せるのは個人開発の強みだなーとかは痛感してます。(なにか起きたら全て自己責任なので、自分が色々忙しくなるだけで済む)

実際作ってみて、この領域は大きすぎてやはり個人で運営していくには色々と深い領域だなぁ、、、と思ってはいますが、考えうる最善手を最短ルートで通っていくのが得意な性分なので、生温かい目で見守って頂ければ幸いです。

漫画ビレッジっていうWebサービスを作った

漫画ビレッジっていうサービスを今週リリースした。

ITmediaの記事の記事でも紹介してもらったが、無料のマンガサービスに掲載されているマンガのリンクを集めたサービスという位置づけ。

各所でご紹介頂いたこともあり、公開2日で80万PVくらいのアクセスがあって結構驚いている。

賛否はあって然るべきだが、マンガアプリ運営している出版社の中の人から「ぜひうちのサービスも登録してください」という問い合わせもあったりするので、これは本当に意外で結構驚いたりはしている。

現時点で3,700シリーズくらいデータを貯めているけれど、まだ独自スコア付けが終わっていないものもあるので全シリーズは公開していないが、コンテンツは徐々に拡充していくつもり。

(どうでもいいけど、個人的なこだわりでサービス名で使っているのは「漫画」だが、固有名詞及びサービス名以外の箇所では全てカタカナの「マンガ」で統一している。本エントリ内も以下同様)

きっかけは id:kensuu さんとゴールデンウィーク前に「ブラウザだけでマンガ読めたら最高だよねぇ」「合法漫画村(パワーワード感)」っていう会話をしていて面白そうだなぁと思ったこと。

自分もマンガ好きで普段からKindle(まとめ買い勢)、マンガワンピッコマとかはよく使っているのだけれど、とにかくマンガアプリは種類が多すぎて

  • どのアプリでなんのマンガが何話まで読めるのか分からない
  • 各アプリごとにユーザ登録がめんどくさい(アカウントが大量になってどのアプリでどうログインするのか分からなくなる)
  • 読んでたマンガがどのアプリで読んでいたのか分からなくなって何話まで読んだか分からなくなる
    • これは動画をNetflixで見てたかAmazon Primeで見てたかdアニメで見てたか分からなくて何話見ればいいんだっけってなる現象も同じ

みたいなつらさはそこそこ感じていた。

マンガアプリを提供している各社プラットフォーマーからすると当たり前だけど自社のアプリを使って欲しいはずなので、 アプリごとの個別最適化は進んでいくものの横断的に見れる仕組みは出てこないだろうなぁと思っていたので、それなら作っちゃえ!と。

最初は1週間くらいで作れるかなーと思ったけど、個人開発の常でテンションが続かずにUI周りハマり始めて結構開発から離れちゃったりしたので、結果的に丸1ヶ月掛かった。

自分はエンジニアなので、以下は漫画ビレッジがどんな技術つかって作ったのかをまとめてみる。

フロント

フロントはNuxt。管理画面系はVue + Bootstrap。

UIフレームワークとしてはアプリUIっぽく作れるFramework7のNuxt wrapperのnuxt7を使っている。

ただ、UIフレームワークに関していえば結果的に選定をミスっていて、

  • Framework7-Vueに乗っかるとFramework7Domの独自実装の兼ね合いでServer Side Rendering(SSR)ができない
  • Nuxtの機能のfetchやasyncDataなどが潰されていて恩恵に与れない部分がいくつかある

というつらみがそこそこあって、UI周りは今日中にFramework7を捨てて作り直す予定。

別で作っているサービスでもAdmin機能としてFramework7は使っているが、ここらへんの事情を無視できるサービスであれば使うのは悪くないかなーと個人的に思っている。

バックエンド

バックエンドはすべてFirebaseに乗っかるようにしていて

クローラーだったり細かなバッチ処理類は Cloud Functions

WebAPI相当とデータベースに該当するものはすべてFirestoreを使っている。

ユーザ登録/認証機能は使っておらず、このサービスにおいては今後も実装する予定はない。(元々の思想に「ログインだるいよね」があるので)

FirestoreはまだβではあるもののRDB脳を捨て去ってからはかなりクセはあるもののとても快適で、APIレイヤーを意識しなくていいのはだいぶ楽ができている。 SubCollection周りの取り回しやQueryにまだまだ難はあるものの利点のほうがでかいので、別サービスでも積極的にFirestoreは使っていくと思う。

ただ、PVがそこそこあるせいでFirestoreのデータ転送量が意外と多く(もともとFirestoreはめちゃくちゃ安いんだけど)、収入がないの個人サービスを支えていくにはちょっとつらい規模の支出が出ているので、そこはなんとかしたいところ・・・。

インフラ

特に強いこだわりはないがいまのところはHerokuで動かしている。

1台でも余裕で捌けていたのだが、Zero Downtime DeploymentのためにPrebootを使いたかったので Standard-1X dynosの2台構成で今は動かしている。こっちはFirestoreとは違って微々たるもので、5000円/月くらい。

今後どうしていくのか

あくまでマンガアプリのプラットフォーム側の発展を意識して邪魔するつもりや、タダ乗りする気は無く、うまい感じに共存関係を築ければ良いなぁと思っているので、そんな視点でゆるく改善を続けていこうかなーと思っています。

VIVE Proを買ったので所感

www.vive.com

発売日からは若干出遅れたけど、HTCのVIVE Proを買った。

元々PSVRは持っていていくつかソフトも買っていたんだけれど、960x1080の解像度はいかんせん画質の粗が目についてしまってしまい、没入感はあるものの長時間ゲームに集中することが結構できなかった。 そんなわけなので解像度の変化がどれくらい大きく変わるのかが今回一番期待していたところ。

VIVE Pro フルセットが欲しかったんだけれど、完全に売り切れていて入手できなかったので、代わりにHTC VIVE + HTC VIVE PRO アップグレードキットの組み合わせで買った。ドスパラにだいぶ在庫があったので即日買えた。

この買い方をすると実はフルセット版よりも1万円ほど安く買える。 ただ、アップグレードキットの場合はベースステーション2.0が付属していないため、HTC VIVEのベースステーション1.0を使うことになる。

ベースステーション2.0は調べた限り一番の違いは、対応範囲がかなり広くなる(4台まで組み合わせると最大10m四方のスケール)という理解。自分の場合はそんな広いスペースがあるわけでもないのでとりあえずこの組み合わせで良いかなーと。HMDが2つになっちゃうけど、旧式は未使用なら2万円前後で売れるみたいなので実質フルセット版より3万円程度安く買える計算になる。

買ったものと所感

PSVRよりも若干浅いかぶり心地で最初は違和感あったけど、慣れるとVIVEの方がフィット感高かった。 PSVRはすぐ上下にズレて視点がぼやけてしまうので位置を直すことが必要だったけど、VIVEはほぼそれがない。

全体的に少し解像度が高いだけでここまで変わるのかー、という印象。あと、体感型のゲームの種類が豊富なので個人的には◎。

store.steampowered.com

ボクシング音ゲーYoutubeの音源で音ゲーをできるやつ。超楽しい。 ゲーム内のメニューの操作性と検索性は致命的に悪いので、公式サイトでSteamアカウントでログインして、「気に入った曲をStar付けまくる→ゲーム内で自分のプレイリストからプレイ」する方式が快適。

store.steampowered.com

ボクシング音ゲー。(たぶん)オリジナル曲が25曲ある。同じボクシング系でもSoundboxingとは結構違う。こっちの方が汗かく。楽しい。 曲数追加に期待。

store.steampowered.com アーケード系の音ゲー。上の2つはスポーツ感が強いのに対して、こっちは音ゲー寄り。音ゲーとしての難易度も上2つより高いかな?という感じ。全体的綺麗だし、音ゲーの中に入っている!感が強いので楽しめる。

store.steampowered.com

刀、弓、銃のアクションゲーム。Waveもの。 銃のリロードとか刀の取り出しとかがリアルなのもおもしろい。 個人差はあると思うけど、この手のアクション系は割と酔いやすいんだけど、このゲームは全く酔わなかった。

store.steampowered.com

超楽しい。ついに転生した感がある。 ワープ方式での移動ができるので、PSVR版よりは酔わないなーという感じだけど、それでも他のゲームと比べると酔う。楽しいけど連続してできるのは1時間くらい。 せっかくのVRなので没入感高めるためにもテクスチャやエフェクト系のMod入れまくって画面綺麗にしてからやるのがおすすめ。

一緒に買ったサードパーティ製品

T&B HTC Vive用 革材 フェイスクッション 12mm VR MASK

T&B HTC Vive用 革材 フェイスクッション 12mm VR MASK

純正のスポンジは水洗いできないので、早々にこれに切り替えたけどフィット感が若干上がったのと値段的にも安価だし丸洗いできるので良かった。オススメ。

汗対策で紹介されてたので買ったけど、サイクリングとかと違って家で使う用途なので100均で売っている髪速乾用の被るタオル(正式名称知らない)のほうがコスパ良いかも。

グルーディア 燻製器を買ったので長谷園のいぶし銀と比較した

2年ほど前から燻製にハマっていて、長谷園のいぶし銀っていう燻製用の土鍋を愛用して使っている。

そこそこお値段がするだけあって結構本格的なものまで作れて、これで作る燻製たまごやベーコンはマジで美味い。

けど、いぶ し銀にも欠点はいくつかあって

  • 調理時間に最低25分〜掛かる
  • 食材の下ごしらえとかも必要
  • 煙に関してはほぼ無煙だが、そこそこ燻製臭は部屋に残る

ということもあり、土日とかに「やるぞ!」と思って気合を入れた日じゃないと作りづらい。


そんなわけもあり最近は家で作る頻度が落ちていたが、 グルーディア 燻製器 というものを知ったので買ってみた。

f:id:rinrin900:20180324130942p:plain

こんなチャッカマンの進化版みたいなものでどれくらい燻香が付けられるのか半信半疑ではあるものの、色々作って比較してみた。

ちなみにはスモークチップはヒッコリーを使用した。

いぶし銀

f:id:rinrin900:20180324131152p:plain おなじみいぶし銀。まだ火をつける前だけどこの時点で既に美味そう。

グルーディア 燻製器

f:id:rinrin900:20180324131216p:plain いぶし銀の網がボールにちょうどサイズがあったので、二重底にして食材を並べる。

f:id:rinrin900:20180324131237p:plain ボールにラップを念入りに被せて、付属のチューブを差し込んで煙を入れる。

完成

f:id:rinrin900:20180324131828p:plain 左がいぶし銀(燻製時間20分)、右がグルーディア燻製器(燻製時間10分)

できあがりはこんな感じ(鳥のササミとししゃもは別途焼いた状態)。

グルーディア燻製器で作ったほうは「若干色味が付いたかな?」程度。でも、香りはある程度しっかり付いている模様。

実食

いぶし銀は熱燻、グルーディア燻製器は冷燻なので、たこやサーモンなど食感が変わっているものもあるが、個人評価ではこんな感じ。

食材 いぶし銀 グルーディア 燻製器 一言コメント
たこ グルーディアは刺し身の状態で燻製されている
食感でとても新鮮
ベビーチーズ
ししゃも ほとんど燻香が付かなかった
ササミ ほとんど燻香が付かなかった
燻製たまご ほとんど燻香が付かなかった。
固茹でにして半分切ってからやるといいかも。
サーモン 酸味が出てしまった。
ポップコーン 新しい感覚。
ポップコーンはマッシュルーム型より
チューリップ型が味が染みそう

全体的にはいぶし銀に劣るが、熱を加えずに燻製ができる(手軽に冷燻ができる)という点は評価できそう。

熱燻の燻製器と比較するというよりは、食材や用途が棲み分けできるので共存できそうな印象を受けた。 たこやポップコーンしかり、冷燻で映える食材を開拓していくのはとてもおもしろそう。

個人的な課題としては、煙が結構漏れてしまうので蓋付きの専用の容器が欲しいなぁと。

なにかいい食材見つけたら追記します。

最近の自分の働き方と思ったこと

家でリモートで働いていると移動時間とかも気にせず延々とコードを書き続けられるわけで、とはいえ仕事のコードばっかり書くわけにもいかないから趣味のコードも書く。

ただ仕事の進捗を遅延させるわけにはいかないので、必然的に仕事を終わらせてから1日の中の余った時間で趣味の方を書くことになる。

この感覚がなにかに似てるなあと思ったんだけど、昔ネトゲをやってた頃に似ているのかなあと。

10代の頃は毎日12-16時間くらいネトゲ(MMO)をやってたけど、そのゲームでは効率が良いと1時間80-100万経験値くらい稼げるので「毎日1,000万/経験値を稼ぐ」みたいなノルマを自分に課していた。 で、ノルマを達成したらレアアイテム狙いの狩場に移動してもいいしPvPやっても装備強化してもいい、みたいな。

特にオチもないんですが、「わかるわー」って人と語り合いたい気持ち。

Cloud Functionsトリガー実行時にFirestoreに認証情報を渡す

firestore.ruleで request.auth を使って「レコードは自身の保持しているデータしか操作できない」ような制約を掛けるケースはよくあると思います。

match /profiles/{uid} {
  allow read, create, update if resource.data.userUid == request.auth.uid;
}

普通に操作する分には問題ないんですが、例えばCloud Functionsの認証トリガーを利用して「ユーザが更新されたら、ユーザに紐づくプロフィール情報(Document)を更新したい」ようなケース。

exports.updateExternalProfile = functions.auth.user().onUpdate(event => {
    const user = event.data;
    const email = user.email; 
    const displayName = user.displayName; 

    db.collection('profiles').doc(user.email).update({ displayName })
});

そのまま更新すれば良いように思えますが、Cloud Functions内でのこの外部トリガーからの実行処理中はユーザログイン情報がないため、つまり request.auth がないので更新しようとすると権限エラーになります。 Cloud Functions + Firestoreを使っていると苦しむこの問題。どうするか。

github.com

Issuesも上がっていますが、2018年03月時点ではまだfirebaseAdminでは解決していない模様。ちなみにRealtime Databaseは同様のことを満たすための databaseAuthVariableOverride っていうオプションがあるみたいですね。


ではどうすれば良いか。 認証情報を冗長に持つ必要があるものの、firebase-admin-node に加えて firebase-js-sdk を使って、signInWithCustomTokenメソッドを使ってログイン状態にさせると一応動くようになります。

const admin = require('firebase-admin');
const serviceAccount = require('/path/to/serviceAccount.json');
admin.initializeApp({
  credential : admin.credential.cert(serviceAccount),
});

// 追加処理
const firebase = require('firebase');
require('firebase/auth');
require('firebase/firestore');
firebase.initializeApp({
  // webAppの設定
});

const signIn = async (userUid) => {
  const customToken = await admin.auth().createCustomToken(userUid)
  return firebase.auth().signInWithCustomToken(customToken)
}

exports.updateExternalProfile = functions.auth.user().onUpdate(async event => {
  const user = event.data;
  const email = user.email;
  const displayName = user.displayName;

  // 追加処理
  await signIn(email)

  db.collection('profiles').doc(user.email).update({ displayName })
});

こんな感じ。 認証周りの冗長感が否めないので、早くfirebaseAdmin側で対応してくれないかなー。

FirestoreのSubCollectionに対してQueryが使えない問題にどう立ち向かうか

Firestore、便利ですよねぇ。 ただ、2018/03執筆時点ではまだβなので荒削りだったり要件満たしにくい部分でつらいなぁっていう部分はいくつかあります。

その1つが表題の件の「SubCollectionにQueryが使えない」問題。 どういうことかというと、例えば「CDごとに曲名とその曲番号(連番)を保持する」構造を例に考えてみます。

- albums [collection]
    - title
    - musics [collection]
        - title
        - songNumber

Firestoreでどう表現するかというと、disksっていうCollectionを作って、その中にSubCollectionとしてmusicsを持つ形が一番シンプルに見えます。

f:id:rinrin900:20180309180400p:plain

こんなイメージ。ただ、この構造の場合は表題の制限があり、musics内のデータを直に参照したいケース、 具体的には「ユーザ属性は曲のタイトルを持っているが、その曲がどのアルバムに属しているか分からない(紐付けることができない)」時に、sounds内のデータを特定することができません。 この件についてStackOverFlowGoogleエンジニアが「Collection group queryという機能で提供する予定だが、提供はすぐではない」という回答をしています。

ObjectやArrayとして下位データを持つ方法もなくはないですが、そもそものSubCollectionのメリットを活かせないのではここでは考慮から外しつつ、 Collectionであることメリットを活かし現時点で解決できるパターンをいくつか見ていきます。

親CollectionにSubCollectionのキー一覧を持つ方法

f:id:rinrin900:20180309181719p:plain

親のCollection側に、soundsのキー一覧を持つのが一案としてあります。

soundKeys: [
    "1. 主よ,人の望みの喜びよ(In 4 Mix)"
    "2. 眠れる森~Sleeping Forest",
    "3. Mouring The Passing Time"
]

キー一覧を持つ際、上記のようにFirestore内のデータとして配列で持ちたくなります。 が、Queryでwhere in句のように配列を対象に検索することができないので、以下のようなObject形式で持つ必要があります。

soundKeys: {
    "1. 主よ,人の望みの喜びよ(In 4 Mix)": true,
    "2. 眠れる森~Sleeping Forest": true,
    "3. Mouring The Passing Time": true
}

sound側のkeyを持っている状態からこのalbums -> soundsのdocを取得する場合には

const userPlayingTitle = '2. 眠れる森~Sleeping Forest'
const querySnapshot = await db.collection('albums')
    .where(`albums.${userPlayingTitle}`, '==', true).get()
...

というような形でquerySnapshotを持ってくることができるようになりました。