Chienomi

Chienomiのコメント機能を刷新、遅延評価SPAに

開発::web

Chienomiのコメント機能を刷新した。

PureBuilder Simply組み込みのコメント機能としては4世代目、ChienomiはWordPress時代があるため、Chienomiのコメント機能としては5世代目になる。

コメント機能はこの5世代を通じてほとんど使われていないが、あったほうが良いものではあるので、使いやすくする方向に改善した。

大きな変更として、従来はサーバーアプリケーションがHTMLを返すSSR形式だったが、ページ変遷を行わないSPA形式に変更した。 もちろん、Vanilla JavaScriptで書かれている。

今回は自身の実力向上を感じる要素もあった。

コメント機能の基本的な方針としては従来を継承している。 特徴的な要素である「自身が何者であるかを名乗る」も引き継いでいる。

遅延評価モジュール

今回の新しい工夫点がこちら。

コメント機能のようなほとんどの人が必要としないものを(ユーザーのリソースを使って)勝手にロードするのは好ましくない。 そこで、コメントを開いたときにロードするようにした。

そもそもの「コメント開く」という処理だが、個人的にコメントというのは不快な要素であることが多い(YouTubeなど)ため、「勝手に見せるな」の気持ちが強い。 なので、コメントはユーザーが明示的に開いた場合のみ表示するようにしている。

この場合、「コメントを開く処理」が入っていないといけないわけだが、それに関してはbase.js内でイベントリスナーだけは定義してある。 このイベントコールバックでは、新たなscript要素を作ってheadに追加している。このイベントを発生させるボタン自体が消えるのでイベントの再発火はないはずだが、ここで読まれるscript要素が作る要素を参照し、存在する場合はreturnするようになっている。

このscript要素で読まれるcomment.jstype="module"のスクリプトである。

モジュールになっている大きなメリットは、importimport()が使えること。 これでスクリプトの重複ロードを避けるのが容易になり、これによりfetchwrapperをロードしている。 fetchwrapper関連のドキュメントが不正確なので苦労してしまったことから、修正した。

comment.jsはイベントリスナーを設定するのではなく、それ自体がDOMコンテンツを生成する。 ちょっとダーティに感じるが、comment.jsのロード自体がclickイベント起点で始まっているため、ユーザーの体感としては問題ない。

コメント送信部分は単純なフォームであり、JavaScriptの関与がない。

GDBM vs LMDB

コメント機能のサーバーアプリケーションは第1/2世代ではdbmライブラリ(汎用のdbmインターフェイス)を使っていた。

第3世代はファイルシステムベースのYAMLファイルになっている。 これは自作のオブジェクトマッパーを介している。 このオブジェクトマッパーは、オブジェクトストレージ的な機能と、透過的シリアライゼーションを兼ねている。

最も時間がかかった要素がこのデータベース部分をどうするかというところで、DBMに戻すのが本線だと考えていた。

ところが、DBM的なKVSで現在もメンテナンスされているのは、GDBM, CDB, LMDBあたり。 Tokyo Cabinet, QDBM, Tkrzwももう利用は難しく、放置されている状況だ。 Berkeley DBももう7年ほど更新がない。

DBM的なKVSは先行き不安、下手な選択をすると大変なことになる可能性がある。

個人的には取り回しの良さを重視しており、ファイルベースのほうが良い。 とはいえDBM系KVSの状況は悪く、最悪、sqliteやMongoDBを使うことも考えた。

オブジェクトマッパーを使い回すことも考えられるが、オブジェクトマッパーはかなり複雑な上に、雑に作られているものなので使い続けるのはちょっと気になる。

最終的に色々と調べた上で実験し、GDBMもしくはLMDBを候補とした。

ここからはRubyから使う上での話になるが、最も大きいのは並列安全性である。

GDBMの場合は

GDBM.open("db.db") do |dbm|
  data = Marshal.load dbm["record"]
  dbm["record"] = "foo"
end

のように使い、LMDBは

lmdb = LMDB.new("lmdb")
db = lmdb.database

lmdb.transaction do
  db["record"] = "foo"
end

lmdb.close

のように使う。 比較するとLMDBはややコードが長く、複雑になっている。

両者ともRubyスレッドセーフになっているが、挙動が違う。

GDBM.openはすでにデータベースを開いているスレッドが存在する場合、Errno::EWOULDBLOCKまたはErrno::EAGAINを発生させる。 GDBM::NOLOCKではこの例外を無視することはできるが、ブロックすることはできない。

LMDB::Environment#transactionは排他ロック(transaction(true)で共有ロック)し、ブロックされる。

アクセスが激しい状況を考えるとブロックされるのは望ましくないため、データベースへの書き込みが排他ロックというのは近年は嫌われるが、扱いやすさで言えば一番だ。

このため使い勝手はLMDBのほうが優れている。 gdbmライブラリはRuby 2.5.0でDefault gemsになっており、Ruby的な取り回しの違いもあまりない。

が、コメントへのアクセスは本当に少ないので、競合の問題はかなり小さい。 Default gemsのほうがいくらかメンテナンスを信頼できるだろうということで、GDBMを採用し、競合エラーに対してはタイミングをずらしてretryするようにした。

KVS vs RDBの入り口な部分

