Jan 31, 2007

Split Browser開発のよもやま話(7):状態の保存と復元

作り込みのフェーズの続きですよ。だいぶ間が空いてしまったけど。

ブラウザの分割の状態を保存する、と一口に言っても、保存するべき内容は2つのレイヤに分けることができる。

  1. それぞれの分割されたブラウザの履歴の状態
  2. どの方向にどれだけ分割されているか、という状態

1の情報の保存と復元の処理は、nsISHistoryの状態の保存と復元が鍵になる。これはFirefox 2のセッション保存機能を実現しているnsSessionStore.jsの中身を見れば理解できるだろう。要約すると、処理の流れは以下のようになる。

  1. browser要素のsessionHistoryプロパティ(nsISHistoryのインスタンス)のgetEntryAtIndex()メソッドで、個々のヒストリエントリ(「戻る」「進む」で辿れる個々の履歴)を取得する。
  2. 個々のエントリのオブジェクト(nsISHEntry/nsIHistoryEntryのインスタンス)の、URIやタイトル、キャッシュのキーなどの情報を取得する。
  3. 取得した情報を何らかの形で保存する。
  4. Firefoxを再起動する。
  5. 保存されていた情報を読み込む。
  6. 保存されていた情報から、同じ情報を持ったnsISHEntryのインスタンスを生成する。
  7. browser要素のsessionHistoryプロパティにnsISHistoryInternalインターフェースでアクセスして、addEntry()メソッドでヒストリエントリとして追加する。

tabbrowser要素に対しては、これをタブの数だけ繰り返せばいい。

Split Browserでは、nsSessionStore.jsではなく、タブブラウザ拡張の同等の機能の方のコードをコピペして使った。nsSessionStore.jsのコードに比べて、このコードにはフォームの入力内容を保存・復元する処理が含まれていないんだけど、個人情報が云々というツッコミを受けるのも嫌だし、まあ、要望があれば追い追い……ってことで。

3と5のステップをいかにして実現するかは後述するとして、先にもう一つの要素の方に話を移す。つまり、ブラウザの分割そのものの状態――どの方向にどれだけ分割されているのか、という情報の収集についてだ。

入れ子の構造であるという点自体は、特に問題は無い。厄介なのは、親ノードとなるブラウザに対して上下左右4方向、どの方向に分割されたブラウザであるかという点。タブの状態の保存なんかではコンテナ要素から見たときの子ノードを頭から順番に見ていってそれぞれの状態を保存してるんだけど、Split Browserの実装方法では、素直にそうできないという問題がある。

例えばこんな状態になっていたとして……

<vbox>
  <subbrowser-container/> (1)
  <subbrowser-container/> (2)
  <hbox>
    <subbrowse-container/> (3)
    <tabbrowser/> (4:最初からある)
    <subbrowser-container>
      <vbox>
        <subbrowse-container/> (5)
        <hbox>
          <subbrowse/> (6)
        </hbox>
      </vbox>
      <subbrowse-container/> (7)
    </subbrowser-container>
    <subbrowser-container/> (8)
  </hbox>
  <subbrowser-container/> (9)
</vbox>

この状態でトップレベルのコンテナから内容を順に走査して(1、2、3……)情報を収集しても、それを「再現」するのは大変だ。最初からあるtabbrowser要素は、この場所を動かせないし後から挿入することもできないから、これのためにいくつも例外処理を書かないといけなくなる。tabbrowser要素より後だったら挿入位置のインデックスを1増やす、みたいな感じで。これは、真面目に考えるのがアホらしくなるくらいやる気がしない作業だ。

そもそも、要素の位置を指定して「n番目の子として追加」みたいなことをする機能自体が無くて、「特定のsubbrowser要素をキーとして指定し、そのsubbrowser要素から見て上下左右いずれかの方向にコンテナを追加する(ブラウザを分割する)」、という機能しか今の所は無いのでそこから作らないといけないことになる。そんな面倒な事、とてもとてもやってられません……

