Here is the English version of this article.
この投稿は個人サイトとのクロスポストです

2017年の8月下旬に思い立って、ツリー型タブのWebExtensions版を作り始め、去る9月26日にバージョン2.0としてリリースしました。

(ツリー型タブのサイドバーパネルを表示した状態のスクリーンショット)

重い腰を上げて取り組む気になれたのは、必須と目していたAPIが一通り実装されてきて、Firefox 57でようやく技術的に作れる目処が立ってきたからでした。
関係者の皆さんの尽力に改めて感謝の意を表明します。

やっている事自体はそう難しい話ではなく、技術的に目新しいトピックは無いのですが、主に歴史的資料としてレガシーなアドオンの移行の一事例の記録を残しておこうと思います。

ツリー型タブとは?

一言でいうと、ツリー型タブ(Tree Style Tab、略してTST)は「Firefox用の、タブ同士の来歴・関係をツリー構造として視覚化してWebブラウスを支援するアドオン」です。

TSTの来歴(昔話)

今あるTSTの源流を遡ると、「タブブラウザ拡張(TabBrowser Extensions、TBE)」という拙作の古いFirefox用アドオンと、「iRider」という個性的なWebブラウザに行き着きます。

2004年頃のFirefoxの貧弱なタブ管理機能を補完するオールインワン型のアドオンとして開発していたTBEは、良さげな機能をなんでも無秩序に取り込む方針でした。
紹介記事を見て知ったiRiderの「タブのように見えるサムネイル付きの履歴項目がツリー状に表示される」「タブのクローズボックスをドラッグして選択してまとめて項目を閉じられる」といった挙動も、腕試しを兼ねてTBEに実装したと記憶しています。

無秩序に肥大化・複雑化したTBEはFirefox 2での大規模な仕様変更に追従できず、Firefox 1.5と共に歴史の闇に消えていくこととなります。
TBEを諦める代わりに、僕は当時自分自身がTBEで愛用していた機能のいくつかを機能単位で切り出して再実装することにしました。
そうして「タブのインデントによるツリー表示」という機能に特化して2007年に生まれたのがTSTです。

Firefoxの更新に追従しきれずドロップアウトしていくアドオンも多い中、TSTはメンテナンスを繰り返しながら2017年まで地味に生き残ってきました。
「Firefox 57以降に対応したWebExtensions版」(以下、WE版)であるTST 2.0は、そのTSTの誕生以来最大のマイルストーンと言えます。

フェイズ1:移行計画

作業に着手する前から、TSTのWebExtensionsへの「完全な移行」は不可能だということは分かっていました。
そのため今回のWE版開発は、コンセプトを今一度明確化し、何をやって何をやらないか(諦めるか)をハッキリさせるという所から始めました。

「TSTのWebExtensions対応」ではない、「TSTのWebExtensions版開発」

ところで、FirefoxのWebextensionアポカリプスに備える(主要アドオンのWebextensions対応について)のように「WebExtensions対応」という言葉を目にする事があります。
これに対し、自分は「WebExtensions版」という表現を使う事が多いです。
この違いはどこからきているのでしょうか。

元々Firefoxは、「Geckoという実行環境レイヤの上で、XML+CSS+JavaScriptを動かして、Webページの要領でアプリケーションを実現する」という構造をしています。
従来のアドオンは本質的には、このFirefoxのJavaScriptが動作している名前空間に任意のスクリプトを注入して動作を変える、いわゆる「モンキーパッチ」であったと言えます。

(Firefoxのレガシーなアドオンのアーキテクチャの概念図)

対するWebExtensionsベースのアドオンは、隔離された専用の名前空間の中でアドオンのスクリプトを実行し、あらかじめ用意されたAPI(WebExtensions API)を介してFirefoxと相互作用するという仕組みで成り立つソフトウェアです。

(FirefoxのWebExtensionsなアドオンのアーキテクチャの概念図)

この本質的な差異ゆえに、直接Firefoxの中身をいじくり回していたレガシーなアドオンを「若干手直ししてFirefox 57でも使えるようにする」ということは不可能で、「従来提供していたユーザー体験を再現する事を目指して、WebExtensionsベースで新しくアドオンを開発し直す」というアプローチを取らざるを得ません。
TSTのWE版も、実質的にゼロからの開発に等しい作業となりました。
これが、自分が「WebExtensions版」という言葉を使っている理由です。

