我が名はなんとか菜である!

主に技術系の記事を書きますが、ポエムも混入します。

宝鐘マリンボタンを支える技術【GatsBy.js → Next.js for SSG】

最近作ってるやつについて話します。

ページはこちらです。

宝鐘マリンボタン🏴☠

OGPの設定がよく分かってないのでタイトルにリンクを貼っておきます。

ボタンを押すと宝鐘マリンの音声が流れるだけのシンプルなアプリです。
同様のテーマのサイトは過去にいくつか存在し、さなボタン宇志海ボタン などがあります。

そもそも宝鐘マリンとは

宝鐘マリンとは、カバー株式会社が運営しているhololiveというバーチャルライバーグループに所属しているライバーの、3期生の一人です。

www.youtube.com

宝鐘海賊団の船長で、愛称は『船長』ですが、
現在海賊船を所有しておらず、今のところはただの海賊コスプレ女です。(公式プロフィールより)

www.hololive.tv

技術スタックについて

ざっくり以下のような構成です。

  • TypeScript

    • んんwwww今時直接JavaScriptでアプリを書くのはあり得ないwwwww(役割論理)
    • FlowTypeはもっとあり得ないwwww
  • React.js

    • Viewライブラリ
  • Redux

    • State管理ライブラリ
  • react-redux

    • ReactとReduxの繋ぎこみを楽にしてくれるライブラリ
  • GatsBy.js

    • コンパイル時にReactアプリを実行してHTMLを生成してくれるフレームワーク(これを俗にサーバーサイドジェネレーター, SSGと呼ぶ)
      GraphQLクライアントが統合されており、アプリのビルド時にGraphQLを用いて
      データを事前に組み込むことができる。
      どういった種類のデータが組み込めるのかは、入れるプラグイン次第
    • とある理由により、現在は Next.js に移行済み
  • Next.js

    • 現在メインで使っているフレームワーク
    • Reactアプリをサーバーサイドレンダリング(SSR)するためのフレームワークとして登場し、
      長い間その領域ではトップの使い勝手のよさでじわじわと使われていた
    • しかし、 node.js 上で動かさなければならないので実際に運用しようとすると
      node.js が動かせる環境を用意する必要があるのと、
      POSTパラメータを取得するためには自前でExpressを扱うコードを書く必要があり、
      実際に一通りのチュートリアルをこなすのはそれなりにパワーがいる
    • しかし、 ver 9.0 から状況が変わり、なんと GatsBy.js と同様な SSG をこなすことが可能になった
    • 個人的に Next.js のAPIGatsBy.js に比べて直感的だと感じるので、移行してよかったなと思った
  • Docker

    • Webフォントは、配布されているそのままを使うと容量が大きすぎて初期描画時間に悪影響を及ぼすため、 サブセットフォントを生成して容量を削る必要があった
    • 今までは手動でサブセットフォントメーカーというツールを使って生成していたが、流石にアホらしくなってきたので
      なんとか自動化してえなと調べたところ、依存するツール/ライブラリが多かったのでコンテナで実行することにした

頑張ったところ

音声の追加作業を楽に

流石に音声データを一つ一つ配置してそこに手動でリンクを貼るという作業を毎回するのはだるいというレベルではないので、出来ればディレクトリに音声ファイルを配置したら自動的にボタンも生成されるようにしたいものです。
なので、「コンパイル時に特定のディレクトリを舐めてファイル一覧を出力してくれるなにか」が欲しいなと思っていました。
すると、どうやら GatsBy.js を使うと GraphQL を通してディレクトリ配下のファイルパスを取得することが出来るらしいのを知り、「これは使えそうだ」となりました。
まあ、結局 Next.js に移行することにしたので、現在その機能はシェルスクリプトのfindで置き換えられました。(そういうこともある)

初期描画時間の短縮

Reactアプリは性質上、どうしても初期描画がワンテンポ遅れます。
前述の通り GatsBy.js の SSG を利用するとこの初期描画時の重さを改善することが期待できます。
実際、 Lighthouse のスコアではそこそこ良い感じになってます。 f:id:happo31:20200610004859p:plain

満点にしてぇ~

大量のボタンがある割には早いんじゃね?と思っていますがいかがでしょうか。

GatsBy.js をなぜ諦めたか

GraphQLを使えることがそんなに嬉しくなかった

よく考えなくても、ディレクトリ配下のファイルを舐めるだけなら変な小細工をせずにシェルスクリプトとかでも十分作れるんじゃね、と思って軽く書いてみたら30分ぐらいで出来てしまいました。

#!/bin/bash

cd static/audio

# $IFS を書き換えて、\n をトークンの区切りとして扱うようにする
OLDIFS=$IFS
IFS=$'\n'

files=$(find . -type f | sort)

# いちいちオプション書くのがだるいので改行させないechoを定義
_echo() {
    echo -n $@
}

# 要するに "a" "b" "c" という感じの文字列の配列を作ってる
# よく考えたらjsonっていう命名はおかしい気がする
json=`for file in $files[@]; do echo \"${file:2}\",; done`

# ${json%,} で list.join(".") 相当の処理が出来る
_echo [
_echo ${json%,}  
_echo ]

# このシェルスクリプトをファイルに対してリダイレクトすればトップレベルが配列のjsonファイルの出来上がり

# IFS は戻しておく
IFS=$OLDIFS

あっさり塩味。

で、これを package.json の "prebuild" などでこんな感じで呼ぶと、コンパイル時にファイル一覧が取得出来ます。
(update_resources.sh は、リソース更新処理周りをまとめたシェルスクリプトです、他のリソースの生成とか特定のファイル名のファイルに出力をリダイレクトしたりとかしてます)

// ....
  "scripts": {
    "updateResources": "./bin/update_resources.sh",
    "prebuild": "npm run updateResources",
    "build": "next build",
    "start": "next start",
    "export": "next build && next export",
    "predev": "npm run updateResources",
    "dev": "next"
  }
// ...

GraphQLで取得できるデータの内容にプログラマー裁量権がない

例えば、特定のディレクトリ以下(どこのディレクトリを見るのかはconfigファイルで行う)のファイル一覧の情報を取得するには以下のようなGraphQLを書く必要があります。

query {
  allFile(
      sort: {
        fields: [name],
        order: ASC
      })
   {
    edges {
      node {
        name
        relativePath
      }
    }
  }      
}

GraphQLを知らない人のために補足しておくと、
GraphQLは書いたスキームと全く同じ構造のデータが取れるというのが最大の特徴であり、利点でもあります。
しかし、今回の場合はデータが allFile.edges.node という構造に依存してしまいました。
「いやでもそれは別にリポジトリパターンでなんとかなるでしょ」という意見もあるかもしれませんが、ぶっちゃけこのアプリで持つデータは今のところ音声データの一覧と再生状況だけなので、コスパが悪すぎるということで実装されていません、この辺もきっちり作れたらいいんだろうけど。

あと、もしかしたら Gatsby.js の設定、あるいは graphql のクエリ次第でなんとかなるのかもしれないけど、そこまで詰めなきゃいけないならじゃあもうやらんでいいや、という怠惰とも取れる理由でやめてしまったのでよくわからないです。

今後の実装予定機能

  • ランダム再生
  • ループ設定
  • 音声ファイルの投稿フォーム

などがあるといいなぁと思いました。
なお、余裕はあまりない。

以上、こんな感じでした。
これからもマリン船長を推していきたいです。