そこでSplit Browserでは、今あるものを最大限活用する、考えられる手法の中で一番泥臭いやりかたを採用することにした。コンテナの中を頭からスキャンするのではなく、コンテナの中に最初からあるtabbrowser要素、つまりFirefoxの本来のページ表示領域を中心にしてそこから右方向、左方向、上方向、下方向に走査していくというやり方だ。これは具体的に先の例で言えば「4、3、8、7、6、5、1、2、9」という順番なんだけど、これってよくよく考えてみれば、ユーザがFirefoxを起動してから行った操作手順ほとんどそのままである。だからその手順をそのまま記憶しておいて、次回起動時には「その手順どおりに処理した結果を得よう。……というのが、このやり方の基本的な考え方だ。

この場合のメリットはやはり、簡単な再帰だけで処理を書ける事だろう。指定したtabbrowserまたはsubbrowser要素を出発点として走査していき、さらに分割されたブラウザがあれば、またそこを新たな出発点として走査していく。これによって、コンテナ同士の入れ子関係に基づいたごくごくシンプルなツリー構造のデータを得ることができる。うん、再帰ってやっぱ重要だわ……

ちなみに、ブラウザの分割方向についてはPOSITION_LEFT、POSITION_RIGHT、POSITION_TOP、POSITION_BOTTOMという4つのフラグで管理してる。フラグで管理することによってまた別のメリットが生まれてるんだけど、それは今回の話からはズレるので、割愛する。

さて。分割の状態とブラウザの履歴、両方の情報をうまく収集して、さらにその状態を復元するところまでの、見通しはついた。残っているのは、先ほどスルーした部分。すなわち、「前述の方法で取得した情報を、どのようにしてどんな形式で保存するか」という部分だ。

セッションヒストリは、階層構造を持ったデータになっている。フレームを使用したページにおいては、個々のヒストリエントリはさらに「子供」のエントリを持っていて、全体としては入れ子の構造を取っている。こういうデータは単純なカンマ区切りやタブ区切りの形式で保存するのにはあんまり向いていない。

こういう複雑な情報を保存する方法として一番手っ取り早いのは、JSONまたはそれによく似た形式でデータを文字列化することだ。

JSONはAjaxが流行りだしてからよく使われるようになった(?)データ形式で、簡単に言えば、JavaScriptのオブジェクトリテラルとして評価可能な文字列によってデータを表現する形式だ。複雑な情報をただの文字列に一旦変換すれば、通信などで扱いやすくなる。また、受け取った側でJavaScriptのeval()で文字列を評価すれば、そのまま階層構造なども含めて元の情報を完全に復元できる。

JavaScriptではオブジェクトのtoSource()メソッドを使えばオブジェクトリテラルとして評価可能な文字列を取得できる。これは厳密なJSON形式と比べると若干ルーズなものなんだけれども、どうせ書き出すのも読み込むのもSplit Browser内部での話なので、そこはこだわらないでおく。ともかく、これをFirefox標準の設定システムで文字列値として保存して、次回起動時に復元すれば、OKというわけだ。

参考に、以下に実際に保存されているデータの例を示そう。