技術的制約から決まってくる部分

WebExtensionsの仕様上の制約から、レガシー版TSTのどの機能を実現できてどの機能は実現できないかというのはある程度決まってきます。

  • 「縦長のタブバー」の実現にあたっては、サイドバーが唯一の選択肢になります。 WebExtensionsのサイドバーは、サイドバー自体を任意のイベントで開閉したり、位置をウィンドウの下や上に移動する機能はありません。 この事から、「タブバーを自動で隠す」機能や「横長のタブバー内でツリー表示」機能は実現不可能だということになります。
  • WebExtensionsのAPIの方針故に、「Firefoxがタブを開こうとした時に、その前のタイミングに割り込んで何かをする」ということもできません。 全ては事後的に通知されるのみなので、事前の処理に割り込む必要がある系統の機能はすべて再現不可能です。
  • タブを開くトリガーとなる操作への割り込みや、タブが開かれる前の判断への介入ができない以上、リンク等から開かれたタブの親子関係を推測する材料はtabs.Tab.openerTabIdだけが唯一の頼みの綱ということになります。 openerTabIdは、Google Chromeの仕様には以前から存在していたもののWebExtensionsには長らく実装されないままとなっていた機能の1つです。 実は、これの実装を取り扱うbugにパッチが提出され、Fx57で正式に実装済みとなったという事が、TSTのWE版開発に着手する直接のきっかけとなりました。
  • ツリー構造の保存と復元は、レガシー版ではタブやウィンドウに紐付けて情報を保持していましたが、WE版開発を始めた時点ではまだsessions APIには対応する機能がありませんでした。 ただ、実装を取り扱っていたbugを見ていると、Fx57には間に合いそうな勢いでパッチの提出とレビューが進行していましたので、これが必要になる頃には投入されていそうな様子でした。

以上のような簡単な分析を踏まえ、半ば見切り発車で作業を始めたのでした。

技術的制約以外の、明確な意図を持って決める部分

レガシーなアドオンのWebExtensions移行では、全ての機能について「移行できて、する」「移行できないので、しない」「移行できるが、コストが見合わないので移行しない」といったことを判断しなくてはなりません。
これは精神的ストレスの大きな難しい仕事です。
TST 2.0においてそれをやり遂げられた事の背景には、核となるコンセプトがはっきりしていて、すべての判断をそのコンセプトに基づいて行えたという事がありました。

僕がTSTのTBEからの切り出し時から一貫して意識してきたコンセプトは、

  • 「タブのツリー」という単機能に特化する。主題と無関係の(且つ、Firefox本体の機能でもない)機能は、技術的には実現可能でも入れない。
  • その代わり、他のアドオンと併用できるようにして、ユーザーが欲しい機能をユーザー自身が任意に組み合わせられるようにする。

という2つの事でした。

単機能のアドオンは設計を簡潔に保ちやすくなります。
それは、機能が少ないからと言うよりも、コンセプトが明確であるという事自体が設計の骨子となり、全体に秩序と一貫性をもたらすからだ……と僕は考えています。

(オールインワン型のアドオンの利点と欠点)

(単機能型アドオンの利点と欠点)

取捨選択の判断基準が明確であればそれだけ意志決定のコストも下がります。
「大事にしなくてもいい事を切り捨てて、大事にしたい事にリソースを集中する」というのは何事にも言える理想ですが、それを実際にやるためには「そもそも何を大事にするのか」がはっきりしていないといけないわけです。

WebExtensionsでは得られにくいアドオン同士のシナジー

そして、単機能に集中する事と同時に大事にしたかったのが、もう1つのコンセプトであった「他のアドオンとの相互運用性」です。

実質的にはモンキーパッチであったレガシーなアドオンではアドオン同士が衝突しないように気をつける必要があったのに対し、決まったAPIに基づいて実装されるWebExtensionsのアドオンでそんな事をわざわざ気にする必要など無いのでは? と思うかもしれませんが、逆です。
WebExtensionsではアドオン同士が綺麗に隔離されすぎているせいで、複数のアドオンの機能を連携させにくくなっているのです。

