entry-header-eye-catch.html
entry-title-container.html

entry-header-author-info.html
Article by

ピクシブ百科事典のフロントエンドをリファクタリングした話

こんにちは。福岡オフィスで課題解決部に所属しているエンジニアの petamoriken です。趣味で ECMAScript の動向を追ってたりします。よろしくお願いします。

この記事では私がレガシーだったピクシブ百科事典のフロントエンドを如何にしてリファクタリングし、モダン化していったかを紹介していこうと思います。

まずピクシブ百科事典の構成の調査

ピクシブ百科辞典のフロントエンドのコードは複数の JavaScript がクラシックスクリプト形式で読み込まれ、実行されていました。もっと詳しく説明すると、ページ共通のエントリーポイントの中で今どのページにいるかの判定をし、そのページに必要なスクリプトを LABjs というライブラリを使って動的に <script> タグを追加することによって読み込んでいました。

これにより別のコードで定義されたグローバル変数を使うのが当たり前の状態になっていました。もちろん読み込む順番にも気を配らなければなりません。

ということでまずはじめにファイルの依存関係がどうなっているかを調べることにしました。ピクシブ百科辞典は PHP による MPA 構成なため、幸いなことにフロントエンドの責務はイベントを登録してレンダリングを変えるのが主なのでそれほどファイル数も多くありません。

ESLint を導入し、no-undef ルールを使って外部ファイルで定義された変数を使っている箇所の洗い出しをしました。ついでに全部のファイルを触るということで TypeScript 化も進めました。拡張子を .ts に変更し、ESLint と TypeScript によるエラーを一つずつ潰していくという地味ですが今後のためになる大事な作業をこなしました1

リファクタリングの方針を決めて、実行

ファイルの依存状況を調べ、書き換えながら、今後どういう方針でリファクタリングしていくかを固めました。

  1. モジュールの依存をネイティブの ES Modules に移行する
  2. レガシーなライブラリからモダンなライブラリへと移行する

1. モジュールの依存をネイティブの ES Modules に移行する

LABjs に依存したままなのは不健全なので、エントリーポイントを <script type="module"> で読み込むこととし、別モジュールの変数を使う必要がある箇所について整理して、グローバルを経由するのではなくちゃんとモジュールとして管理できるようにしました。また動的に読み込む必要のある箇所についてはネイティブの Dynamic Import に移行しました。

<script type="module"> を使った場合 defer として扱われるため DOMContentLoaded より前のタイミングで実行したいコードは別ファイルに切り出す必要があります。幸いなことにエントリーポイントから読み込まれるコードの中に該当する部分がなかったため、簡単に移行することが出来ました。

webpack のようなバンドラーを使うことも考えましたが、Dynamic Import を多用する設計になっているためバンドルしてもそれほどファイルが結合されず、配信速度が速くなりそうもありません。今回は移行のしやすさなど総合的に考えてネイティブ機能を使う選択をしました2

2. レガシーなライブラリからモダンなライブラリへと移行する

調査をしていく中でレガシーなライブラリがまだ使われてしまっていることがわかりました。特に気になったものを列挙すると以下のようになります。

少なくともこのようなレガシーライブラリに依存したコードが今後の開発で増えていってしまうことは防がないといけないと考えました。

古いブラウザのための shim ライブラリは単に取り除き、underscore.date は一箇所で使われているのみだったので Date を使ってなんとか置き換えました3。jQuery については使用箇所が多かったため、今後新たに使わないこととして今回のリファクタリングのスコープからは除きました。

そして Backbone.js と mustache.js によるレンダリングの書き換え部分については熟考した結果 LitElement に置き換えてコンポーネントとして実装することにしました。

何故 LitElement なのか

弊社の様々なプロダクトの中でもっともよく使用されているのは React です。慣れていることもあり当初は React の導入を試みていましたが、以下のような問題がありました。

  • ピクシブ百科事典は検索流入が多く、サーバーサイドによるレンダリングを削って React の CSR を採用するのは SEO に対する影響を考えると厳しい
  • とはいえ BFF サーバーを用意したり Rendertron を採用したりするのは目指していることに対して設計の変更が大きくなりすぎる

可能な限り PHP による HTML のレンダリングを削減することなく、更にその結果を再利用してフロントエンドでレンダリングの書き換えが出来たほうがよいと考え、Shadow DOM を扱える LitElement を採用することとしました。

Preact でも Shadow DOM を扱うことが出来ますが、LitElement は HTMLElement の薄いラッパーであるため、将来的に別のフレームワークに移行する判断になったときに剥がしやすいだろうと思いこちらを選択しました。

Shadow DOM と <slot>

前述したとおり Shadow DOM を採用するとサーバーのレンダリング結果を再利用することが出来ます。

例えばサーバーサイドで以下のように記事の一覧がレンダリングされているとします。

gist.github.com

これに対してフロントエンドで <article-list> に以下のような Shadow DOM をアタッチすることで、サーバーサイドのレンダリング結果を <slot> にそのまま入れることが出来ます。

gist.github.com

このように Shadow DOM を採用した場合、サーバーサイドによる HTML レンダリングについてはフロントエンドで変更されうる部分を Custom Element で囲むだけの変更で済みます。

詳しくは MDN を参照してください。 developer.mozilla.org developer.mozilla.org

最後に

今回は如何にしてピクシブ百科事典のフロントエンドをリファクタリング、モダン化していったかを紹介しました。手を動かし試行錯誤しながらの方針決めや技術選定で大変でしたが、かなりやりがいのあるタスクで楽しかったです。

ピクシブ株式会社では妥協なく技術選定に取り組むエンジニアを募集しています。

https://hrmos.co/pages/pixiv/jobs/002hrmos.co

他にも様々なポジションがオープンしていますので、コーポレートサイトをご参照ください。

www.pixiv.co.jp


  1. 結果的に700個ほどのエラーを手で潰したみたいです。
  2. キャッシュをパージするため配信される JavaScript を加工する必要がありました。今回はビルド時に使われる簡単な Babel プラグインを自作して、モジュールの読み込みパスに params を追加して対応しています。
  3. 早く Temporal が実装されてほしいですよね。
20191219014119
petamoriken
2017年7月から福岡オフィスでアルバイトをしている。元々フロントエンドエンジニアだったがiOSアプリやサーバーサイドの開発も携わっている。 午後のおやつと同じくらいECMAScriptやDOM APIの仕様を追うのが好き。百合男子。腐男子。