Home > Latest topics

Latest topics 近況報告

たまに18歳未満の人や心臓の弱い人にはお勧めできない情報が含まれることもあるかもしれない、甘くなくて酸っぱくてしょっぱいチラシの裏。RSSによる簡単な更新情報を利用したりすると、ハッピーになるかも知れませんしそうでないかも知れません。

萌えるふぉくす子さんだば子本制作プロジェクトの動向はもえじら組ブログで。

宣伝。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。 以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能! シス管系女子って何!? - 「シス管系女子」特設サイト

Page 11/248: « 7 8 9 10 11 12 13 14 15 »

evalが危険でそれ以外の方法が安全だと思ってる人へ - Feb 08, 2010

先日、ソース表示タブのアップデート版をAMOにアップロードしたところ、公開申請が却下されました。「不必要なeval()が多すぎる。拡張機能におけるeval()の5つの間違った使い方(原文:Five wrong reasons to use eval() in an extension)をよく読んで出直してこい。(大意)」という感じのコメントが添えられていました。

また、ツリー型タブのアップデート版も別の人から同じコメント付きで公開申請を蹴られました。

全文+コメントを翻訳するくらいには当該エントリを読んだ僕ですから、今更読み返すこともないのですが、こう何度もこのエントリを引き合いに出して申請を蹴られると正直腹に据えかねる物がありますので、自分の考えをちょっとまとめておきたいと思います。

つたない英語力で頑張って英語に翻訳してみました。

evalの使い方に「正しい」も「間違っている」もない

まず何より明らかにしておきたいのは、言葉尻を捉えて反論するのもどうかと言われそうですけれども、evalの使い方には「正しい」も「間違っている」も無いということです。

例えばHTMLの仕様書には、em要素は強調を意味するとか、この要素はこの要素の中には記述できないとかの、色々な定義が書かれています。その定義から外れた使い方をすれば、確かにそれはinvalidな(間違っている)HTMLと言っていいでしょう。

では、evalはどうか。ECMAScriptの仕様書(リンク先のページには3rd Editionと書いてあるけどPDFは5th Editionです)15.1.2.1 eval (x)の項には、evalの仕様としては「与えられた引数をスクリプトとして実行する」という、挙動についての取り決めだけが書かれています。「何のためになら使ってよい」という風な記述は一切ありません。当たり前といえば当たり前ですが。

そこにあるのはただ、「その仕様の通りに動作した結果、何が起こるのか?」という結果についてのリスクの高低だけです。多大なリスクを伴う使い方、全くリスクを伴わない使い方、色々な使い方が考えられますが、動作する限りはそれらはすべて「仕様通りの正しい使い方」です。なので当該エントリのタイトルも本当は「evalの5つの危険な使い方」などとするのが妥当だと僕は思っています。

こういう場合にこういうデメリットがあること考えられる、だからこのような対策が必要だ、あるいはメリットとデメリットを天秤にかけないといけない、というのがリスク管理の考え方です。「間違っているから駄目だ」と思考停止するのは、天秤にかける事すら否定するということです。

例えばSSLを使っていない普通のHTTP接続で閲覧しているWebページの内容は、「第3者によって改竄されている可能性」を完全には否定できません。だからといって、そのページに書かれている情報は一切全く信用してはいけない、ニュースだろうが料理のレシピだろうが一切アテにしてはいけない、という考え方が健康的と言えるでしょうか? また、SSLは接続している先のWebサイトが本当にその運営者が運営するものであるかどうかということは保証してくれますが、運営者が善人か悪人かということまでは保証してくれません。相手が善人であると保証されないのだからネットで買い物はしたくない、カップラーメンもマンガの本もホッチキスの針も、詐欺に遭うのが怖いからネット経由では絶対何も買わない、という考え方は妥当でしょうか? そこまで行くともはやインターネット<ruby><rb>恐怖症</rb><rp>(</rp><rt class="読み">フォビア</rt><rp>)</rp></ruby>でしょう。

前出のエントリで「この使い方は正しい」とされている以外の使い方がされているevalを、個々のケースごとのリスクの高低を無視して一律で否定するのは、僕には単なるeval<ruby><rb>恐怖症</rb><rp>(</rp><rt class="読み">フォビア</rt><rp>)</rp></ruby>としか思えません。

そのevalには本当に、Webからやってきたコードが混入する恐れがあるのか?

前出のエントリで挙げられている5つのケースのうち、1番目(JSON形式のデータのパース)、2番目(プロパティ名が動的に変化する時に、そのオブジェクトのプロパティにアクセスするため)、4番目(HTMLやXULにインラインで記述されたイベントハンドラの実行)の3つのケースでは、Webからやってきた信頼されていないコードを特権付きで実行してしまう可能性があることに触れています。これはWebアプリケーションにおいてのSQLインジェクションに対する注意喚起と同様の物と言えます。

この観点でevalの使い方に神経質になることは大事です。僕も、evalを使うすべての人は、この点に気をつけなくてはならないと思います。この理由で公開申請が却下されたのであれば、その判断は実に妥当だと思いますし、反論の余地は全くありません。

では、今回のケースはどうでしょうか。

ソース表示タブツリー型タブでは、evalはFirefox本体の関数を書き換えるためにだけ利用しており、そこにWebから取得したコードが混入するとは考えにくいです。もしこの理由で申請が却下されたのであれば、それは、レビュアーがきちんとコードを読まずに、単に機械的チェックにおけるevalの警告だけを見て判断を下したという可能性が出てきます。これは言い換えると、「evalが機械的チェックで検出されたからNG、検出されなかったからOK」という風な安直な判断を下すようになっている可能性があるということです。

もしそうなのであれば、そのようなレビュー姿勢は宜しくないものだと自分には思えます。そういう機械的なチェックをくぐり抜けるための抜け道はいくらでもあります。難読化されたコードなどはその典型ですし、難読化されていない「読みやすそうなコード」でも、ぱっと見分からないようにごまかすのは簡単です。もし機械的なチェックの結果だけを見て判断を下しているのであれば、そういうケースによる危険なコードの混入の可能性は否定できないことになります(そういう場合、時間をかけてでも・判断が遅くなってでもきちんと精査するべきでしょう)。そういうケースに対しても対処できるようにというのが、わざわざ人の手でレビューすることになっている理由の1つであると自分は信じています。

evalによる関数の書き換えのリスクは、それ以外の方法のリスクと比べてそんなに高いのか?

ソース表示タブツリー型タブで使われているevalはすべて、前出のエントリで挙げられている5つのケースのうち5番目のケース、「Firefox内で使われている関数の内容を書き換えるため」に該当するものです。前出のエントリでは、この使い方を以下の理由から非難しています。

  • 書き換える対象の関数の内容が変化した時に、ブラウザ自体がまともに動作しなくなってしまう恐れがある。
  • せっかく修正されたセキュリティ上の問題を、再び持ち込んでしまう恐れがある。

そして、代わりに推奨するやり方として以下のような「無理のないやり方」を挙げています。

  • 関数を別の関数で置き換えた上で、置き換え後の関数内で元の関数をfunc.apply(this, arguments)で呼ぶ。
  • Object.watch()を使って、特定の変数やプロパティの値が変化したタイミングで処理を行う。
  • そういう「無理のないやり方」では実現できないことをしようとしているのであれば、もう素直に諦める。

換言すると、前出のエントリの筆者は「このような無理のないやり方の方が、evalによる関数の置き換えに比べて、前述のような事態が発生するリスクが少ない」と述べていると言えます。