例えば従来のアドオンでは、「ツリー型タブによって縦置きされたタブの中で、別のアドオンによって埋め込まれたサムネイルが表示されていて、さらにタブをダブルクリックするとまた別のアドオンによって提供された機能が発動する。さらに、タブが無い無駄な領域には別のサイドバーパネルが表示されている。」というように、アドオン同士が暗黙のうちにお互いを補完しあう形で作用するということが自然とできていました。

(レガシーなアドオン同士の暗黙的な連携の様子の概念図)

ところが、WebExtensionsではそれはできません。
WE版ツリー型タブが提供する縦型タブバーとしてのサイドバーはそれ自体で完結しており、その中に他のアドオンが提供する機能は入り込むことができません。
また、サイドバーパネル同士は排他的なので、「ツリー型タブのサイドバー」と「ブックマーク一覧のサイドバー」はどちらか片方だけしか表示できません。
それぞれの機能を単独で使う事はできても、機能同士を組み合わせることによるシナジーが生まれにくいのです

(WebExtensionsなアドオン同士では暗黙的な連携が無い様子の概念図)

どんなに便利な機能を提供するアドオンでも、他の機能と排他的にしか使えないのでは意味がありません。
どちらしか使われなくなるならまだいい方で、最悪の場合どちらも使われなくなってしまうという事にもなり得ます。
僕がWebExtensionsに対して抱いていた最大の懸念事項は、むしろこの点にありました。

結論を先に言ってしまうと、WE版TSTはこの点で大幅な後退を余儀なくされています。
他のアドオンとの暗黙的な連携は非常に限定的で、他のアドオン作者の厚意に期待して明示的な連携のためのAPIを用意する事しかできませんでした。
これについては後ほど詳しく述べます。

フェイズ2:初期開発

既に述べたとおり、TSTのWebExtensions移行は実質的には「レガシー版TSTをリファレンスとした、新しいWebExtensionsアドオンの開発」に他なりませんでした。
WebExtensionsアドオンの一般的な開発の仕方に属する部分についてはあまり詳しくは述べず、ここでは「TSTのWE版」の開発という点にフォーカスして、実際の進め方を振り返ってみます。

実証実験を兼ねた開発

TSTのWebExtensionsベースでの再実装にあたっては、他の既存のアドオンの派生として開発する方向と、全くのスクラッチで開発する方向の2通りの進め方があり得ました。

WE版TSTはWebExtensionsでの縦長タブバー型アドオンとしては最後発になるため、既に十分に使い勝手の良い物があるのなら、それを改造する形で作ったほうが効率が良いというのは道理です。
しかし他の人の書いたコードを何種類も読み込んで比較検討するら事の精神的コストを考えると気が遠くなったのと、「何だかんだでまだまだレガシーなアドオン開発の意識が抜けきっていない僕自身の発想」をWebExtensionsに移行するためには単純に経験を積み増す方が良いと思ったのとで、敢えてのフルスクラッチでいくことにしました。
(実際の所は、とりあえず実験的にやり始めてみたらこれはやはり大変そうだということが分かって、大変だからこそ浅い理解のまま他人のプロダクトに乗っかったら大火傷する・そうならないためには細部までの深い理解が必要だと思って、完全に理解できる程度の小規模から作り始めてAPIへの理解を深める事に決めた……という感じでした。)

サイドバー関連APIへの慣熟と、実装されたばかりの新機能のtabs.Tab.openerTabIdの検証のため、最初はサイドバー内で完結する物として、

  • タブを開く前に何かするのでなく、開かれた後のタブから情報を得てツリーに組み込む。 特に、tabs.Tab.openerTabIdで「どのタブから開かれたか」の情報が与えられている場合は明示的に子タブが開かれたものとみなして取り扱う。
  • Firefox自身のタブバー上のドラッグ操作やWebExtensions API経由での操作によってタブの位置が変更された場合、タブが既存のツリーの中のどこに出現したのかを検出して、ツリー内に矛盾なく組み込む。
  • 親にあたるタブが閉じられたときは、そのタブがなくなった後もツリー構造が破綻しないように、既存のタブを新しい親に昇格させるなどの対応を取る。
  • 親のタブが自身の配下の子孫のタブが並ぶ中の位置に移動された場合は、ツリー構造の破綻を防ぐために、そのタブを元の位置に自動的に押し戻す。
  • ツリーが折り畳まれている状態で親にあたるタブが閉じられたときは、子孫のタブも道連れに閉じる。

などなど、タブに起こったイベントを監視し縦型タブバーとして表示する所から始めました。