私はRDBが嫌い(というか、SQLが嫌い)なのだけど、絶対使わないというほどでもない。 いや、私自身が使う機会はほとんどないし、GitHub上にはひとつもないけど、実際はsqliteぐらいなら使うこともなくはない。

だいたいの人は無条件に(習慣的に)RDBを選択するけれど、じゃあフラットに適したほうを使いましょうとなった場合どちらを選ぶかには割とシンプルな指標がある。

まず、ユニーク文字列のあるエンティティを登録していくだけならKVSが圧倒的に良い。

エンティティの登録と単一エンティティの取得がほとんどである場合も、KVSのほうが良い。

検索が主体になる場合はRDBのほうが良い。 特に複合的で複雑な(ケースによって異なる条件での)検索を行う機会が多いと、KVSは割と厳しい。

これがNoSQLと呼ばれるようなデータベース(MongoDBのような)になると話が全然変わってくる。 というか、MongoDBはだいたいの場合は万能である。 取り回しは良くないが。

モデレーションと管理者コメント

第3世代でオブジェクトマッパーを使っていたのは、コメント管理を楽にするためでもある。

今回は今までよりはちゃんと作るようにしたため、こうした処理はコマンドラインユーティリティで行えるようにした。

DBMを使っているとデータベースアクセス部分のコード量が少なく、要求されるライブラリも少ないので、コマンドラインツールを書く方法も非常に取りやすい。

サーバーアプリケーションの形態

引き続き、Sinatraで書かれているBasic Web Appsを拡張する形になっている。

この点はあまり特徴がなく、RackHandler/CGIになっているやつのように語るところもない。

海を泳がずに作る

私にとってプログラミング時に最も価値のある時間は、深い思索の海を泳いでいる時間だ。

思考を深めて没頭していく中で、自身の肉体という感覚を失い、言語も緩やかになりながらプログラムを考える。 それこそ、まるで自分がプログラムやデータベースやコンピュータになったかのようにだ。

この深い思索によって出された結論は、ほとんどの場合非常に良いものだ。

この時間は、外から見ていると「何もしていない」ように見える。 そもそも意識がそこにないから、寝ているのと同じような状態だ。

そこまで深い、一種ゾーンのような状態に入るのはやはりそれなりに難しいものだが、深めやすい状態はあるので、集中力が高まっていかない、もしくは明確な言語での思考が続いてしまうときは思考しやすい環境に身を置くこともある。

最も思考を深めやすいのはシャワーを浴びているときなので、服を脱ぎ捨てて蛇口をひねる、ということが多い。 もっとも、これをやると水道代はすごいことになる。

まぁ、難点もいくつかある。 一番は、自分の肉体を認識できていないため、割と事故を起こす。近くに飲み物をおいていたりすると非常に危険だ。 それ以外にも、時間を完全に見失うというのもある。 また、戻ってきたときにその過程で何を考えていたのかあまり覚えていない、というのは効率の面でよくない。うわ言のように言葉を発していることもあるので、音声を記録しておく、みたいなことをするときもある。

このような方法で書かれたものの代表例としては、PureBuilder SimplyやAltPostLocalなどがある。

そこまで深い思考に入らずとも、ずっとそのことを考えている、というときもある。 断片的な思考を続けている中で脳内でソフトウェアの最適化が進み、書き始めるときには明確なゴールを意識した状態で始められるというものだ。

この方式で書かれたものは、DLsite Voice Utilsなど。

対して今回は、最初から理詰めで作っている。

「コメント機能を改善しよう」という発想から始まり、現在の問題点を列挙、それに対して望ましい状態を定義してゴールを定め、ゴールに近づくように順番に作っていった。

手法の違いは、今回は元となるものが存在するというのが大きい。 「最適なイメージ」を突き詰める場合は、元となる存在が足かせになって実現しない、もしくは歪なものになってしまうことが多々ある。

それと、今回はまとまった時間がとれなかった。 思考を深めて一気に書く場合はどうしてもまとまった時間が必要になるので、時間が断片化する上に時間をとれるタイミングが限られるとなかなか進捗しないことがよくある。

こうした事情から手法として取りにくかったというのもあるし、理屈でまとめなければいけない要素が多かったのもある。

理由はともかくとして、とにかく今回はゴールに必要なものを満たすために端っこから手を動かす、という方法を取った。 なので、コードクオリティはおそらく普段と比べるとだいぶ低いが、少なくとも欲しいものは満たしているように思う。 CSSは(サイトのテーマ感に合わせるのもあるが)そんなに凝ったものではないから見栄えしないかもしれないが、機能的に不満はないはずだ。

別に大した違いはないのでは、と思うかもしれないが、機能的に同じに見えたとしても、両者は工業製品と芸術作品と同じような違いがある。

ただし、スクリプトロードをクリックイベントコールバック内でやる、というアイディアだけは、シャワーを浴びている間に思いついた。

クローズドソース

サーバーアプリケーションはクローズドソース。 これは、私のウェブサイトのアプリの全体的な傾向だ。

実のところ、新しいコメント機能は設定できる箇所を増やすくらいで公開できなくもないのだけど、前提として想定されている環境が完全に固定されているので汎用性がなく、公開する意味が薄いため公開の予定はない。

それに、クライアント側は想定が完全に固定されていて、構造の異なるウェブサイトに適用できるようになっていない。 使っているテクニック的にもコードを書ける人間が組み込まなくてはいけない感じなので、これも公開に適さない理由のひとつ。