僕には、これはアンフェアな言い方と思えます。リスクの多寡で問題を語るのであれば、ここに挙げられていないリスクについても考慮するべきでしょう。そうすることで初めて、僕のような人が「evalによる関数の書き換え」という手法を選択している理由が明らかになります。僕だって、考えなしにevalを使っているわけではありません。メリットとデメリットを天秤にかけた上で、その方がリスクが小さいと考えてevalを使っているのですから。

リスク:Firefox自身の機能や他のアドオンと競合する可能性について

アドオンを「機能を実現する方法」の観点で見ると、以下のように分類できます。

  1. Firefoxや他のアドオンが提供するAPIに則ったアドオン。ツールバーへのボタン追加、サイドバーパネルの追加、nsIContentPolicyによるコンテンツフィルタリング(AdBlock Plus)、Jetpack Featureなど。
  2. Firefoxや他のアドオンがAPIを提供していない箇所に改変を加えるアドオン。
    1. XBLを使って機能を加えるアドオン。
      1. 新規にXBLを適用するアドオン。
      2. 既存のXBLを上書きするアドオン。Tab Mix Plusなど。
    2. JavaScriptの関数のはたらきを変更して機能を加えるアドオン。
      1. 関数を置き換えるアドオン。
      2. Object.watch()等で本来の処理に割り込むアドオン。
      3. 関数をevalで書き換えるアドオン。
    3. CSSでXUL要素の見た目を変えるアドオン。Personas、テーマなど。

1つのアドオンが複数の分類にまたがる場合も多いのですが、このうち「他のアドオンとの競合の可能性」という点で本当に低リスクであると言い切れるのは、1の「APIに則ったアドオン」の枠から外れないものだけです。それ以外はどんなアドオンであっても、Firefox自体の挙動や他のアドオンに何らかの影響を与える可能性があります。テーマですら例外ではありません。

前述の「無理のないやり方」の説明では、さもそのようなやり方を採用することによって競合のリスクが全面的に低減されるかのような書き方がなされていますが、場合によってはこの方が却って競合してしまう場合すらあります。実際、Firefox 3.6上でサイドバーの開閉状態の変化を捕捉するためにIDがsidebar-boxである要素のhiddenプロパティに対してObject.watch()で監視を行おうとしたところ、XUL要素の挙動が破壊されて、サイドバーの開閉自体が行われなくなってしまいました。(これは結局、DOMAttrModifiedイベントを監視してhidden属性の値の変化を見るという方法で回避することにしました。)

別の例として、「無理のないやり方」で関数を丸ごと置き換えられてしまうと、evalで元の関数を書き換えようとしていた他のアドオンが、関数を書き換えられないせいで期待通りに動作しなくなってしまうということも考えられます。……「そんな邪道なやり方をしている方が悪いのだ」という非難は無しですよ? 他のアドオンに与える影響を考えていないのは、お互い様なのですから。

また、これはリスクとは別の話ですが、「evalによって特定の箇所に特定のコードを注入することによって関数の定義内容を変えてしまわなければ実現できない機能」というものもあります(前出のエントリに対するコメントでも、Dorando氏が例を示しています)。そのような「無理なやり方」でやらなければならない事はするべきではない、というのが前出のエントリの著者の結論のようですが、後述する物も含めたリスクの点で明らかな優位性を主張できないのであれば、「無理なやり方はするべきでない、なぜならevalの間違った使い方だからだ」「evalの間違った使い方はするべきでない、なぜならそれは無理なやり方だからだ」というトートロジーに過ぎません。

リスク:Firefoxのバージョンが変わると予期しない結果になる可能性について

この点のリスクは、evalによる関数の書き換えでも、「無理のないやり方」と呼ばれている関数の置き換えでも、同様に存在します。前出のエントリでは「置き換え対象の関数の内容が変わったらどうするんだ」ということを指摘していますが、それを言えば、関数の名前が変われば「無理のないやり方」も破綻します。セキュリティアップデートでは関数の名前やそれぞれの働きが変わることはない、というのはただの思い込みです。そんなルールはどこにもありません。

また、「置き換えた関数A」→「置き換えた関数B」の順で処理が行われることを期待してコードを書いてる場合に、「置き換えた関数A」だけが実際には置き換えられなかったとすると、「置き換えた関数B」が動作する上での前提が崩れますので、その時どんなトラブルが起こるかは予想できません。これはFirefoxのバージョンが変わった時だけでなく、他のアドオンと同時に利用した時の競合についても全く同じ事が言えます。

いずれの場合にせよ、Firefox本体や他のアドオンのバージョンアップに対して、改変を加える箇所のソースコードをその都度調査し直さなければならないという、メンテナンスのコストの問題は、どちらにも共通して存在しています。「evalと関数置き換えのどちらの方が元のコードの変化に強いのか」ということを論じてもあまり意味はなく、どちらの手法を使うにせよ、Firefoxのバージョンアップに際してきちんと動作を検証しているかどうかや、前提が崩れた時のためのフェイルセーフがきちんと考えられているかどうかの方が、リスクに対する備えとしては重要且つ効果的なのではないか? というのが僕の考えです。

Firefoxのアドオンやテーマの正体は「動的に適用されるパッチ」です。ソースコードに対して、そこら辺に転がってる野良パッチをあててビルドする、というのはLinuxを使ってる人にはよくある光景ではないかと思いますが、「パッチが書かれた時のバージョンと違うバージョンのソースにパッチを当てること」がいかに危険かは、言うまでもないでしょう。Firefoxのアドオンを「アドオン作者によって動作確認されたバージョン」以外のバージョンにインストールすることは、それと全く同じ事です。

リスク:メンテナンス性、ソースの可読性が低下する可能性について

evalによる関数書き換えの場合、ソース中には「書き換える対象の関数名」「書き換えたい前のコード片」「書き換え後のコード片」だけが書かれることになります。

このコードを解読するためには「書き換える対象の関数」の内容を参照する必要があるため、その内容が頭に入っていない場合(大抵はそうでしょう)、解読が難しくなりがちです。その代わり、ソースコードの量自体は必要最低限になります。また、書き換える対象がロジックに絡まない「widthをheightに書き換える」という風な部分なのであれば、コードの読みやすさという点でもさほど問題は無いと言えます。このあたりは、「evalで関数を書き換える時の、互換性や後々のメンテナンス性を高く保つためのノウハウ」が重要になってくる所です。

関数の置き換えを行う場合も、ソースコードは結局は断片的な物になります。「前処理の関数」「元の関数をラップする関数」「後処理の関数」「元の関数の中で特定のプロパティに変更が加えられた事を捕捉して処理を割り込ませる関数」などが細切れに定義されますし、また、元の関数の処理結果の受け取り方法として「戻り値」以外の情報を使うのであれば、結局は元の関数の内容が頭に入っていないといけないことになります。

元の関数の中に変更を加えなければどうにもならない機能をどうにかして実現したい場合(前出のエントリのコメントで指摘されているようなケースなど)、こちらの手法にこだわるのであれば、「元の関数に必要な変更を施した後の関数」をアドオンの中で定義しておく以外に手はありません。そうなると、Firefoxや他のアドオンのコードと全く同じコードがそのアドオンの中に大量に含まれるようになってしまいます。これは、以下のような問題に繋がります。

  • それぞれのライセンスが衝突してしまうために、コードを丸ごと引用できないという事態が起こり得る。その機能を実現できない、という苦渋の決断を迫られる。
  • 前出のエントリ内でも指摘されている「せっかくFirefox(や他のアドオン)の側でセキュリティ上の問題が修正されても、関数を古いバージョンのそれに基づいた改変版に丸ごと置き換えてしまうことで、再びセキュリティ上の問題が持ち込まれてしまう」問題が起こり得る。
  • 単純にソースの行数が増加する事それ自体も問題となる。