そうして実装が進むうちに、ツリーの一部が閉じられた場合などの挙動も検証する必要が出てきたので、ツリーの開閉機能、タブのツリーをドラッグ&ドロップで編集する機能、ウィンドウをまたいだタブの移動、タブバーの自動スクロールなども早々に実装しました。
これらの処理は元々Firefoxの実装に強く依存していたわけではなかった一方で、「様々な局面で、空気を読んでイイ感じに自動判断する」というTSTの特徴を形作る部分であったため、下手にスクラッチから書き直せばロジックが変わって使い勝手も別物になってしまうと考え、使える所は可能な限りコードをそのまま使い回すようにしています。

「Firefoxによってタブが複数開かれる場面で、それらをグループ化用のタブで自動的にツリー化する」という機能については、タブを開く前の時点で処理に介入できない事がはっきりしていたため、当初は移植もしないつもりでした。
しかし「一定時間以内に連続して複数のタブが単独で開かれた場合」のように場面を限定すればある程度の推測は可能かも知れないと思い立ち、そのような場面でタブを自動的にグループにまとめる機能を実装してみました。
苦肉の策ではありましたが、結果的にはこれはそこそこ期待通りの成果をもたらす事となりました。

また、予定外でしたがこの時点で「セッションをまたいだツリー構造の保存・復元」のstorage.localによる暫定的な実装も投入しました。
自分は基本的に「捨てる前提の実装」はしないようにしているのですが、毎回Firefoxを起動する度に検証用のツリーを構築する手間が馬鹿にならなくなってきたため、この時ばかりはポリシーを曲げざるを得ませんでした。
(最終的には、この暫定的な実装は削除し、browser.sessions.setWindowValue()browser.sessions.setTabValue()に基づく本来の想定通りの実装に置き換えました。)

バックグラウンドページを中心とした設計への転換

レガシー版TSTはFirefoxの各ウィンドウ内でタブバーに寄生するような形で動作する設計で、全体を統括する管理機構のような物はありませんでした。

(レガシー版TSTのアーキテクチャの概念図)

WE版TSTの初期開発段階もそれと同じ設計でスタートしましたが、途中で「バックグラウンドページがマスタープロセスとして全体を管理し、サイドバーは基本的にはその指示を受けてツリーを描画する」という設計に改めることとなります。

(WE版TSTのアーキテクチャの概念図)

これには大きく以下の2つの動機がありました。

  • マスタープロセス不在の状態でのフラグ管理の煩雑さの低減。
    タブに起こった変化を監視するにあたっては、「TSTによって意図的に行われた変更であれば何もせず、そうでない外部要因によって意図せず引き起こされた変更であれば、その変更後もツリーの構成に矛盾が生じないようにツリーを自動修復する」といった要領で、内部操作と外部操作とを厳密に識別する必要があります。 そのためには、WebExtensions APIにはタブを開く時にメタ情報を指定する機能が無いため、「これからTST自身がタブを開こうとしている」という事を意味するフラグを何らかの方法で立てて、WebExtensions APIを呼び、処理が終わったらフラグを下ろすという事をする必要があります。
    しかし、WebExtensionsではウィンドウ間のメッセージのやりとりを非同期にしか行えないため、まともにやろうとすると、ウィンドウをまたいでタブを移動するような場面では「フラグは移動元のウィンドウと移動先のウィンドウのどちらで立てるのか」「立てたフラグをいつ下ろせばいいのか」「フラグを立てる事を通知するメッセージとタブを操作するAPIの呼び出しのどちらが先に発生するか保証できない」などの問題に悩まされることになります。
    同期的にフラグを管理するためには、ウィンドウをまたがってタブの変化を横断的に監視するマスタープロセスを用意する他の方法はありませんでした。
  • サイドバーが閉じられている間・TSTが機能していない間のタブ操作のトラッキングの必要性。
    WebExtensionsのサイドバー機能は排他的な選択式サイドバーパネルを提供する物なので、当然、TSTが提供する「タブバーとしてのサイドバーパネル」も切り替えによってメモリ外に追い出される事があります。
    そうなると、TSTが機能していない状態でツリーの中のタブが部分的に閉じられたり、あるいはツリーの中に新たにタブが開かれたりといった変化が起こる事を避けられません。
    再びサイドバーパネルがアクティブになった時に壊れきったツリー構造をまとめて修復する、というのは現実的ではなく、ツリー構造が破綻しないようにするためには、タブに起こる変化を常時監視し、変化が起こる度に適切なメンテナンスを行う必要があります。
    これにも監視用のマスタープロセスが必要となります。