user_pref("splitbrowser.state", "({children:[{children:[{children:[], content:{histories:[{entries:[{id:3, uri:\"http://ja.www.mozilla.com/ja/firefox/central/\", title:\"Firefox \\u3092\\u4F7F\\u3063\\u3066\\u307F\\u3088\\u3046\", isSubFrame:false, x:0, y:0, children:[], cacheKey:0}, {id:4, uri:\"http://ja.www.mozilla.com/ja/firefox/help/\", title:\"\\u30D8\\u30EB\\u30D7\\u3068\\u30C1\\u30E5\\u30FC\\u30C8\\u30EA\\u30A2\\u30EB\", isSubFrame:false, x:0, y:0, children:[], cacheKey:0}], index:1}], selectedTab:0, uri:\"http://ja.www.mozilla.com/ja/firefox/help/\", width:245, height:233, type:\"subbrowser\"}, position:4, height:233}], content:{histories:[{entries:[{id:1, uri:\"http://ja.www.mozilla.com/ja/firefox/central/\", title:\"Firefox \\u3092\\u4F7F\\u3063\\u3066\\u307F\\u3088\\u3046\", isSubFrame:false, x:0, y:0, children:[], cacheKey:0}, {id:2, uri:\"http://www.mozilla-japan.org/addons/firefox/\", title:\"Mozilla Japan - Firefox \\u7528\\u30A2\\u30C9\\u30AA\\u30F3\", isSubFrame:false, x:0, y:0, children:[], cacheKey:0}], index:1}], selectedTab:0, uri:\"http://www.mozilla-japan.org/addons/firefox/\", width:245, height:356, type:\"subbrowser\"}, position:2, width:245}], content:{type:\"root\", width:627, height:596}})");

これじゃ読みにくいので、適当に改行などを入れて整形してみたのが以下のもの。

({
  content:{
    type:"root",
    width:627,
    height:596
  },
  children:[
    {
      content:{
        selectedTab:0,
        uri:"http://www.mozilla-japan.org/addons/firefox/",
        width:245,
        height:356,
        type:"subbrowser",
        histories:[
          {
            entries:[
              {
                id:1,
                uri:"http://ja.www.mozilla.com/ja/firefox/central/",
                title:"Firefox \u3092\u4F7F\u3063\u3066\u307F\u3088\u3046",
                isSubFrame:false,
                x:0,
                y:0,
                children:[],
                cacheKey:0
              },
              {
                id:2,
                uri:"http://www.mozilla-japan.org/addons/firefox/",
                title:"Mozilla Japan - Firefox \u7528\u30A2\u30C9\u30AA\u30F3",
                isSubFrame:false,
                x:0,
                y:0,
                children:[],
                cacheKey:0
              }
            ],
            index:1
          }
        ]
      },
      position:2,
      width:245,
      children:[
        {
          content:{
            selectedTab:0,
            uri:"http://ja.www.mozilla.com/ja/firefox/help/",
            width:245,
            height:233,
            type:"subbrowser",
            histories:[
              {
                entries:[
                  {
                    id:3,
                    uri:"http://ja.www.mozilla.com/ja/firefox/central/",
                    title:"Firefox \u3092\u4F7F\u3063\u3066\u307F\u3088\u3046",
                    isSubFrame:false,
                    x:0,
                    y:0,
                    children:[],
                    cacheKey:0
                  },
                  {
                    id:4,
                    uri:"http://ja.www.mozilla.com/ja/firefox/help/",
                    title:"\u30D8\u30EB\u30D7\u3068\u30C1\u30E5\u30FC\u30C8\u30EA\u30A2\u30EB",
                    isSubFrame:false,
                    x:0,
                    y:0,
                    children:[],
                    cacheKey:0
                  }
                ],
                index:1
              }
            ]
          },
          position:4,
          height:233,
          children:[]
        }
      ]
    }
  ]
})

これを見れば、階層構造を持ったデータが、オブジェクトリテラルや配列リテラルの組み合わせによって、きちんとただの文字列に変換されて保存されているのが分かると思う。

セッションヒストリの保存と復元のコードのコピペ元であるタブブラウザ拡張では、JSON風形式ではなくRDFを使って無駄にややこしいことをしていた。このせいで色々トラブルが起こっていたので、次バージョンではこれもJSON風の形式に統一したいところだなあ。

とにかくまあこんな感じで、ブラウザの分割の状態は完璧に保存・復元できるようになったわけですよ。そしてこの話題はまだしつこく続く。次は、ブラウズ領域の上下左右でポップアップ表示されるボタンの実装についての話

エントリを編集します。

wikieditish message: Ready to edit this entry.











拡張機能