リスク:公開申請時のレビューが通りにくくなる可能性について

これは「メンテナンス性、ソースの可読性が低下する可能性」にも共通する話です。AMOのエディターは最終的にはコードレビューによってそのアドオンの安全性を精査する必要があり、コードがトリッキーなものであればあるほど、また、コードの行数が増えれば増えるほど、コードレビューは大変な物になります。その意味で、関数の部分的な書き換えも関数全体の置き換えも、コードレビューの手間が増えるという点では一長一短です。

もっとも、今回の事例のように「evalを使っていること」を理由に公開申請が却下されることが相次ぐのであれば、eval無しでトリッキーで可読性の低い分量の多いソースコードの方が、比較的レビューが通りやすいという事になるのかもしれませんけれどもね。その場合、これもやはり「レビューを通してもらうためにはevalを使わない方がいい、なぜなら、evalを使っているとレビューが通らないからだ」という、何の説明にもなっていないトートロジーになる訳ですが。

まとめ

上記のような理由で、僕はソース表示タブツリー型タブの中で使われているevalが不必要な物ばかりであるとは考えていません(むしろどれも必要だからそうしているというのが自分の立場です)し、それらのevalの個々の用法が「これを読んで出直せ」と提示されたエントリの中で書かれている「代替案」に比べて目立ってリスキーであるとも思っていませんし、その「代替案」で代替できるとも思えません。

「evalの方が危険だ」と言っている人のうち何割の人が、本当にMozillaのコードを読んだことがあるのか、読み続けてきたのか、APIが用意されていない箇所でブラウザの機能を拡張する事に取り組んできたのか、継続的にFirefoxのバージョンアップに追従し続けたことがあるのか、他のアドオンとの競合を解消する方法を模索し続けてきたのか、僕には疑問に思えます。

XPCOMコンポーネントのIDL定義で「FROZEN」というフラグが立っている物以外のすべてのAPIは、いくらでも変更される可能性があります。FROZENになっているAPIは、Preferenceの読み書き、W3CのDOM関連など、全体から見れば本当にごく一部だけです。ブラウザ内のJavaScriptのレイヤで定義された関数に至っては、それが変更されない保証などどこにもありません。「Firefoxのバージョンに依存しないAPIが提供される」と思われていたFUELですら廃止される可能性が出てきている、Mozillaはそんな世界なのです。それで「evalの方が危険だ、同じ名前の関数で置き換える方がマシだ」と言うのがどれほど馬鹿げた話か。

ですので僕にとっては、evalの使用を理由として公開申請が却下された今回の出来事に対しては、不当な判断を下されたという思いが強いです。

ツリー型タブのGoogle Chrome版は作らないの?(Tree Style Tab for Google Chrome) - Feb 05, 2010

Q

Any plans to add Tree Style Tab to Google Chrome or Chromium?

ツリー型タブをGoogle ChromeもしくはChromiumに移植する計画は無いの?

A

Now I have no plan to export Tree Style Tab to Chrome. The fact "Tree of tabs are permanently shown, without any operation" is the best benefit of Tree Style Tab for me. Currently there seems to be no API to do it. I don't want to use such a feature if clicking on a toolbar button (or any other operation) is required to show the view.

Moreover, it is the main reason of my development that I want to improve my PC environment. Because I'm not planning to switch to Chrome, I will not develop Chrome extensions.

By the way, there are some similar extensions VerticalTabs, Tab Menu, and Tree Style Tabs (Beta) (Note: it has same name but it is NOT my product!). They possibly help you.

Updated at 2012-11-22: More extensions, Sidewise Tree Style Tabs and Tabs Outliner are available now. They are good alternative of Tree Style Tab on Google Chrome.

今の所、私自身がツリー型タブをChromeに移植する予定はありません。私は、「ツリー型タブ」の便利な所は「タブの一覧(ツリー)が常時表示されている」点だと思っています。現状のChromeの拡張機能向けのAPIでは、そのような拡張機能は残念ながら作ることができないようです。ボタンをクリックしないとツリーが表示されないような機能は、自分は使いたくないです。

また、私自身の開発のモチベーションは主に「自分が日常的に使う環境を使いやすくしたい」という所に支えられています。私は今の所Chromeを常用するつもりが無いため、Chrome用拡張機能を開発するモチベーションがありません。

なお、似たような拡張機能でVerticalTabsTab MenuTree Style Tabs (Beta)(注:同じ名前ですが私が開発した物ではありません!)というものがあります。まずはこれらを試してみてください。

2012-11-22追記:その後、Sidewise Tree Style TabsおよびTabs Outlinerという拡張機能が公開されています。これらを使うと、ツリー型タブにより近いユーザー体験をGoogle Chrome上で得られます。

ツリー型タブでタブバーの「新しいタブ」ボタンを隠せなくなった(How to hide "New Tab" button in the tab bar?) - Feb 05, 2010

Q

I just updated the Tree Style Tab addon and the new tab button that used to be hidden is now visible. Is there a way to hide it? I always use Ctrl-T, so it just wastes space.

ツリー型タブを更新したら、今まで隠れていた「新しいタブ」ボタンが表示されるようになってしまいました。これを隠す方法はありませんか? 私はCtrl-Tをいつも使っているので、このボタンは不要です。

A

Sorry, I removed the option because it is not a feature related to "tree". Instead, you'll hide the button by customizing your userChrome.css like:

.tabs-newtab-button {
    display:none !important;
}
/* hide the "shadow" */
tabbrowser
  .tabbrowser-strip
  .tabbrowser-tab
  .tabs-container
  .scrollbox-innerbox {
    background-image: none !important;
}

Note: These examples work on Tree Style Tab 0.8.2009100101 or later.

すみません。「ツリー」と関係ないものだったので、その機能は削除してしまいました。代わりにuserChrome.cssに上記のようなスタイル指定を書き加える事で、ボタンを非表示にできます。

縦置きタブバーの下にサイドバーを統合するUnified Sidebarをリリースしたよ - Feb 05, 2010

とりあえずMozilla Add-onsに置いた

ツリー型タブに対して表題のような要望が寄せられることが何度かある。ツリーと関係ないからツリー型タブにそういう機能を入れる気はないんだけど、まあそういうニーズはあるんだろうなというのは理解できる。なので以前にuserChrome.cssでそれっぽくする方法を考えてみたりもした。その時の結論としては「ちゃんとアドオンにしないと駄目だね」という感じだったんだけど、結局誰もアドオン化してくれてなかったようだったので、仕方がないから自分で作った。

このアドオン自体はタブを縦置き表示する機能は持ってません。単にサイドバーの表示位置を変えるだけです。タブを縦置きするにはuserChrome.cssで頑張るか以下のアドオンを使って下さい。

他にもタブを縦置きするアドオンがあれば、なるべく連携するようにはしたいので、このエントリにコメント付けるなどして教えて下さい。

タブをまとめてリロードする時にちょっとずつリロードを行うようにする「Reload Tabs Progressively」をリリースしたよ - Feb 02, 2010

とりあえずMozilla Add-onsにうぷした

マルチプルタブハンドラについて「複数のタブをリロードする時、全部一気に読み込み始めるんじゃなく、3秒ずつ位間を空けて読み込むような機能を付けて欲しい」という風な要望をもらったんだけど、マルチプルタブハンドラに依存する機能にするよりは単機能で使えるようにしといた方がいいような気がしたので、サクッと作って公開した。

  • BarTapで待機状態になってるタブについては、リロードではなく普通の読み込み開始になります。
  • ナローバンドだったりPCが遅かったりすると効果的かもしれない。
  • 副作用として、いつまでもリロード中になってしまうようなページ(Cometでセッション張りっぱなしでレスポンスを待つようなページ)があると他のタブの再読み込みがいつまで待っても始まらないかもしれません。
  • tabbrowser要素のreloadTab()メソッドとreloadAllTabs()メソッドをゴッソリ入れ替えてるので、この辺を触ってるアドオンがもしあったら衝突するかも。