また、このあたりのタイミングで、ウィンドウやタブのセッション情報にメタ情報を保持できるようにするAPIの実装がFirefoxに投入されたため、TSTでもこれを使い、セッションをまたいでタブを一意に識別するためのIDを保持するようにし始めました(そういう事をFirefox自身がやって欲しいという要望は出ていますが、長らく動きが無いのです……)。
これもマスタープロセスの重要な仕事の1つとなっています。

ただ、「全ての判断をバックグラウンドページに任せてサイドバーは描画のみに特化する」という方針は厳密には徹底していません。

(WE版TSTで採用しなかった、完全な中央集権の様子の概念図)

そうではなく実際にはサイドバー側でも、タブの増減や移動の反映などで難しい判断が必要ない部分は自律的に処理したり、タブの親子関係を示す完全な情報を保持していたりします。

(WE版TSTで各モジュールが半自律動作する様子の概念図)

このことにより、タブのドラッグ操作などイベントリスナ内で即座にイベントをキャンセルしなくてはならないような場面で、バックグラウンドにいちいち非同期処理で問い合わせなくても、サイドバー内のスクリプトだけで同期的に「このタブは折り畳まれた子孫を持っているかどうか?」のような判断を行えるようになっています。

(ちなみに、WebExtensionsではbrowser.runtime.getBackgroundPage()でバックグラウンドページのグローバルオブジェクトそのものにアクセスできるのですが、この機能は使わず、バックグラウンドとサイドバーとの間での情報のやり取りには必ずbrowser.runtime.sendMessage()を使うようにしています。
これは、プライベートブラウジングモードのウィンドウからはbrowser.runtime.getBackgroundPage()でバックグラウンドページにアクセスできないという制約があるからです。)

CSSによるスタイリングとアニメーション効果

タブとしての見た目を整える事については、「面倒だが、やればできる事である」という事が分かりきっていたため、後回しにして実証実験レベルの実装を優先して作業を進めていました。
その実装がある程度進行したため、このタイミングでようやくスタイリングに着手し始めました。

タブの外観はレガシー版でもWE版でもCSSで制御していますが、その内容については顕著な改善がありました。

  • レガシー版はFirefox本体がタブに反映しているスタイル指定を一旦キャンセルし、その上から独自のスタイル指定を反映していました。 この「Firefox本体の指定のキャンセル」が結構な難題で、Firefoxのバージョンやプラットフォームの違いによってバラバラなスタイル指定を片っ端から潰してまわるために、今まで相当な労力が費やされていました。
    (レガシー版のスタイル指定:Firefoxの組み込みのスタイル指定をキャンセルするために多くの行が割かれている)
    WE版ではサイドバー内のコンテンツには自分で一からスタイルを指定することになるため、そういった苦労一切が不要となり、はるかに素直な形でCSSを記述できています。
    (WE版のスタイル指定:本当にスタイル指定に必要な行だけが含まれている)
  • 単位となる値の定義やアニメーションの挙動など、レガシー版ではJavaScriptとCSSの複雑な組み合わせで成立していた部分の多くをCSSだけで完結させるように改めました。
    UIの見た目をCSSで指定する時に問題になるのが、各要素のサイズの定義です。 タブのアイコンは縦横16ピクセルで一定なのに対し、UIの文字サイズはプラットフォームによって12ピクセルだったり16ピクセルだったりと様々です。 そのため、例えばタブの高さを32ピクセルと決め打ちすると、文字の大きな環境では狭苦しくなり、文字の小さな環境では余白が大きくなりすぎるという結果になってしまいます。 かといってem単位を使っても、今度はタブのアイコンが相対的に大きすぎる・小さすぎるといった問題が起こります。
    WE版TSTではこの問題の解決のために、カスタムプロパティを多用する事にしました。 これはいわゆる「CSSの変数」で、1箇所で定義した長さや数値をvar(変数名)で他の箇所から参照できるというものなのですが、実は値の定義を後から動的に変更する(新しい値の指定で上書きする)こともできます。 これとcalc()を併用して各部分のサイズ指定を「この部分は--tab-heightの半分」「この部分は100%分の高さから--favicon-sizeを引いた長さ」のように書いておき、リファレンスとなる要素の大きさを起動時にgetBoundingClientRect()で測定して、その結果をカスタムプロパティの値として再定義すれば、実行環境に適したサイズで全体の見た目を簡単に揃えられます。
    (タブのサイズを計測してスタイルシートを動的に定義している部分)
    なお、サイズ計測用のダミーの要素は、position: fixedで通常のフローから切り離して画面の隅に置いた上で、visibility: hiddenopacity: 0でユーザーの目に見えないようにしつつ、ユーザーのクリック操作に反応してしまわないようにpointer-events: noneも指定しています。 pointer-events: noneを指定したボックスは他にも「ユーザーの操作を横取りしないで、見た目上だけ半透明の色付きボックスを上に重ねる」のようなことをするのにも有用で、背景画像で頑張るとか、枠をつけるためだけにボックスを一階層増やすとかの無理をしなくてよくなります。
  • カスタムプロパティと値の計算は、インデントの深さの変更やツリーの折りたたみでのアニメーション効果にも活かされています。 レガシー版では個々のタブの要素に対してその都度styleを操作しアニメーション効果を適用していましたが、WE版では全てのアニメーション効果を事前に動的に生成したスタイルシートで定義しきっておき、個々の要素はそのトリガーとなるクラスの追加・削除のみ行うという設計に改めました。
    (インデントのアニメーション効果を実現するために自動生成されたスタイル指定の様子)
    この設計変更を決断できた背景には、上記のカスタムプロパティと値の計算が使えるようになった事に加え、テンプレート構文によって変数の値を埋め込んだ長大な文字列を容易に定義できるようになったからという事もあります。 (理論上は同じ改善をレガシー版にも反映できるのですが、余生の短いレガシー版のためにこれ以上の時間を割きたくないので、自分の手でそれをやる事はもう無いでしょう。)

