📖

グループ読書が少し便利になるアプリ「ぐるどく」の技術構成

2023/11/13に公開

グループみんなで EPUB ファイルの本文にコメントができるリーダーアプリ「ぐるどく」を作りました。iPhone/iPad/Apple Silicon Mac に対応しておりますので、下記リンクよりお試しください。
https://apps.apple.com/jp/app/id6465693672

経緯や想いの話は別途 note に記事を公開しているので、気になる方はこちらも合わせてご覧ください。
https://note.com/seiyamo/n/nc07cc8e05c04

これは何ができるの?

「ぐるどく」でできることはシンプルです。

  • EPUB 形式の本が読める
  • 本文にグループメンバーそれぞれがコメントが書けて、それを確認できる
  • その他ブックリーダーに必要な機能がそれなりに使える

Kindle にもメモ機能があると思いますが、あれをグループで使えるようにしたイメージを持ってもらうのがわかりやすいでしょう。

技術構成

私が考えた条件は下記でした。

  • 縦スクロールではなく Kindle のように横にページを捲るように読めること
  • モバイルだけではなく PC でも使えること
  • 本文の任意の箇所をハイライトできること
  • オフラインでも本が読めること
  • EPUB ファイルはサーバーにアップロードせず、端末上のみで管理すること
    • 権利周りとか考えたくない、私が見ようと思ったら見れてしまう状態も避けたかったため

サーバーとインフラ

サーバーは Node.js と TypeScript で実装しました。普段は Express を使って GraphQL サーバーを作ることが多いのですが、今回は Fastify を使ってシンプルな REST API を実装しました。このへんはあまり時間をかけたくなかったためです。気に入らなくなったら後で置き換えればよいかなと思っています。

インフラは慣れた Google Cloud にし、DB は Firestore、認証は Firebase Auth にしました。このあたりも慣れの部分が大きいですが、Firestore はオフライン対応やリアルタイム更新検知機能が簡単に使えるため、今回のアプリにマッチしていたのもあります。

アプリ

「ぐるどく」はリーダー部分を除けば、サインアップ/サインインや蔵書管理・グループの管理など比較的単純な CRUD 機能が多いです。何で開発しても体験はほとんど変わらないと考え、クロスプラットフォーム対応もでき、私自身サクサク開発できる Flutter を採用しました。

次節で詳しく触れますが、リーダー部分もあわよくば Flutter でゴリゴリ作ってやろうと考えていたのですが、それは泣く泣く断念しました。

リーダー

EPUB ファイルには HTML/CSS が含まれており、それを実装するのはもはや Flutter でブラウザを作るようなものです。しかも実際の本のように改ページがあり横に読んでいくような体験にするにはなかなか骨が折れます。作り始めた当初はできるだけ早く使いたい事情があったので、サードパーティの Flutter 対応のライブラリ(もしくは iOS/Android それぞれのライブラリ)を探していました。ですが、かなり昔に開発がストップしていたり、縦スクロールでの読書にしか対応していなかったりと、私が求める機能を備えているものが見つけられませんでした。

そこで視野を広げ、Web で EPUB を読めるライブラリを探し始めました。そこで見つけたのがepub.jsです。このライブラリは、私が求める機能の大部分を備えていました。
https://github.com/futurepress/epub.js

このライブラリをアプリから使うには、WebView を通して Web アプリとして使うことになります。このライブラリをざっと触ってみた感じ、EPUB ファイルは HTTP 通信で取得してきてレンダリングするようになっていました。しかし私としてはオフライン対応をしたいし、EPUB ファイルをサーバーにアップロードもしたくありませんでした。そこで思いついたのが以下の図ような方法です。

これは、Flutter アプリ内にサーバーをたて、そこに向けて WebView からアクセスし、Flutter 側で HTML ファイルや EPUB ファイルを返却しています。HTML ファイルは React で実装した Web アプリを Vite を使って 1 つのファイルになるようにビルドしています。できあがった HTML ファイルは予めアプリにバンドルしておくことで、オフライン対応を実現しています。やろうと思えばリーダー部分だけですが OTA アップデートも可能でしょう(めんどくさいのでやってないですが)。

Flutter でローカルサーバーをたてるのは、コードで書くと以下のような感じです。localhost で使われていない port にサーバーを立ててくれます。

final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
server.listen((request) async {
  // 実際はこんな適当じゃないが、HTMLを返すイメージはこんな感じ
  request.response
    ..headers.contentType = ContentType.html
    ..add(utf8.encode(htmlContent))
    ..close();
});

あとは React で epub.js を使ってリーダーを作るだけ!なんですが、コメントを投稿したり取得したりといった、データの取り扱いで少し苦労しました。

プロトタイプとして一旦使えるもの作ったときは、React(WebView)側でデータ通信を行うようにしていました。Flutter アプリ側から React 側へ Firebase Auth の Custom Token を渡し、React 側でログイン処理をします(Custom Token とは特定のユーザーとしてログインできるトークンで、サーバー側でしか作れないようになっています)。そうすると Firestore からコメントデータを取得したり、コメントを投稿できたりします。

しかし今はこの方式を使っていません。この実装をしたときは早く使いたくてすごく急いでいたのもあって、すごく馬鹿な選択をしていました。この方式だとオフライン対応が難しくなってしまいます。ログイン状態を作るために必ず Custom Token を発行してもらう必要があり、それは「ぐるどく」のサーバーと通信が必要だからです。また、Firestore との通信も React 側で行う関係で、localhost だったからなのかはあまり深く追っていませんが、ローカルキャッシュが全然上手く動きませんでした。

今は Flutter アプリ側ですべて通信をおこない、JavaScript を通して React 側にデータを渡しています。こうするとアプリ側ですでにログイン済みであるため余分な通信は不要ですし、Firestore のローカルキャッシュも存分に使え、オフライン対応は特に意識しなくても実現可能になります。React 側で通信する方法は、React アプリを作っているのと変わらないので、PC でブラウザからデバッグが簡単に行えるメリットはあったものの、動作の安定感的にも現在の実装のほうが圧倒的によい選択でした。

開発期間

2023 年の 2 月ごろに開発を始め、だいたい 60 時間くらいで最低限動くものができました。上記 note の記事でも触れましたが、私が参加している技術書を輪読しているグループで使い始めました。使っていると上述したデータの取り扱いのミスや、その他たくさんの不具合が出てきましたが、体験としては良いものでした。9 月ごろから本格的にリリースに向けての対応を始め、9 月下旬ごろにリリースすることが出来ました。

おわりに

リーダー部分が Web ベースであるために、読書単体の体験として最高とは決して言えません。世にある様々な EPUB ファイルで動作を検証したわけではなく、書店 EC から購入できる EPUB のみでの動作確認となっているので、その点でもまだまだ改善できると思います。

それでもグループで読書するためのリーダーとしてはいいものになっているので、お試しいただけますと幸いです。

https://apps.apple.com/jp/app/id6465693672

Discussion