こういう風にブラウザそのものの挙動を変えてしまうアドオンはChrome(Chromium)では作るのは難しいような気がするけど、実際にチャレンジしたわけではないので、ほんとのところはどうなんだろ。

コアJetpackミーティング - Jan 26, 2010

23日にMozilla Japanで行われたコアJetpackミーティングに参加してきました。2月にGomitaさんとあかつかさんがMozilla Corporationまで行く時に持って行く意見・アイデア等をまとめるのが趣旨の会合でした。Mozilla信者な視点だけからでは意見が偏るんじゃないかと思ったので、Chrome拡張機能のえらい人のos0xさんにも来てもらいました。

議題は「Jetpackのスクリプトの開発環境について」「Jetpack Galleryについて」「Jetpackが提供するAPIについて」の3つでした。以下、思い出しながらグダグダ書いてみます。

開発環境について

既存のアドオンを冷遇していくとかの話はさておき、これまで普通のアドオンを作ってた人が、Jetpackで事足りるような機能についてはJetpackでやるようになる、という未来を実現するには何が必要か? という話をしました。

結論としては、Bespin要らないんじゃね、ていうか好きなエディタを使わせて下さい、的な所に意見が集中しました。Bespin自体技術デモの性格が強いような物で、IMEがまともに使えない等のどうしようもない問題を沢山抱えていて、はっきり言って「これで(vim|Emacs|秀丸)を捨てられる!」みたいな未来は当分、つうか絶対に来ないと思われるわけで。かといって、eclipseみたいなファットな開発環境をJetpackの中に突っ込むというのも、それはなんか違うんじゃないの? とも思うし。既にデバッグ用にはFirebugを使うのがお勧めみたいな事も言ってる事だし、適材適所でそれぞれ分担すりゃいいじゃないの。頑張ってここ(BespinとJetpackの連携)に色々と機能追加する事にリソースを作よりも別の所で頑張りませんか? というのが、参加者達の総意だった気がします。

「新規のアドオン開発者」として取り込みたいターゲットと思われるWeb制作の現場の人のためには何をすればいいか? という事を次に議論したんですが、参加者の中にWebデベロッパが一人もいないという状況だったので(ひでーな!)想像だけで議論した限りでは、とりあえず、「スクリプトの頭と尻にお約束の構文を書いたら後はwindow等の見慣れたキーワードに普通にアクセスできる」ような状態にしておいた方がいいんじゃないのか、という事を提案しました。Greasemonkeyが受け入れられたのって、Webページの中に埋め込まれるスクリプトと全く同じ感覚で書けたからだと思うんですよね。で、Webのスクリプトを書く時、名前空間の違いとかクロスウィンドウなスクリプトとかそういうのを意識する事ってまず無くて、1つのフレームの中で動くスクリプトを書く感覚が染み着いてるんじゃないのかと。その感覚でスクリプトを書けるかどうかってのが、Webデベロッパに対する第1印象を決定づけるんじゃないのだろうかと。ちなみに、Google Chromeではこの要求に対する回答として、「コンテントスクリプト」という種類の拡張機能であればWebページ内のスクリプトと同じ感覚で書けるという設計になってるようです。

一般ユーザ、つまり今までアドオンを作った事もなければスクリプトを書いた事もないという層の人に対してはどうか。

グラフィカルプログラミングがどうとかそういう話もチラッと出はしたんですが、そこに力を入れるのもやっぱりJetpackの仕事じゃないんじゃないの? と。そういうのは大学の研究室とかで頭のいい人達が散々やってるわけだし。

ポイントは、「簡単にプログラムをかけるかどうか」「知識が無くてもプログラムをかけるかどうか」ではなくて、そもそも「プログラムを書かなくてもカスタマイズできるかどうか」なんじゃないのか、という指摘が出てました。Ubiquityでは複数のコマンドをユーザが組み合わせて使う事を想定している(これはコマンドラインで複数のコマンドをパイプで繋げて使うような感覚)、というのを引き合いに出して、例えるなら「新しい文房具を簡単に作れるようにする」んじゃなくて「ハサミとホッチキスとセロテープ、という道具を手に入れたユーザがそれを好きなように組み合わせて使えるようにする」という風な。そこが大事なんじゃないのか、と。だから例えばAutoPagerizeのSITEINFO on wedataとか、アップローダに勝手改造版スクリプトをアップロードするみたいな、「スクラッチでスクリプトを書ける人以外でも気軽に改良できる、参加できる」ような仕組みがあるといいんじゃないか? という提案が出ました。そういうAPIがJetpackに標準で入ってて、Jetpack GalleryでユーザがSITEINFOをどんどん追加できるようになってて、APIを使っていればそれを簡単に取得できる、みたいな仕組みが用意されていれば、もっと「Webを便利にしてくれる人」の裾野が広がるのではないかなー。と、僕は思ってます。

Jetpack Galleryについて

Jetpack Galleryへの要望・提案としては、まず前述の話にも出た、JetpackのAPIとも連携したwedataのような仕組みがあるといいんじゃないかという話を出しました。

それと、ドキュメント類が不足してる問題への解として、Galleryに登録されてるスクリプト全部をオンラインで検索できるMXRみたいな機能があればいいんじゃないの?という話も出しました。実際、自分がアドオン作る時でも、使い方が分からないAPIはMDCのドキュメントを見るだけでなく、MXRでFirefoxのソースを検索して「ほうほうこうやって使うのか」と調べる場合が非常に多いです(MDCにドキュメントが無ければそれしかないし)。ドキュメントを書け書けと言っても人はめんどくさがって絶対にそんなもん用意せんのだから、じゃあ既にあるコードをスニペットとして全部活用できるようにすりゃあいいじゃないか、という意図での提案です。

現状のGalleryにはレビューの仕組みがないので危険だ、という話も出ました。これは次の話にも関係してるので、そっちで詳しく書く事にします。

APIについて

ここは議論が割れました。あかつかさんはrawのような仕組み、つまりAPIが用意されてない部分にアクセスしてその気になればなんでもできるような余地を残しておいた方がいいんじゃないのか、という立場で、僕とかは、そういう余地は一切残すべきでないという立場で話をしました。

確かに、そういう余地があったからこそ今のFirefoxのアドオンはここまで色々なものが出てきたわけで、そういう自由度をなくしてしまうのは残念だという感覚は、僕もよく分かります。ただ、だからってFrozenじゃない所に全部アクセスできるようにしてしまうというのも乱暴すぎて、それじゃあ結局今のアドオンのように、Firefoxのバージョンが上がったら使えなくなっちゃいました、アドオンのバージョンアップ待ちです、みたいな状況が絶対また出てくる。それって、Jetpackが謳う「安定したAPIでバージョンごとの非互換から解放されます」という話とまるっきり矛盾してしまうわけです。

それに、レビューの問題もある。今はJetpack GalleryにはAMOのような「レビューが通らないと一般公開されない」という風な仕組みはないけど、「最初は無難そうなスクリプトで登録して関係者を欺いて、後からプライバシーぶっこ抜きなコードを追加したものを自動アップデートで一斉配信して、悪意のある開発者がウマー」という事態を防ぐには(今のGalleryではこれが出来てしまう!)、レビューの仕組みは絶対必要になります。