フェイズ3:他のアドオンとの連携を考慮した作り込み

単体のアドオンとしてのWE版TSTが形になる目処が立ってきた辺りから、もう1つのコンセプトである「他のアドオンとの相互運用性」を意識した作り込みにも着手し始めました。

アドオン同士の暗黙的な連携

TSTの基本機能のうちいくつかは、WebExtensionsにおいてもアドオン同士の暗黙的な連携が可能なように設計してあります。

  • browser.tabs.create()openerTabIdを指定して開かれたタブは子タブになる。 (それ以外にも、openerTabIdを伴った状態でタブが開かれる場面はすべてTSTの検出対象となる。)
  • 現在のタブ配下のツリーが折り畳まれた状態で現在のタブを閉じると、子孫タブも道連れに閉じる。

これらの仕様により、マウスジェスチャ系アドオンやキーボードショートカット変更系アドオンが真っ当にWebExtensions APIを使用していれば、それらは違和感なくTSTと一緒に働いてくれるはずです。

タブのコンテキストメニューの提供

ただ、先にも触れましたが、WebExtensionsではあるアドオンが提供する領域内での他のアドオンと暗黙的な連携はほぼ不可能です。
せめてコンテキストメニューを介してだけでも連携できれば良かったのですが、現在のところWebExtensionsでは「サイドバーで独自のタブバー風UIを提供する」場合であっても一般的なタブのコンテキストメニューをそこに流用する方法がありません(bug 1376251bug 1396031などで提案はなされていますが……)。

正直な所としてはAPIが整備されるのを待ちたかったのですが、タブのコンテキストメニュー無しでは実用に支障をきたす事が明白です。
よって、非常に不本意ではあるのですが、他の同系統のアドオンがやっているように、TSTでもサイドバー内に独自のコンテキストメニューを実装する事にしました。

ただし、半端なオレオレUIにはせずに、Firefoxのタブのコンテキストメニューを可能な限り真似るようにという事には気を遣っています。

(サイドバー内の偽コンテキストメニュー)

自分としてはこれはあくまで「普通のタブのコンテキストメニューをサイドバー上でも使えるようになるまでの暫定的な実装」「いずれ捨てるつもりの実装」なので、この部分で独自の世界観を作り込むつもりは無く、技術的に不可能であるという以外の理由での差異は可能な限り発生させたくなかったのです。

