結城です。
筆者個人に寄せられたWaterfoxのサイドバーUI開発に関するご相談を当社の業務として手がけた件(参照:Waterfoxプロジェクトのブログにおけるアナウンス)について、前編の記事では交渉と作業内容の検討段階の経緯をご紹介しました。
前編では「Waterfoxのためのモジュール開発」の趣が強い話でしたが、最後の段になって、実は最終的な落とし所はそれとはやや見え方が異なる「WaterfoxプロジェクトとTree Style Tab(以下、TST)プロジェクトとのコラボレーション」の形になった、ということに触れました。 後編となるこの記事では、「調査・開発を進める過程で、何故そのようにゴールが変わったのか」を説明した上で、主に「既存の開発プロジェクトに対する特定顧客向けの機能追加の要件をどのように分離したか」に焦点を当てて、実際に行ったことをご紹介します。
作業方針決め
前編に記載した検討の結果、今回の作業内容・計画は以下の通りに決まりました。
- 機能はWebExtensionsアドオンとして実装する。(仮称「Waterfox Sidebar」)
- 開発するアドオンは、TSTの実装を一部流用する。
- アドオンの仕様はManifest V3とする。
- ただし、バックグラウンドページは何らかの方法で永続化できる前提とする。
- つまり、アドオンの全体的な設計はManifest V2からほぼ変えない。
- バックグランド永続化の具体的な方法は、Waterfox側での特別な対応を想定する。
- ただし、バックグラウンドページは何らかの方法で永続化できる前提とする。
- UIは仮想スクロールを用いて、ほぼ新規に開発する。
- サムネイル、設定画面の組み込みなど、WebExtensions APIでは実現不可能な部分をExperiments APIで実装する。
この作業内容から、成果物は「バックグラウンド部分はTSTを流用し、UI(サイドバーパネル)はほぼ新規に開発する」形になると考えられました。 そのため、まずバックグラウンド部分から作業を開始し、以下の要領で進めることにしました。
- 出発点として、TSTの現行のバックグラウンド実装のコードをコピーする。
- 要件上、Waterfox Sidebarには不要な処理を削除する(随時)。
- TSTとWaterfox Sidebarに共通の部分で必要な変更については、公開のTSTにまずその変更を行い、次に、Waterfox Sidebarの対応する箇所にその変更内容をバックポートする。
- その後、Waterfox Sidebar固有の機能の実装を進める。
この中でも特に3の点は、当社が関わる開発案件での「案件の成果のうち公開して問題がない部分は公開する」方針と同じ考え方です。 最初に案件に強く紐付ける形で非公開情報を含めて開発した成果を、後から公開できるようにしようとすると、案件固有の非公開情報の切り離しに苦労しがちです。 そのため通例では、設計の時点で「案件被依存の物として開発した上で、案件固有の部分を後付けや差し替えできるようにする」という考え方で設計することが多いです。 今回も、TSTとWaterfox固有の部分を分けるために1同じ考え方を採用しました。
Manifest V3対応
バックグラウンドページの永続化を前提にしたので、TSTから流用するバックグラウンド部分について、全体的な設計変更は不要になりました。 とは言うものの、将来的にManifest V2が廃止される可能性は依然としてあります。 そこで、各種APIの使用箇所をManifest V3の仕様に合わせて改修することにしました。
ただ、Manifest V3でのバックグラウンドページの永続化は、通常リリース版のFirefoxでは不可能で、Waterfoxでのみ可能となります2。 コード全体をManifest V3前提の物へと大きく書き換えて、「Manifest V3対応」には直接関係しないはずの処理にまで変更が及んでしまうと、今後TST側で行った変更をWaterfox Sidebarに持ち込む際に、差異を考慮しながら個別に作業しなくてはならない部分が増えてしまいます。 そのため、TSTとWaterfox Sidebarの間で可能な限り実装を共通化できるよう、Manifest V3対応は以下の要領で行うことにしました。
- Waterfox Sidebar用のマニフェストファイルは、Manifest V3準拠の物を作成する。
- TST由来の実装は、Manifest V3 Migration Guideを参考に、Manifest V3とManifest V2のどちらで読み込まれても動作するよう改修する。
Manifest V3での変更は、単にAPIの名称が変わっただけの物もあれば、利用方法が大きく変わった物もあります。
TSTが使用するAPIの中で特に変更が大きかったのは、browser.tabs.executeScript
からbrowser.scripting.executeScript
への移行でした。
ダイアログ風のウィンドウを開くためのライブラリーに加えた変更の差分を見ると、両者の差異の吸収の仕方を確認できます。
仮想スクロールの導入と、方針の再度の見直し
仮想スクロールとは、「長いコンテンツの一部だけを少しずつスクロール表示する」代わりに、「短いコンテンツを少しずつ書き換えて、長いコンテンツの一部だけをスクロールして見ているように感じさせる」方式です。 ソフトウェアのUIを仮想スクロール方式で実装すると、消費リソースの削減や処理速度の向上といった恩恵を得られると期待できます。
仮想スクロールの導入は、TSTのパフォーマンスの問題を解消する手段としての期待の他に、次項で述べるタブ内のプレビュー画像表示機能の消費メモリー削減の効果への期待もありました。 プレビュー画像はそれなりの大きさとなることから、描画範囲外のタブに対してまでプレビューを表示すると、消費メモリー量が尋常でないことになると予想できたためです。
実装にあたっては、前編や先述の方向性で示していたとおり、当初はUIのほぼ新規での開発を想定していました。 しかしながら、改めての調査の結果、実際にはそこまでの大規模な作業は必要なく、TSTの既存UIの実装の改修で対応できるようだ、ということが分かってきました(この点の技術的な詳細については、筆者の個人ブログの記事に詳しく記載しています)。
こうなると、バックグラウンド部分もTSTほぼそのまま、UI部分もTSTほぼそのままということで、今回のWaterfox Sidebarは全体としては、「TSTと一部の実装を共有した別のアドオン」と言うよりも、もはや「TSTを小改造した派生版」と言った方が実態に相応しいです。
実は当初は、TSTの機能のうちいくつかは今回の要件上必須ではないことから、主に軽量化や不確定要素の排除を目的として、Waterfox Sidebarには実装を引き継がないことを想定していました。
しかし、サイドバーパネルの実装も現行のTSTの物をそのまま流用できる目処が立ったことで、「要件上は必須でない実装もそのまま引き継いでおいた方が、TSTとの差異がファイル単位では小さくなり、TST側の変更をgit diff
で抽出した差分ファイルをpatch
コマンドでそのままWaterfox Sidebarに適用3、といったことをしやすい」状況となりました。
そのため、前述の作業方針を見直して、ここまででWaterfox Sidebar用に用意しつつあった実装はマニフェストファイルを除いてすべて破棄し、以後の作業は原則として以下の方針で行うことにしました。
- 出発点として、TSTの現行のコードをすべてコピーする4。
- 要件に基づいて行う変更について、Waterfox固有の事情が強い要件かどうかを判断する。
- Waterfox固有でない変更の場合、公開のTSTにまずその変更を行い、次に、今回開発するアドオンにその変更内容をバックポートする。
- Waterfox固有の変更の場合、コードは今回開発するアドオンに固有のモジュールとして実装する。 モジュールの読み込みや処理の差し替えが必要な箇所は、公開のプロジェクトのTSTに存在しても違和感が無い形で、TST側に必要なAPIを追加し、Waterfox Sidebar側ではそのAPIを利用する形にする。
元の方針との違いは、Waterfox Sidebarの要件上不要な処理を削除する工程を省くことにした点と、Waterfox Sidebar固有の実装を明確に別モジュールに分けることにした点です。
こうして、本開発プロジェクトは改めて、「TSTのWaterfox向け派生版の開発」という体裁で再出発しました。 仮想スクロールの実装が軌道に乗ったため、以降は仮想スクロールの不具合修正を行いつつ、Waterfox Sidebar固有の機能の実装に入っていきました。
タブ内プレビュー
タブ内プレビュー機能の実装上のポイントは以下の2点です
- どうやってプレビュー画像を用意するか
- どうやってタブにプレビュー画像を埋め込むか
1点目についてはVisual Tabsなどの先例がすでにあり、技術的な実現可能性で困る部分はありません。今回は、むしろ2点目の方が重要でした。 タブのHTML要素の生成処理を書き換えれば、機能は容易に実現できますが、そうすると「TST由来の実装」と「Waterfox Sidebar固有の実装」が混ざり合って、TSTの更新への追従コストが大きくなってしまうからです。
幸いなことにTSTには、他のアドオンから送られたメッセージに基づいてタブの中に任意のHTML要素を埋め込むAPIがすでに存在しています。 今回はこれを拡張し、TSTを「プレビュー画像を埋め込みたい位置(タブのラベルの上)へのHTML要素の挿入指定」に対応させた上で、他のモジュールからの通常のメソッド呼び出しでもAPIを使用できるように改修しました。 これにより、TST側の実装とWaterfox Sidebar用の実装を綺麗に分離したままでプレビュー埋め込み機能を実装することができました。
ブラウザー本体の設定画面への組み込み
ブラウザーの設定画面(about:preferences
)へ独自の設定項目を組み込む際の実装上のポイントは、以下の2点です
- どうやってUIを組み込むか
- 設定情報をどこに保存するか
1点目については、ブラウザー上で設定画面が開かれたことを検知して、動的にHTML/XUL要素を生成して組み込む必要があります。 今回は、Experiments APIの実装の初期化処理でブラウザー内部のAPIを使用してリスナーを登録して実装することにしました。
悩ましいのは2点目です。
Firefoxはユーザー設定を保持する「preferences」という簡易的なデータベースを持っており、設定画面の情報のほとんどはこのpreferencesで管理されています。
その一方でWebExtensionsのアドオンは、preferencesにアクセスするAPIが無いため、storage.local
などの既存APIを使って設定ストアを実装する必要があります。
preferencesデータベースにアクセスするためのExperiments APIを実装し、TSTの設定読み書き部分をstorage.local
ではなくExperiments APIによる独自APIを使用するように書き換えれば、「Firefoxの設定画面からTSTの設定を管理する」という要件は満たせます。
ですが、それではTSTとの差異が大きくなり、TSTの更新への追従コストが増大してしまいます。
そこで今回は、「preferencesデータベースの読み書きと変更の監視を可能とするExperiments API」を実装した上で、
- アドオンの初期化時に、TST側が保持しているすべての設定項目の初期値を、preferencesデータベースへ初期値として登録すると同時に、preferencesデータベースに「ユーザー設定値」が保存されている物のユーザー設定値を取得して、TST側の設定ストアに値を反映する。
- 以後は、TST側の設定ストアとpreferencesデータベースのそれぞれについて、片方で値が変更されたときには、即座にもう片方に値を同期・反映する。
ということを行うモジュールを作成し、TST部分で読み込ませるようにしました。 これにより、preferencesデータベースがユーザー設定値の最終的な保存先となり、ブラウザー側に組み込んだ設定画面から無理なく設定値を制御できるようになる、という寸法です。
WebExtensions APIの制約により未実装となっていた機能の実装
TSTのサイドバー上のタブの振る舞いは、Firefox本体の機能や振る舞いを、一般的なWeb技術とWebExtensions APIの組み合わせのみで再現した物となっています。 そのため、APIが提供されていなかったり、セキュリティ上の制約により禁止されていたりする以下の機能は再現できていません。
- Windows 10以降およびmacOSでの「共有」機能による、タブのURLの他アプリへの連携機能
- Firefox Syncを用いての、Android端末も含めた他の端末へのタブの送信(現在は
storage.sync
を使って擬似的に再現しているが、機能が限定されている) - YouTubeの動画をバックグラウンドのタブで開いたときに、自動再生がブロックされたのを検知して表示し、タブ上の操作で自動再生を継続する機能
- File URLや
about:addons
など、アドオンの権限では読み込めないURIを伴ってタブを開く操作(ブックマークからタブを開く場面などで、現在はabout:blank?forbidden-url=...
という代替URLに置き換えて開くようになっている) - ファイルのドロップ時のFile URLでの読み込み
今回の開発ではExperiments APIを使うため、これらの制約は無視できます。 ただ、通常のWebExtensions APIの呼び出し部分と同じ要領でExperiments APIを呼び出すよう実装すると、TSTの実装の中にWaterfox Sidebar固有の記述が混ざってしまい、やはりTSTの更新への追従コストが増大してしまいます。
そこで今回は、TST側に「内部的なAPI」として、いわゆる依存性注入と同様の考え方でモジュール登録のためのインターフェースを追加しました。 具体的には、以下の箇所がこれに該当します。
- 「共有」機能提供用モジュールの登録処理(共有機能を呼び出すモジュールさえ追加すれば、それを使うようになる)
- Firefox Sync機能の提供モジュールの登録処理(Firefox Syncの機能を呼び出すモジュールさえ追加すれば、それを使うようになる)
- 自動再生のブロックを解除する操作を通知するメッセージの送信処理(メッセージを受け取るモジュールさえ追加すれば、この機能を使用できるようになる)
- 通常の権限では読み込めないURLの読み込み要求が発生したことを検知して任意の処理を行う処理(要求されたURLを強制的に読み込むモジュールさえ実装すれば、そのURLを強制的に読み込ませられるようになる)
- File URLの解決用モジュールの登録処理(ドロップしたファイルの実際のFile URLを取得するモジュールさえ追加すれば、そのモジュールの処理結果を使うようになる)
その上で、これらのインターフェースに合わせて設計した「Experiments API呼び出し用のモジュール」をWaterfox Sidebarでのみ追加することで、TST側の実装をWaterfox専用のExperiments APIに対して疎結合に保つようにしました。
専用サイドバーの実装
TSTのサイドバーはWebExtensions APIのサイドバーAPIに基づいています。 そのためTSTのサイドバーは「ブックマーク」「履歴」などの既存サイドバーパネルとの排他的な選択にならざるを得ず、「タブを表示している間はブックマーク一覧を見られない」「履歴の一覧を表示している間はタブを見られない」といった不便があります。
TSTではこの不便を解消するための代替策として、TSTのUI上に他のアドオンが任意のUIを挿入するためのAPIを提供していて、「ブックマーク」サイドバー相当の機能を提供するヘルパーアドオンTST Bookmarks Subpanelを、筆者自らAPIのデモンストレーションも兼ねて開発・公開していますが、WebExtensions API自体の仕様からくる機能的な制約があり、完全な代替とはなっていません。
そのため今回のWaterfox Sidebarでは、WebExtensions APIで触れる対象のサイドバーとは別に、「Waterfox Sidebarのサイドバーパネル専用のサイドバー」を提供することにしました。 具体的には、Firefoxにおけるサイドバーが「読み込むコンテンツを随時入れ換えるインラインフレーム」として実装されていることから、通常のサイドバー用のインラインフレームとは別のインラインフレームをExperiments APIで追加し、そちらにWaterfox Sidebarのサイドバーパネルを読み込むようにしています5。
専用サイドバーの提供と、前項で述べた「本体のタブにしかなかった機能」の実装により、「サイドバーのタブUIだけを使っていると、できない操作がある」という場面を考慮する必要がほぼなくなったことから、Waterfox Sidebarでは専用サイドバーが表示されている間は、混乱を避けるためウィンドウ最上部のタブバーを隠すようにしています。
この他にも、WebExtensions APIでは実現できなかったことを実現するための機能がWaterfox Sidebarには追加されていますが、いずれも実装はExperiments API内、もしくはExperiments API呼び出し用の追加モジュールに隔離してあり、TSTの将来のバージョンアップに追従しやすい状態を保っています。
そうして「Waterfox Sidebar」に求められる機能を実装し、先方にも検証して頂いているのが現状です。
すでに、Waterfox公式のダウンロードページからダウンロード可能なWaterfox G6.0.10以降のバージョンにおいて実験的に機能が組み込まれており、about:config
でbrowser.sidebar.disabled
をfalse
に設定することで機能を試せる状態となっていますので、横長のタブバーの使い勝手に不満があったり、公開のTree Style Tabの使い勝手に不満があったりする方は、お試し頂ければ幸いです。
また、今回の開発の成果のソースコードはすべて、Waterfoxのソースツリーの中に組み込まれる形で公開されています。 ここまでで述べた事がどのように実装されているかを知りたい場合は、Tree Style Tabのソースと比較してみてください。
まとめ
以上、個人開発のオープンソースソフトウェアに寄せられた連絡を企業としての開発案件に繋げた事例の紹介の後半として、「Waterfoxのサイドバー開発」から「TSTのWaterfox向け派生版開発」への方針転換の経緯と、その方針における実装上の工夫をご紹介しました。
前編では「マネタイズに成功したオープンソース開発プロジェクトからの開発依頼」という点に着目しましたが、後編での方針転換により、TSTプロジェクトもまた結果的に「個人で開発を始め、マネタイズに至ったオープンソース開発プロジェクト」となりました。
個人レベルのオープンソース開発プロジェクトのマネタイズというと、「寄附」や「買収」がメジャーな方法です。 しかし、OpenSSLの脆弱性発覚時に問題となった、寄附ベースでのプロジェクト運営体制の貧弱さや、近頃話題になった、オープンソースでの開発で困窮し、マネタイズのためにオープンソース開発をやめた事例に見られるように、寄附のみに基づくプロジェクト運営は充分な額の金額を継続的に集めることは難しい場合が多いです。 また、買収では買収先企業にフルタイムの開発者として雇用される事例もありますが、そのような事例は少数派で、ブラウザー用拡張機能の規模ではむしろ、既存のユーザー層を手に入れてそのまま商材とするためだけの、ソフトウェアとしての維持・発展をまったく考えない買収の方が多い印象があります6。
TSTは筆者が自分自身で使うために開発している側面が強く、寄附があってもなくてもプロジェクトは続けていくつもりでいます。 また、買収提案があったとしても、自分が使いやすいように作りたくて作っている物を、自分でメンテナンスできなくなっては意味がありませんので、買収には応じるつもりがありません7。 そのような状況で、TSTプロジェクトにとっても相手のプロジェクトにとっても双方に利がある形での協力事例ができたことは、とても喜ばしいことだったと筆者は感じています8。
当社では、OSSを使用した開発案件において、OSS開発プロジェクト側と利用者側9の双方にとってメリットが大きくなるように、長期的な利を考慮した開発を心がけています。 また、当社請負での受託開発のみに留まらず、そのような知見を活かしての「お客様ご自身によるOSS開発」のサポートも行っております。 OSSの自社製品への組み込みや、カスタマイズした上での社内運用にあたって、具体的な取り組み方の相談先をお探しの企業ご担当者さまは、お問い合わせフォームよりご連絡を頂けましたら幸いです。
-
TSTのプロジェクト自体はWaterfoxプロジェクトと別に固有のポリシーを持っており、今回の開発内容の一部は、そのポリシーに抵触していました。そのため、今回の開発の成果をすべて公開できるとしても、TST本体にすべての成果を取り込むことはできませんでした。 ↩
-
正確には、「Firefox」のブランド名を伴わない開発版ビルドであれば使える、という状況です。 ↩
-
こういうケースでは、改造元のプロジェクトを
git submodule
などで参照し、ファイルはコピーしない方法もあります。今回は、モジュールを綺麗に分けられず、若干ながらTST自体の構成ファイルにも手を加える必要のある箇所があったので、ファイルをコピーするやり方としてみました。 ↩ -
このためWaterfox Sidebarには、当初計画ではドロップ予定だったTSTの機能もすべて含まれる結果となっています。 ↩
-
そのため結果的に、Waterfox SidebarはWebExtensions APIのサイドバーAPIを使わない設計となっています。 ↩
-
今回の案件での開発が進行している間にも、筆者のもとには「TSTを売ってくれ」というオファーのメールが届いていました(無視しました)。 ↩
-
なので、返事をするとしても「TSTに基づいた製品を作りたい場合、ライセンスに基づいて自由にforkしてくれて構いませんよ」と返すことにしています。 ↩
-
ちなみに、今回の件で筆者の収入が増えたということはありません。個人でやっていれば個人的に金銭的な利益を直接得られたでしょうが、筆者は、オープンソース開発プロジェクトとの協業実績や海外企業との取引実績が当社に増えることが、自分にとって働きやすい場である当社のビジネスの継続や発展に利すれば、長期的にはその方がありがたいという考えで、間接的に利益を得られたと考えています。 ↩
-
フォーク版プロジェクトや、カスタマイズして使用するエンドユーザーなど。 ↩