その時に、rawで生のXPCOM等を直接触れてしまう仕組みがあると、レビュワーは結局全部のコードをしっかり精査しないといけないことになる。レビュワーにも知識と経験が求められる。それでは、今のAMOにありがちな「有能なレビュワーのリソースは限られてるので、レビュー完了まで何ヶ月も待たされてしまう。なので、何ヶ月も最新版が公開されないで放置されてしまう。」という風な状態が、Jetpack Galleryにも発生してしまうわけです。

Jetpackが提供するAPIしか使えないのなら、そういう状況に陥る事を防げる。例えば、「Jetpackが提供するAPIでなければファイルにアクセスできないと」いう仕様で、スクリプトの先頭に書く各種の宣言部分で「このパス以下のファイルしか読み込みません」という宣言を書かなければファイルアクセスは一切できない、ファイルアクセスをする時も宣言された範囲のファイルにしかアクセスできない、そういう設計になったとしましょう。すると、レビュワーは冒頭の宣言部分だけ読めば「こいつは危険だな」とか「こいつは安全だ」といった判断ができて、レビューが早く進むようになる。また、スクリプト作者が面倒くさがって「あらゆるファイルにアクセスする可能性があります」みたいな宣言を書くと、レビュワーから疑われていつまでも放置されるから、いつまで経っても公開されないことになる。なのでスクリプト作者は詳細な指定を宣言に嫌でも書かないといけない事になる。そういう風に、みんながみんな自分の利益を最大化する事が結果としてコミュニティ全体の利益に繋がるような仕組みを、今から作る事ができるわけです。

Jetpackという仕組みをゼロから作ろうとしてる今こそ、過去のアドオンの「Firefoxのバージョンに依存する」「レビューにめちゃめちゃ時間がかかって、いつまでも最新バージョンが公開されない」という問題と決別するチャンスなんですよ。なんでそのチャンスをみすみす見逃して、同じ問題をまたJetpackの世界に持ち込むんですか? と。

だいたい、Jetpackが本体に入った後も既存のアドオンの仕組みは依然として残り続けるわけで、偉い人はXULとXPCOMを全部なくすとか言ってるけど実際にコアのコードを書いてる人曰く「そんなの絶対無理(加藤さん・談)」なわけで、だったら「安定したAPIのJetpackで、まずは作ってみる。それでできない事が出てきたら、自由度の高い(でもAPIが不安定な)既存のアドオンの仕組みで作る方にステップアップする」というスキームでやっていけばいいじゃないですか。と。

他には、プラットフォーム(XULRunner、Geckoのレイヤ)でやるべき事とJetpackがやるべき事はちゃんと切り分けて欲しいという話も出ました。例えば音声取り込み用のAPIなんてのは、Jetpackにプラットフォーム別のバイナリをブチ込むんじゃなくて、Geckoで面倒見るべき話でしょう。そのせいでJetpackの構成ファイルが何MBにもなってダウンロードも起動も遅くなって、その一方で、ほんのちょっと起動を速くするためにFUELを廃止するって、何ですかそれは? 何か大事な事を履き違えてるんじゃあないですか? と。

とにかく、Jetpackというプロジェクトが何にフォーカスしているのか、プロジェクトのポリシーが僕には全然見えない。Jetpackをウォッチし続けてるcon_mameさんもteramakoさんも、Jetpackの方針が揺らいでるように見えるという風におっしゃってました。なので、Mozilla Corporationに行く時にはGomitaさんとあかつかさんにはぜひとも、Jetpackが最優先しようとしているのは何なのか? 他のものを犠牲にしてでも貫かないといけないと考えているポイントはどこなのか? というプロジェクトの指針を明らかにしてきて欲しい、という事をこのミーティングの参加者達の最大の関心事として託しておきました。

余談:イノベーションの話

ミーティングの最中にtwitterで言及されてMicrosoftのAzureの話を読みました。既存の開発者のために腐心しすぎるとイノベーションが起こらなくなってしまう、僕のようなロートルがいつまでもでかい顔して居座っていられるような状況にしてしまっては新しいプレーヤーが参入して来れない、というのは実に耳の痛い話だと思いました。

もちろん将来的に、いつかはJetpack自体も今のアドオンのように「もう時代遅れだ」と冷遇され切り捨てられる時代が来るのかもしれません。が、それはその時に考えればいい事なんじゃないかと僕は思います。

しかし自分が特に耳が痛いのは、既存プレーヤーがデカいツラしやがるという話です。自分の既得権益、自分の居場所を守るために必死に抵抗すればするほど、自分自身が今のまま何も変わらずに一線に立ち続けようとすればするほど、自分がすがりついている物の未来を奪う事になって、自分も他の人も不幸にする。「シーンの最前線から退く覚悟はあるか?」というエントリは、本文の内容は全然この話題と関係ないし趣旨も違うけど、でも僕は最近この事を考えがちなので、タイトル見ただけでこの事に結び付けて受け取ってドキッとしました。みんなのためを思うなら、老兵は黙って去って、不幸になるのは自分だけで終わらせないといけないんじゃないのか。

余談:Mozillaの生き残り戦略について

Microsoftは、Windows 95上でWindows 3.1用のアプリの動作を保証するために、Windowsの側で頑張って対策をしてまで互換性を維持したそうです。VistaのUltimateだったかWindows 7だったかではWindows XPとの互換性を維持するために仮想マシンまで用意しました。昔のAPIでも、「obsolete」という風にはMSDNに書いてあっても、動く事は動くという状態でずっと残ってるそうです。そういう「とにかく過去のソフトウェア資産を使い続けられるようにする」戦略をMicrosoftは取り続けて、製品はいまいち垢抜けない物ばかりになってしまいましたが、その結果としてシェアはものすごい事になりました。圧倒的多数の凡人がちょっとずつ落とすお金のおかげで、Microsoftは食えてます。

Appleは、ジョブズが思い描く「素晴らしいプロダクト」を目指すために、古いAPIを容赦なく切り捨ててがんがんアップデートしてます。Mac OS Xで「obsolete」となったAPIは、2つ後のバージョンくらいではもう綺麗さっぱり消えてしまうそうです。その結果、ついていけない脱落者が多くなるし、世界の過半数を占めるようなものすごいシェアは獲れてない。けれども強烈な信者ができて、彼らのおかげでAppleは食えてるわけです。

Googleは広告のシェアをガッツリ掴む事で、Operaはモバイル市場をガッツリ掴む事で、食っていってますよね。

で、Mozillaはどんな道を歩むつもりなんでしょうか。

今までの路線を見る限り、安定版リリースの寿命は短い(次のメジャーバージョンが出たら前のバージョンは半年で更新が打ち切られる)し、安定したAPIになるとか言ってたFUELもメジャーバージョン2つと保たずに黒歴史になってしまいそうだし、どう見てもゲイツよりはジョブズの生き方に見える。でも、ジョブズがジョブズの生き方を貫けてるのはあくまで、信者がお金を出して高い製品を買ってくれてるからですよね。でもこれって、言ってみればブランド物の生き残り戦略ですよね。Mozillaは物を売ってないから、その戦略じゃあ明日のおまんまが食えなくなるんじゃないの? 開発者を養っていけなくなるんじゃないの? そこが僕は心配でならないです。「いやオープンソースでバザールだから開発者はみんな善意で関わり続けてくれるんだよ」、という反論はすぐに思いつきますけど、今は世間はみんなWebkitに流れてますよね。オープンソースの開発者も、Geckoより将来有望な(ように見える)Webkitに流れていくんじゃないですか? その時、Geckoに関わり続けてくれる開発者はどれくらい残るんでしょうか?