メニューそのものの内容・見た目を真似るだけでなく、コンテキストメニューに項目を追加するAPIも、WebExtensionsネイティブのmenus APIを真似ています。
指定する内容や提供する機能はmenus APIの下位互換となるように注意しており、そのため他のアドオン対しては

  • 既存のコードに対して行わなくてはいけないTSTのための変更は最小限で済む
  • 将来のバージョンのFirefoxでmenus APIがサイドバー内のコンテンツに対しても有効になった時には、単にTST対応のためのコードを削除するだけで済む

といったメリットがあります。
ドッグフーディングも兼ねて、TST自身の追加のコンテキストメニュー項目そのものも完全にこの枠組みの上で提供するようにしています。

(サイドバー内の偽コンテキストメニューにおける、ツリー型タブの追加のコンテキストメニュー項目)

メニュー以外のAPI

menus APIを真似た部分以外のTST独自のAPIは、「TSTに指示を与えるAPI」と「TSTからの通知を受け取るAPI」の2種類を実装しました。

  • 前者は「ツリーを折り畳む・展開する」や「特定のタブを別のタブの子にする」などで、これはTSTに対してbrowser.runtime.sendMessage()でIDを指定して通知のメッセージを送るだけで使えます。 戻り値も、WebExtensionsの他の機能と同様にPromiseで返されます。
  • 後者は「タブバー上でのクリック」「ドラッグ操作の開始」などを通知するイベントAPIです。 browser.runtime.onMessageExternalに登録したリスナで通知を受け取るというものですが、TSTのイベントAPIではもう1ステップ、「TSTに対して自身の存在を知らせる」という事も必要となっています。 これは、browser.runtime.onMessageExternalで待ち受けているリスナにメッセージを送るためには、送信側で必ず受け手のアドオンのIDを明示しないといけない、という制約があるからです。

ここで問題になるのがアドオンの起動順序です。
TSTが先に初期化を終えている場合なら、連携する側のアドオンは単にTST宛に通知を送るだけで済みます。
しかし場合によっては、連携する側のアドオンの方が先に初期化されているという事も当然起こり得ます。
この場合、連携する側のアドオンからは「TSTに対していつ自分の存在を知らせればいいのか」が分かりません。
「TSTの初期化が完了した、という通知をTSTから送れば……」と思っても、そもそもそれができないわけで、鶏と卵です。

(メッセージベースで連携する2つのアドオンの起動順序を示したシーケンス図)

この点は突き詰めるとどうしようもないので、TSTでは

  • 他のアドオンに対しては、基本的に自分からは通知を送らない。
  • 過去に存在を通知されたアドオンに対しては、自分の初期化が完了した時点でその事を通知する。
  • TSTと連携できるアドオンがTSTより先にインストールされていた場合、ユーザーが明示的に再読み込み(無効化→有効化)するか自動更新で再起動されるかして存在を通知されるまでは、連携できないままでも仕方ない。

と割り切ることにしました。

(キャッシュされた情報に基づき滞りなく初期化処理を行う様子のシーケンス図)

このようにして実装した通知系APIについて、当初は必要最小限の情報のみを通知するようにしていたのですが、実際にそれに基づいて連携のための処理を実装してみると、思った通りの結果にはならないという事が分かりました。
というのも、通知される情報が足りないとイベントを受け取った側でまた改めて別の非同期な問い合わせ用APIを呼ばねばならず、その結果を待っている間にまたTSTから新しいイベントが通知されてしまって……という事が繰り替えされて、受け手側でイベントの状態を管理しきれなくなったのです。

(十分でない情報しか提供されない通知APIによる連携の様子のシ<br>
ーケンス図)

そのため最終的には、WebExtensionsの元々のAPIがそうなっているように、イベントAPIでは通知するメッセージの中に可能な限り情報を多く入れるようにしました。
例えば「タブがクリックされた」「タブの上でドラッグ操作を開始した」などのイベントでは、タブのIDだけを通知するのではなく、tabs.Tab形式のオブジェクト、ツリー型タブが独自に把握しているツリーの開閉状態などの情報、さらに子孫タブについても同じ形式の情報を収集してすべて1つのメッセージに載せています。

(必要十分な情報を伴ったメッセージに基づく通知APIによる連携の様子のシーケンス図)
イベントが短時間に連続して通知される可能性があるAPIを実装しようと思っている人は、この点に気をつけておくと、API利用者にとって「使いやすい・使い勝手の良い」APIになると思います。