上に挙げた各社のどれとも違う別の道を歩む、というのが多分、Mozillaにとっての模範解答なんでしょうけどね。その「別の道」ってのが一体何なのか、僕にはまだよく分からんです。

汎用的なアンドゥ・リドゥ機能を提供するoperationHistory.jsの、理解しておかないといけない特性 - Jan 18, 2010

前のエントリではoperationHistory.jsの基本的な使い方を説明しましたが、次は、これを使うにあたって理解しておかないといけないポイントを解説しようと思います。


UIに対して行うアンドゥ可能にしたい操作の中には、他のアンドゥ可能にしたい操作を内部で呼び出すものがあるでしょう。例えばブックマークフォルダの内容をまとめてタブで開く時などに使われるtabbrowserのloadTabs()メソッドは、新しい空のタブを開くaddTab()メソッドを内部で呼び出しています。「新しいタブを開く」操作と「ブックマークフォルダの内容をまとめてタブで開く」操作をアンドゥ可能にする際は、これらのメソッドに対してそれぞれアンドゥ・リドゥの処理を定義することになります。

このように「アンドゥ可能な処理」同士が入れ子になっている時、operationHistoryは、それらすべてのアンドゥ可能な処理について、実行が始まった順番通りに履歴に登録を行います。例えば「loadTabs()で3つのタブを開く」という場面では、以下のように処理が行われます。

  1. loadTabs()実行。アンドゥ可能な操作Aが始まる。
    1. 対応する履歴項目A'が登録される。
    2. 1つ目のタブのaddTab()実行。アンドゥ可能な操作Bが始まる。
      1. 対応する履歴項目B'が登録される。
      2. アンドゥ可能な操作Bが完了する。
    3. 2つ目のタブのaddTab()実行。アンドゥ可能な操作Cが始まる。
      1. 対応する履歴項目C'が登録される。
      2. アンドゥ可能な操作Cが完了する。
    4. 3つ目のタブのaddTab()実行。アンドゥ可能な操作Dが始まる。
      1. 対応する履歴項目D'が登録される。
      2. アンドゥ可能な操作Dが完了する。
  2. アンドゥ可能な操作Aが完了する。

この時の操作B~Dは、操作Aが完了する前に、操作Aの中から呼び出されています。そのため、これらに対応する履歴項目B'~D'は、完了していない操作Aに対応する履歴項目A'の子項目として登録されます。つまり、このような親子関係が形成されます。

  1. 履歴項目A'
    1. 履歴項目B'
    2. 履歴項目C'
    3. 履歴項目D'

他の履歴項目の子項目として登録された履歴項目は、親となる項目の一部として扱われます。子項目になった履歴項目は、アンドゥ可能な操作の履歴の一覧には登場せず、「最大100回までアンドゥ可能」といった場合、子項目の数はそのカウントに含まれないことになります。

この履歴項目A'に対してアンドゥを指示すると、operationHistoryは以下の順で処理を行います。

  1. 履歴項目D'のonUndo()
  2. 履歴項目C'のonUndo()
  3. 履歴項目B'のonUndo()
  4. 履歴項目A'のonUndo()

この時の実行順序は項目の登録時の逆順であることに注意して下さい。なお、後で解説しますが、遅延処理のための仕組みによってこの実行順序は保証されます。前の項目のonUndo()が終わる前に次の項目のonUndo()が始まるという事はありません。

逆に履歴項目A'に対してリドゥを指示すると、operationHistoryは以下の順で処理を行います。

  1. 履歴項目A'のonRedo()
  2. 履歴項目B'のonRedo()
  3. 履歴項目C'のonRedo()
  4. 履歴項目D'のonRedo()

今度は、履歴項目の登録順の通りであることに注意して下さい。こちらについても、実行順序はこの通りに保証されます。

操作をアンドゥできるようにする際は、操作の中から呼び出している別の操作がアンドゥ可能である場合、上記の実行順の事を念頭に置いてアンドゥ・リドゥ用の処理を記述する必要があります。例えば上記の例であれば、アンドゥの際は以下の順でアンドゥのための処理が進みます。

  1. 3つ目のタブを開く操作のアンドゥ(タブが閉じられる)
  2. 2つ目のタブを開く操作のアンドゥ(タブが閉じられる)
  3. 1つ目のタブを開く操作のアンドゥ(タブが閉じられる)
  4. 複数のタブを開く操作のアンドゥ

これを見ると、4番目の項目の時点ではもう何もする必要が無いということが分かります。もし4の時点で、3つのタブを開いたのでそのアンドゥ操作としてタブを3つ閉じようとしても、閉じる対象のタブが存在しないためエラーになってしまいます。他のアンドゥ可能な操作を内部で呼び出す操作については、アンドゥ・リドゥの内容が重複しないように注意してください。

汎用的なアンドゥ・リドゥ機能を提供するoperationHistory.jsの基本的な使い方 - Jan 11, 2010

Undo Tab Operationsの核であるoperationHistory.jsは、タブに限らずいろんな操作に対してアンドゥ・リドゥを実装しやすくするための汎用のライブラリとして設計しています(一応)。こいつの使い方を、自分の頭の中の整理も兼ねて少しずつ解説していこうと思います。


  • 最新版はCOZMIX NGにあります。
  • 名前は「UI Operations History Manager」としています。長いので、以下「operationHistory」とだけ呼んでおきます。
  • ライセンスはMITライセンスとします。
  • Firefox 2以降でないと動きません。

読み込み方

まず、読み込みの方法。以下のようにJavaScriptのファイルを読み込ませるだけでOKです。

<script src="lib/operationHistory.js"
        type="application/javascript"/>

複数のアドオンでこのライブラリを読み込んでいる場合、最もリビジョンの新しい物が使われます。今後はAPIは変えないor後方互換を維持していくつもりなので、使う方はあんまり気にしないで使えるはずです。

operationHistoryの各機能には window['piro.sakura.ne.jp'].operationHistory でアクセスできます。以下の説明ではサンプルコードを短くするために、 OH という変数でこれを参照しているものとします。

var OH = window['piro.sakura.ne.jp']
               .operationHistory;

処理をやり直し可能にする

operationHistoryを使って任意の処理をアンドゥ・リドゥ可能にしたい時は、その処理を以下のように実行するようにします。

OH.doOperation(function() {
  // 任意の処理
});

doOperation()の引数に関数を渡すと、それがその場で実行されます。実行時のthisOH自身を指していますが、普通に分かりにくいんで、クロージャを使うなり何なりして好きなように書くといいと思います。

var MyAddon = {
  myFeature : function() {
    var self = this;
    OH.doOperation(function() {
      self.myInternalMethod();
    });
  },
  myInternalMethod : function() {
    // 何かの処理
  }
};

で、これだけだとまだアンドゥ・リドゥはできません。doOperation()に対して以下の引数をさらに指定してやる必要があります。

  • 履歴の名前(文字列、省略可能)
    • "MyAddonOperations" とかそんな感じで好きに名前を付けて下さい。省略すると、履歴の対象のウィンドウが指定されている場合は "global" 、そうでなければ "window" になります。
    • Undo Tab Operationsでは "TabbarOperations" という名前を指定してます。
  • 履歴の対象になるウィンドウ(nsIDOMChromeWindow、省略可能)
    • 拡張機能の名前空間で普通に見える所のwindowです。省略すると、ウィンドウ単位の履歴ではなく、クロスウィンドウな単一の履歴となります。が、動作が怪しいので今の所はウィンドウ単位での使い方だけ推奨しておきます。
  • 履歴エントリ(オブジェクト、必須)
    • オブジェクトリテラル、カスタムクラスなど、何でもOKです。詳しくは後述。