まとめ

諦めなければ道は開ける……かもしれない

このような経過を経て、TSTのWebExtensions移行は「バージョン2.0のリリース」という一旦の区切りを見るに至りました。

「元のTSTと全く同一のユーザー体験が実現されていないので」あるいは「自分にとって必要だった機能がなくなってしまったので」この移行計画は失敗だった、と言いたい人もいるかも知れません。
しかしそれらをゴールにしていては、恐らく「WebExtensions版のリリース」という節目すらも迎える事はできなかったでしょう。
不可能な事は諦め、最も重要なユーザーである自分自身にとって必要な機能・特徴に注力し、やる事のスコープを絞り込んだからこそ、「TST 2.0」は世に出る事ができたと自分は考えています。

自分以外のレガシーアドオンの作者の方々も、きっと同じような事で悩んでいると思います。
「全部をそっくりそのまま移行できないのなら、やる意味がない」と自暴自棄になってしまう気持ちは非常によく分かります。
それでも、実際にTST 2.0の開発段階においても、一旦は諦めていた事が意外な角度から実現できたという事は何度かありました。
誰にも解決できなかった問題をユニークなやり方で解決してきたパイオニア達が、このまま完全に無かった物になってしまうのは忍びないです。
元の物の核となる価値をほんの少しでもWebExtensionsの上で再現できないかという事を、どうか今一度検討して頂ければと思います。

WebExtensionsの今後と、TST以外のアドオンの今後について

従来のアドオンの仕組みは、自由度の高さ故に様々なアドオンが登場する土壌となりました。
最初からWebExtensionsのような方針で始めたとしたら、ここまで多種多様なアドオンが出てくることも無かったでしょう。

しかし、APIと言えるAPIが無い状態でのモンキーパッチであったがために、従来のアドオンではFirefoxのバージョン依存や本体機能の劣化・阻害という副作用の発生を避けられませんでした。
また、それらを避けられるような造詣の深い人以外はアドオンを作れないという状況をも生んでしまいました。

WebExtensionsでないアドオンの廃止はいつか必ず起こる事で、それがFirefoxという名前の間に起こるのか、それともFirefoxが従来のアドオンと共に完全に死んで別のプロダクトが最初からそうなっていたか、という程度の違いでしかありません。
Firefoxという名前の間に起こったことで、設定の引き継ぎ等がスムーズに進みやすくなったと考えると、これが一番妥当だったのだと思います(本当は、もっと早くにそうなるべきだったのでしょうが……)。

WebExtensions APIでは、「Google Chromeとの互換性」に拘らない独自のAPIも提供され始めています。
browser.sessions.setTabValue()browser.sessions.getTabValue()browser.sessions.setWindowValue()browser.sessions.getWindowValue()はその一例で、TSTもこれが無くては実用レベルでのWE版開発は不可能でした。
要望や提案は却下される事の方が多い、というか自分が起票した提案はほとんど全部却下か放置されているのが現実ですが、明文化されていない領域での「ここまではあり」「ここから先は無し」というFirefox開発チーム内での判断基準が炙り出される事にも意義はあるはずと信じて、今後もめげずにやっていきたいと思います。


以上、TST WE版作成のための一連の作業の締めくくりとして、長々と思いの丈を語ってみました。

TST 2.0は一般向けにリリースしたことでより多くの耳目に晒されるようになったため、開発段階とは比較にならない量のバグ報告が連日寄せられている状況です。
TST 2.0の後はさっさとマルチプルタブハンドラのWE版の作業に移りたかったのですが、この分だとまだまだ時間がかかりそうです……

ちなみに、自分は普段はこういったアドオン開発の経験やその障害の原因調査を通じて培ったMozilla製品に関する知見を活かして、株式会社クリアコードでFirefox・Thunderbirdの法人向け技術サポート等の業務を行っています。
業務上でFirefoxやThunderbirdの技術的な事に関してお困りの場合、何かお力になれるかもしれません。

また、この記事中の図のように技術を図解する事にも関心があり、日経Linux誌にてシス管系女子というマンガ形式の技術解説記事も執筆しております。

ということで、図らずも上から下まで自分の持つ技能のすべてを投じた記事となってしまったのでした。
ご笑覧頂けましたら幸いです。