やり直し可能にしたい処理自体と合わせると、doOperation()は最大で4つまでの引数を取るという事ですね。引数は全部型が違う(関数、文字列、DOMWindow、オブジェクト)ので、doOperation()はそれらを受け取った後に、どれがどれなのかを自動的に判別します。なので引数はどの順番で指定しても構いません。

実際のアンドゥ・リドゥ処理を書く(関数を使うやり方)

実際のアンドゥ・リドゥ処理は、履歴エントリになるオブジェクトのプロパティとして関数で定義します。

var entry = {
  // 内部名
  name   : "undotab-addTab",
  // メニュー等に表示する「やり直す処理の名前」
  label  : "タブを開く",
  // アンドゥ時に実行される内容
  onUndo : function(aParams) {
    gBrowser.removeTab(this.tab);
  },
  // リドゥ時に実行される内容
  onRedo : function(aParams) {
    this.tab = gBrowser.addTab();
  },
  // 以下、任意のプロパティを好きなようにどうぞ
  tab : null
};

OH.doOperation(
  function() { // やり直し可能にする処理
    entry.tab = gBrowser.addTab();
  },
  'MyAddonOperations', // 履歴名
  window,              // 処理対象ウィンドウ
  entry                // 履歴エントリ
);

onUndoという名前のプロパティで関数を定義しておくとアンドゥ時にそれが実行されます。onRedoはリドゥの時に実行されます。どちらもthisは履歴エントリのオブジェクト自身になりますので、まあ見た通りで分かりやすいんじゃないかと思います。クロージャ使って書いても全然構いません。

namelabelは、文字列で好きなように名前を付けて下さい。定義しなくても使えますが、デバッグの時にはあると便利ですし、DOMイベントを使った記法(後で解説します)の時には無いと困ります。

アンドゥする・リドゥする

上記のようにして履歴に登録した処理は、以下のようにしてアンドゥ・リドゥできます。

// アンドゥ
OH.undo('MyAddonOperation', window);
// リドゥ
OH.redo('MyAddonOperation', window);

undo()redo()の引数には、doOperation()に対して指定したものと同じ履歴名と処理対象のウィンドウを渡します(こちらも引数の指定順は任意です)。ここではまだ「タブを開く操作」しか書いていませんが、同じ履歴名で「タブを閉じる操作」「タブを移動する操作」などに対してそれぞれアンドゥ・リドゥの処理を書いてやれば、線形にそれらをアンドゥ・リドゥできるようになります。

また、「戻る」「進む」のドロップダウンメニューで項目を指定してそこまで一気に飛ぶのと同じように、goToIndex()で履歴項目のインデックスを指定してそこまで一気にアンドゥする・リドゥする事もできます。

// 現在の位置を得る
var history = OH.getHistory('MyAddonOperation', window);
var current = history.index;
OH.goToIndex(current-3, 'MyAddonOperation', window);

getHistory()は、登録済みの履歴項目の全エントリを格納したオブジェクトを取得するメソッドです。それで取得したオブジェクトのindexプロパティで現在のフォーカス位置を得られるので、上の例ではそこから3つ手前に飛ぶ事になります。

要素やウィンドウに固有のIDを使う

ここまでの説明で既に疑問に思った人もいると思いますが、例えばこんな場合。

function NewTab() {
  var entry = {
    name   : "undotab-addTab",
    label  : "タブを開く",
    onUndo : function(aParams) {
      gBrowser.removeTab(this.tab);
    },
    onRedo : function(aParams) {
      this.tab = gBrowser.addTab();
      gBrowser.selectedTab = this.tab;
    },
    tab : null
  };
  OH.doOperation(
    function() {
      entry.tab = gBrowser.addTab();
      gBrowser.selectedTab = entry.tab;
    },
    'MyAddonOperations',
    window,
    entry
  );
}

function MoveTab(aTab) {
  var entry = {
    name   : "undotab-moveTab",
    label  : "タブを移動する",
    onUndo : function(aParams) {
      gBrowser.moveTabTo(this.tab, this.oldPosition);
    },
    onRedo : function(aParams) {
      gBrowser.moveTabTo(this.tab, this.newPosition);
    },
    tab : null
  };
  OH.doOperation(
    function() {
      entry.tab = aTab;
      entry.oldPosition = aTab._tPos;
      gBrowser.moveTabTo(aTab, 3);
      entry.newPosition = aTab._tPos;
    },
    'MyAddonOperations',
    window,
    entry
  );
}

NewTab()を実行→それで開かれたタブに対してMoveTab()を実行→アンドゥ→アンドゥ→リドゥ→リドゥ という順に操作すると、2つ目の履歴項目のリドゥ時にタブが見つからないせいでエラーになってしまいます。こうならないように、処理対象の要素は固有のIDなどで識別してやらないといけません。

operationHistoryにはそのために、要素に対して一意なIDを自動的に付与してそれを元に要素を検索する仕組みがあります。先の例を安全に書くと、以下のようになります。

function NewTab() {
  var entry = {
    name   : "undotab-addTab",
    label  : "タブを開く",
    onUndo : function(aParams) {
      var tab = OH.getElementById(this.tab,
                   gBrowser.mTabContainer);
      gBrowser.removeTab(tab);
    },
    onRedo : function(aParams) {
      var tab = gBrowser.addTab();
      OH.setElementId(tab, this.tab)
      gBrowser.selectedTab = tab;
    },
    tab : null
  };
  OH.doOperation(
    function() {
      var tab = gBrowser.addTab();
      entry.tab = OH.getElementId(tab);
      gBrowser.selectedTab = tab;
    },
    'MyAddonOperations',
    window,
    entry
  );
}

function MoveTab(aTab) {
  var entry = {
    name   : "undotab-moveTab",
    label  : "タブを移動する",
    onUndo : function(aParams) {
      var tab = OH.getElementById(this.tab,
                   gBrowser.mTabContainer);
      gBrowser.moveTabTo(tab, this.oldPosition);
    },
    onRedo : function(aParams) {
      var tab = OH.getElementById(this.tab,
                   gBrowser.mTabContainer);
      gBrowser.moveTabTo(tab, this.newPosition);
    },
    tab : null
  };
  OH.doOperation(
    function() {
      entry.tab = OH.getElementId(aTab);
      entry.oldPosition = aTab._tPos;
      gBrowser.moveTabTo(aTab, 3);
      entry.newPosition = aTab._tPos;
    },
    'MyAddonOperations',
    window,
    entry
  );
}

getElementId()は、要素に一意なIDが付いていなければ新しいIDを生成て設定した上でそのIDを、既にIDが付いていればその値を、文字列として返します。IDは普通のid属性ではなく別の属性名で保存されるので、通常の動作を破壊することはありません。

getElementById()は、そのID文字列をキーとして要素を検索するメソッドです。tabbrowserの場合はタブなどの内部の要素は普通のdocument.getElementById()等では取得できないのですが、getElementById()はID名の文字列以外に要素ノードを渡すと、その要素の子孫だけを検索するようになります。

ここでは、タブを開く操作のリドゥにおいてsetElementId()も使用しています。これは、既に生成されたID文字列を新しく復元された要素に付与することで、その要素を元の要素の代わりとして参照できるようにするためです。


とりあえず、まずはこの辺だけ解説しておきます。

→続き

汎用アンドゥ・リドゥのための仕組みがだいたいできてきたと思う - Jan 11, 2010

Undo Tab Operationsの開発を通じて実装をこねくり回してた汎用のアンドゥ・リドゥ用のライブラリだけど、最低限必要そうな一通りの機能を実装し終えた……と思う。

最初はもっとコンパクトになるかなと思ってたんだけど、なんだかんだで膨らんで35KBちょいになった(2010年1月11日現在)。ライブラリの使い方は……Undo Tab Operationsのソース読んで実際の使われ方を見た方が話が早いかも。


以下、ライブラリの使い方の説明じゃなくてただの苦労話です。

当初は、単純に以下のようにしようと思ってた。

  • 1つの履歴項目に1つの操作が対応する。
  • アンドゥ用・リドゥ用の処理を関数オブジェクトとして指定する。

しかし実際やってみるまでもなく、これだと考慮しないといけないケースがあまりに多くなりすぎる。例えばタブを開く操作だけでも、「タブバー上のボタン」「ブックマーク」「リンク」等々色々ある。それら1つ1つに対してアンドゥ・リドゥの処理を定義していくのはさすがに無理がある。また、例えば「タブバーのボタンで新しいタブを開く処理に対応する関数」に対してアンドゥ・リドゥの処理を定義したとして、その関数が他の処理の中から呼ばれないという保証はどこにもないわけで、アンドゥ用の処理がかぶったら、タブが2つも3つも開き直されたり、その逆に2つも3つも閉じられたりしかねない。これは危険すぎる。

という事くらいはすぐに思いついたので、次にこんな風に考えた。

  • gBrowser.addTab()gBrowser.moveTabTo()などの基本的な関数それぞれに対してアンドゥ・リドゥの処理を定義する。
  • gBrowser.loadOneTab()(内部でaddTab()moveTabTo()を呼んでいる)のような関数の存在も考慮して、1つの「やり直し可能な処理の単位」の中で行われたやり直し可能な処理はすべて、トップレベルの履歴項目に子供としてぶら下げる。
    • アンドゥ・リドゥの際は、それらすべてを実行する。
  • ただし、いくつかの処理については例外的に、それより下位の項目の実行をキャンセルして自前で処理するようにする。
    • 例えばgBrowser.swapBrowsersAndCloseOther()は場合によっては元のウィンドウを閉じてしまうため、ウィンドウを開き直す→ロード完了を待ってからタブを開き直す、という風な事をしないといけない。ウィンドウが開かれるまでの間に下位の項目(タブを開く、タブを閉じる等)のアンドゥ処理が走るとおかしな事になる。なので、下位の項目のアンドゥ処理はキャンセルして、swapBrowsersAndCloseOther()のアンドゥ処理の中で全部完結させるようにする。

例外を設けたのは、「前の履歴項目のアンドゥ処理の完了を待ってから次の履歴項目のアンドゥ処理を始める」という風な、非同期処理を考慮した仕組みを当初備えていなかったせい。なんでその仕組みを先に作らなかったのかというと、作るのがめんどかったからの一言に尽きる。

で、このような仕様で開発を進めて、Undo Tab Operationsについてはとりあえず素のFirefox上でならまともに使えるようになってきたかなあと思ったので、マルチプルタブハンドラとの連携に着手し始めた。そしたら破綻した。

例えばマルチプルタブハンドラは、タブ1つだけのドラッグ&ドロップでの移動を検知して選択されたタブ全部をその近くに移動するようになってるけど、これをアンドゥ可能にしようと思うと、そのアンドゥの処理が始まる前にUndo Tab Operationsによって行われるmoveTabTo()のアンドゥでタブの並び順が変わってしまうので、最終的なタブの並び順がグチャグチャになってしまう。手っ取り早く解決しようと思うと、Undo Tab Operationsによって行われるタブの移動のアンドゥ処理は全部キャンセルして、マルチプルタブハンドラ側で面倒を見てやった方が、書くのは簡単なわけです。

しかし、そんなことをそれぞれのアドオンがやり始めたら、絶対にどっかで考慮漏れが起こるわけですよ。Undo Tab Operationsとマルチプルタブハンドラだけだったら問題が起こらなくても、そこにツリー型タブや他のアドオンが加わってくると、互いにアンドゥ処理の優先権の取り合いになってしまうのは目に見えてる。

そういう未来が予想できてしまったので、ついに観念して、非同期処理に真面目に対応することにした。で、結局以下のようになった。

  • addTab()removeTab()など、それぞれの基本的な関数に対してアンドゥ・リドゥの処理を提供する。
  • アンドゥ処理には処理待ち機能を提供し、個々の段階の処理が走る時には必ず、前の段階の処理が終わっている事を保証する。

あとついでに、関数オブジェクトをそのまま履歴項目に使うようにすると履歴項目が増える度にクロージャが増えていってメモリリークの温床になりそうだなあと思ったので、DOMのイベントを監視することでも同じ事ができるようにAPIを整備した。Undo Tab Operations 0.2.2010011001以降ではこっちの方法を使ってそれぞれのアンドゥ・リドゥ処理を実装してある。

処理待ちにはJSDeferredを使ってもよかったんだけど、他のライブラリには依存させたくなかったので、JavaScript 1.7以降のジェネレータ・イテレータと継続関数で実現してみた。イベントオブジェクトのaEvent.wait()で処理待ち状態になって、aEvent.continue()で処理の完了を通知するというスタイルにしてある。

クロージャを使っても使わなくても、処理対象のタブを特定するには一意なIDが無いとどうにもならないなあと思ったので、汎用の「要素ノードに自動生成でIDを付与する」とかの機能もライブラリに含めることにした。これを使うことで、閉じられたタブに対応する開き直されたタブを確実に取得できるようになってる。

実際使ってみるとなかなか妙な感じですね。タブをウィンドウ外にドロップしてタブを切り離し→Shift-Ctrl-Zで元に戻す→Shit-Ctrl-Yでまた切り離す なんてことができて、キモくて面白いです。

Firefox上のあらゆるタブ関連の操作を一次元で記録してアンドゥ・リドゥ可能にするアドオン(実験段階) - Jan 07, 2010

こないだから作ってる汎用のアンドゥ・リドゥ用ライブラリを使って、タブ関係の操作をなんでもアンドゥできるようにするアドオンを作ってみた。名前はそのままUndo Tab Operationsです。

ライブラリ自体が実験段階だし、そのライブラリの使い方も(自分で作っておきながら)まだよく把握しきれてないので、練習がてらという感じです。動作は全体的に非常に怪しいです。あと、これを作る上で「あーこういう機能が必要だよな」と躓く度にライブラリの方に手を入れているので、一体何が目的だったのかもうワケが分かりません。

そう。目的。そもそもは「ツリー型タブでウィンドウをまたいだタブの移動を取り消せるようにしたい」的な所が出発点だったはずなんですよね。なのにどうしてこんなに遠回りをしているのか。富豪的にも程がありますよね。

まあ、「リンクからタブを開く時に今のタブのすぐ右にタブを開きたい、ただし連続してリンクを開く時は開いた順に整列させたい」という目的を達成するためだけに、すべてのタブの親子関係を完全に保持する、なんていうめちゃめちゃ無駄なことに力を注ぐような僕のやる事ですからねぇ。

とりあえずこのアドオンで一通りの基本的なところをカバーして、次に親子関係が絡まない単純な応用という事でマルチプルタブハンドラを、最後にツリー型タブを対応させるという感じで作業を進めていこうかなー。と。思ってますけどどうなるかは分かりません。現段階でものすんごいバグバグなので、ひょっとしたら全部お蔵入りになるかもね。

Page 11/248: « 7 8 9 10 11 12 13 14 15 »

Powered by blosxom 2.0 + starter kit
Home

カテゴリ一覧

過去の記事

1999.2~2005.8

最近のコメント

最近のつぶやき