ブラウザー勉強会でWebKitの拡張について話してきました

ブラウザー勉強会というのは、hebikuzureさん主催のウェブブラウザについての勉強会です。「ウェブブラウザについて」と一口にいっても割と範囲が広くて、特に今日の参加者を見ても、ウェブブラウザを実装してる人から、拡張を作ってる人、ウェブサイトを作っている人、企業の社内インフラ管理をしている人、そしてウェブブラウザの利用者とかとか、多種多様なんですよね。そういった色んな立場のウェブブラウザに強い関心のある方が集まった面白い勉強会でした。
で、WebKitの拡張(WebKitの拡張というのは微妙な表現なんだけど、Chromeの拡張とSafariの拡張といちいち書くのも面倒なので…。他に良い名称があれば教えてください…。)についてお話してきたんですが、折角のウェブブラウザ勉強会のなのでWebKitの裏話的なものを取り上げてみました*1
というわけで、発表資料です。
WebKit拡張教室 at ブラウザー勉強会
例によってHTML Slideをベースに、gimiteさんとpaqさんによるタッチ対応のForkを取り入れて少し改良を加えたものになっています。あとでjsdo.itにフィードバックしようと思います。

そういえば、「CSSのベンダープリフィクスについて、WebKitは-webkitが知られていますが、実は-khtmlもしくは-appleと書いても(今は)OKだったり」というネタはあんまり知られてなさそうなので取り上げてみたんですが、やっぱり全然知られてないみたいです。Safari2以前は-khtmlが普通だったみたいです。
-khtml本の虫: QuirksBlog: HTML5のドラッグ&ドロップはクソだで衝撃を受けて、この前のW3C Widgetsのイベントのときに-apple-dashboard-regionをOperaがサポートしているのにも衝撃を受け、そこでふと-khtmlもプリフィクスなんだーと気が付きました。まあ、今となってはプリフィクスじゃなかったらなんなんだという感じなんですが…。
ちなみに、JavaScriptからも各プリフィクスに対応しています。

document.body.style.KhtmlOpacity=.4
document.body.style.opacity // '0.4'
document.body.style.KhtmlOpacity === document.body.style.opacity // true

あ、資料にも書きましたが-khtml、-appleのサポートは切られる予定なので、今後は(今までも使ってる人はそうそういないと思うけど)使わないように注意しましょう。

*1:id:shinichiro_hさんの[http://d.hatena.ne.jp/shinichiro_h/20100109#1263025326:title=WebKitネタ]を参考にしてたんですが、そのshinichiro_hさんが本人が発表後に登場するという個人的サプライズも

NinjaKit for Safari

NinjaKitのSafari版が一応できました。
NinjaKit - 0xFFからどうぞ。
注意点などはChrome版と同じです。ただ、GM_registerMenuCommand未対応なのと、オプションページを呼び出す方法がツールバーのボタンしかありません。このあたり、Safari版は右クリックメニューにしてみようかなと考えています(というか、今のところそれくらいしか候補がないので)。
とりあえず、手前ですがos0x's scripts - Userscripts.orgは動くと思います。

NinjaKit

NinjaKit*1というChrome/Safari拡張を公開しました。
Chrome:Chrome Web Store - NinjaKit
Safari:NinjaKit for Safari
Source: os0x/NinjaKit · GitHub
これはFirefoxのアドオンであるGreasemonkey相当の機能を実装することを目指しています。
今のところ、

  • GM_xmlhttpRequest
  • GM_addStyle
  • GM_getValue
  • GM_setValue
  • GM_deleteValue(new in ver 0.7)
  • GM_listValues(new in ver 0.7)
  • GM_log
  • GM_openInTab
  • GM_registerMenuCommand(Safari版は未サポート)
  • Metadata
    • @include
    • @exclude
    • @require
    • @bookmarklet(独自APIbookmarkletとして実行するのでGM APIは使えないが、ページ側のJSに触れる)

あたりをサポートしています。とりあえず、AutoPagerizeが動く(AutoPagerize自体は拡張版が出ているのでそっちを使ったほうが良いですが)のを基準にしました。
大体実用的なレベルになったと思うので、公開しました。が、まだまだ問題は色々とあります。特にセキュリティリスクがないとは言い切れない(Greasemonkey自体の問題でもある)のでそのあたりはご了承を。

今後の予定

  • Safari拡張対応
  • E4X(一番重い…、ヒアドキュメントとして使っているだけのケースはなんとか動くようにしたい)
  • スクリプトの更新通知
  • @resource
  • その他細かいAPIなど

対応が難しそうな問題点

  • スクリプト同士の名前空間が完全には別れていないので、prototypeを拡張したときなどにスクリプト同士のコンフリクトが起きかねない
  • letとかIteratorsとかExpression closuresとか、Mozilla方言は無理そう

*1:思いつきでNinjaに。あとSafariにも対応しているのでKitに。

Safari拡張の自動更新方法

次のようなXMLを拡張子.plistで適当な名前で保存して、サーバーにアップします。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
   <key>Extension Updates</key>
   <array>
     <dict>
       <key>CFBundleIdentifier</key>
       <string>net.os0x.autopatchwork</string>
       <key>Developer Identifier</key>
       <string>LAM47A73AC</string>
       <key>CFBundleVersion</key>
       <string>1.2</string>
       <key>CFBundleShortVersionString</key>
       <string>1.2</string>
       <key>URL</key>
       <string>http://ss-o.net/safari/extension/AutoPatchWork.safariextz</string>
     </dict>
   </array>
</dict>
</plist>

Developer Identifierは開発者自身のID、CFBundleVersionは拡張のバージョン(実際に表示されるほう)、CFBundleShortVersionStringは内部用バージョン(こっそり更新したいとき用?)です。
あとは機能拡張ビルダーでマニフェストをアップデートのところにサーバーに置いたplistファイルのURLを設定すればOKです。

余談:公式のドキュメントにTeam Identifierって謎の用語があって後回しにしてたのだけど、ドキュメントが間違っていたそうだ。Developer Forumsでも確認できた。

Safari拡張の作り方

Safari拡張をいくつか作って大体感覚は掴めたので、ざっくりと拡張の作成手順を解説してみます。
なお、Windows版で作業していますが、Macでもほとんど同じだと思います。

Safari拡張とは

最初に、Safari拡張とはなにか、について。Safari拡張はHTML/CSS/JavaScriptをベースに、ブラウザ側が用意したAPIを使ってブラウザを便利にするモジュールです。通常、JavaScriptだけではクロスドメインの問題など、実現できることに制限がありますが、その点を拡張用に用意されたAPIで補います。そのため、APIが用意されていない部分については対応できないという制限があります。しかし、開発のし易さ、ウェブとの親和性の高さからアイディア次第で便利で強力なツールとなるのがSafari拡張・Chrome拡張です。
なお、現状のAPIは暫定的なもので、ここで紹介するのもあくまで2010年6月10日時点のものです。特に正式にSafari拡張がリリースされるまでは大きな仕様変更があるかもしれないのでご注意を。
ちなみに、Chrome拡張開発者には、Chrome拡張のSafari版、の一言で十分でしょう。APIも大体似ていますし、ベースがWebKitであり、V8とJavaScriptCoreもよく似ている(似せてある)ので、開発の上では(最初は)ほとんど差を感じないと思います。
また、SafariにはGreaseKitというGreasemonkey Scriptsを動かすプラグインがありますが、Safari拡張ではGreasemonkey Scriptsと同等かそれ以上のことを実現できます。
なお、開発にはもちろんWeb Inspectorが大活躍します。もしWeb Inspectorをご存じない場合は続・先取り! Google Chrome Extensions:第6回 Firebug要らずなChromeのWeb Inspector|gihyo.jp … 技術評論社をどうぞ。
公式のドキュメントはLoading…です。Loading…からLoading…Loading…Loading…Loading…Loading…Loading…などのサンプルも公開されています。また、Chrome拡張やGreasemonkeyなどの開発者向けのドキュメントもあります。Loading…

開発手順

機能拡張を有効に

まず、設定→詳細から、「メニューバーに"開発"メニューを表示」にチェックを入れます。その開発メニューに「機能拡張を有効する」*1という選択があるのでこれをチェックします。チェックすると機能拡張ビルダーを起動することができるようになり、この機能拡張ビルダーを使ってSafari拡張を作成します。
f:id:os0x:20100610063449p:image
機能拡張ビルダーを立ち上げると最初は何も表示されていないので、左下の+アイコンをクリックしてください。すると「新規機能拡張…」「機能拡張を追加」という2つのメニューが表示されます。新規の方は名前の通り新たに拡張を作る際に、追加のほうは既存の拡張を開発モードにするときに使用します(Safari拡張でないフォルダを選択できてしまいますが、インストールできないので注意)。
f:id:os0x:20100610063450p:image
さて、「新規機能拡張…」を選択するとファイルを選択するダイアログが出てきます。ここは適当にワークスペースとする場所を決めて、適当なファイル名(その拡張の表示名になりますが、あとで変更可能です)を入力してください。

証明書を取得

ファイルを決めると機能拡張ビルダーにその拡張が表示されます。ここで、証明書がないため…というエラーがでていると思います。この状態ではインストールもパッケージングもできないので、証明書を取得する必要があります(Safari Extension 三分クッキング! - こたにきに詳しく書かれています。)。
というわけで、Safari Developer Programに登録します。Apple IDを持っていない場合、まずはApple IDを取得する必要があります。たくさんの質問に答えてなんとか登録が完了したら、Safari Extension Certificate Utilityで証明書を発行します。Macの場合、まずローカルのキーチェーンアクセス.appで証明書要求用ファイルを作成、その後、Safari Extension Certificate Utilityで証明書(safari_identity.cer)を作成・ダウンロードし、作成したsafari_identity.cerをダブルクリックしてインストールします。Windowsの場合「Windows Safari Extension Certificate Assistant」の案内に従って、テキストファイル(http://devimages.apple.com/safari/files/certreq.txt )を保存、cmdで certreq -new certreq.txt newcsr.pem を実行(certreqコマンドがない場合(Windows7だとデフォルトで入ってる?)はLoox Uと初音ミクで行こう!: Safariの証明書作成でハマった。)。newcsr.pemをアシスタントにアップロード。safari_identity.cerをダウンロードできるので、これをダブルクリックしてインストールすれば完了。割と簡単です。

機能拡張ビルダー

さて、証明書がインストールされると、先程の機能拡張ビルダーの表示が次のようになります。
f:id:os0x:20100610062940p:image
Safari Developer: (LAM47A73AC) と表示されています。このLAM47A73ACは私という開発者に当てられた識別子で、私が作成するすべての拡張はこの開発者IDを含みます。上記画像の下の方にバンドル識別子という名前の"この拡張の識別子"がありますが、このバンドル識別子とIDをあわせたものが、この拡張のドメインになります。上記の拡張の場合、 net.os0x.test2-LAM47A73AC になります。なお拡張のスキームはsafari-extension:です。例えば、AutoPatchWorkをインストールしている場合、safari-extension://net.os0x.autopatchwork-LAM47A73AC/apw_128.png にアクセスするとアイコンを表示できると思います。このようにSafari拡張はその拡張ごとにウェブサイトを持っているかのように動作します(このあたりの動作もChrome拡張とほとんど同じです)。
ビルダーで定義した内容は 拡張名.safariextension フォルダの中にInfo.plistという名前のXMLファイルとして保存されます。これはChrome拡張のmanifest.jsonに当たるファイルです。
さて、この機能拡張ビルダーを使いながら拡張を作っていきます。

“Injecting Styles”

まずは簡単なところでサイトにCSSを適用してみましょう。適当なcssファイルを用意します。名前はなんでも構いません。中身はとりあえずこんな感じにしておきます。

body{
	zoom:0.5;
}

cssファイルを作業フォルダに保存し、機能拡張ビルダーの下の方に「スタイルシート」の項目があります。新規スタイルシートをクリックすると、ボックスが現れ、保存したcssファイルが選択できます。cssファイルを選択したら、その下のホワイトリストの新規URLパターンをクリックして適用するURLを入力します。このURLパターンには*(ワイルドカード)が使えますが、ワイルドカードが使えるのはドメイン部分については//の直後だけという条件があります。このあたりもChrome拡張と同じ仕様です。なので、詳しいマッチパターンはMatch Patterns - Google Chrome(日本語訳:マッチパターン | Chrome Extensions API リファレンス)を見るとわかりやすいと思います。
f:id:os0x:20100610064227p:image
さて、この2つだけではまだCSSを適用できません。もう一つ、機能拡張ビルダーの真ん中ぐらいに「機能拡張Webサイトアクセス」という項目があります。ここでアクセスレベルを設定しないとCSSの他、JavaScriptも適用されません。アクセスレベルはまず「なし」、「一部」、「すべて」の3つから選び、一部の場合はさらにホワイトリストと同じくURLのパターンを入力します。ここで許可したサイトには、CSSJavaScriptを適用できるほか、XMLHttpRequestでクロスドメイン通信をすることも可能になります。そのため安易に「すべて」を選ばない方が良いでしょう。Chrome拡張ではすべてのサイトにアクセス可能な拡張には、ユーザーがインストールする際にその旨を警告します。Safari拡張でも同様の対応が実装されることになると思われます。
さて、アクセスレベルを設定したら、右上のインストールをクリックしてみましょう。ホワイトリストで指定したページにCSSが適用されるはずです。なお、このCSSは新たに開いたページだけでなく、インストール時に表示していたページにも適用されます。(ただ、一度適用されたスタイルはCSSを編集して再度読み込むをクリックしても反映されないことがあります。おそらくバグだと思われます。)

“Injecting Scripts”

続いて、JavaScriptを適用してみましょう。方法はCSSと同じでjsファイルを作業フォルダに保存します。JavaScriptについては、「スクリプトを開始」と「スクリプトを終了」の2つがあります。「スクリプトを開始」はページの読み込みが開始したタイミングで、document.headやdocument.bodyが存在しない状態です。唯一存在するDOMはdocumentElementだけで、document.documentElement.outerHTMLはを返す状態です。このため、DOM操作などは基本的に行えません。その代わり、早めに実行したいアクション(特定ファイルの読み込みをブロックする、読み込み自体を中止する、URLと関連付けたAPIを呼び出しておく)をいち早く実行する際に使用します。「スクリプトを終了」はDOMContentLoaded相当のタイミングで実行されるスクリプトで、ページのDOMが構築されているので、ページの内容に対してアクションを起こすことができます。
なお、このInjecting Scriptsは拡張ごとに独立したコンテキスト(もしくは名前空間)で実行されるので、ある拡張で定義した変数・関数が他の拡張だったり、サイト側などから参照できてしまうことはありません(できません)。このあたりもChrome拡張のContentScriptsと全く同じ動作です。
また、このInjecting Scriptsはグローバル変数としてsafariというオブジェクトを持ち、いくつかのAPIが定義されています。このAPIを使って後で解説するGlobal HTML PageやExtension barと連携することができます。
Injecting Scriptsは表示しているページのDOMにアクセスできますが、逆に言えばDOMの影響を受ける→悪意のあるサイトからScriptを操作される可能性があるということです。Injecting Scriptsと通常のサイト側のScriptはコンテキストが異なるので簡単には乗っ取ることはできませんが、コード次第では安全ではありません。そのため、Injecting Scriptsからは拡張のAPIの利用が大幅に制限されています(クロスドメイン通信やタブの操作などもできませんし、設定を読み取ることもできません)。ただ、Global Pageとは通信が可能なので、Global Pageを経由して各種APIを操作することになります。

“Global HTML Page”

機能拡張グローバルページでは、1つの拡張につき1つだけブラウザの起動中バックグラウンドで動き続けるページを持つことができます。Chrome拡張で言うBackground Pagesそのものです。
1つの拡張に1つだけと保証されているので、メールチェックなどのタスク、大きなデータの処理など、拡張の中心的な機能を担うことに適しています。
では、Injecting ScriptsからURLをGlobal Pageに渡し、URLに応じたデータをInjecting Scriptsに戻す簡単なサンプルコード書いてみます。
Injecting Scripts

safari.self.addEventListener('message',function(evt){
	console.log(evt.message);// Global Pageから受信
},false);
safari.self.tab.dispatchMessage('URL',location.href); // Global Pageに送信

Global Page

safari.application.addEventListener('message',function(evt){
	var data = evt.message;// Injecting Scriptsからのメッセージ
	/*何かしらの処理*/
	// レスポンスを返す
	evt.target.page.dispatchMessage('Response', custom_data);
	// もしくは
	safari.application.activeBrowserWindow.activeTab.page.dispatchMessage('Response',custom_data);
	// ならアクティブなウィンドウのアクティブなタブにメッセージが送信される
	// つまり、受信したページに返すとは限らない
},false);

また、Global Pageでは機能拡張の設定で定義したユーザー設定のデータを読み取ることが可能です。
Global Page

safari.extension.settings.addEventListener('change',function(evt){
	// オプションが変更されたときに呼ばれるイベント
	console.log(evt.key); //変更されたオプションの識別子
	console.log(evt.newValue); //変更後の値
	console.log(evt.oldValue); //変更前の値
},false);
var option1 = safari.extension.settings.getItem('option1');
var secure1 = safari.extension.secureSettings.getItem('secure1');

なお、前述の通り拡張は1つの拡張につき1つのドメインをもっているので、localStorageやWeb SQL Databaseなどを設定データの保存用に使用して、設定用インターフェースをHTMLベースで書く事も可能です(Chrome拡張はこちらの方法)。

パッケージ

さて、ここで一度パッケージしてみましょう。機能拡張ビルダーの右上にパッケージをビルドというボタンがあるのでこれをクリックします。保存場所を聞かれるので適当に選択しましょう。これで、拡張の出来上がりです。将来的にはギャラリーが用意され、そこで配布できるようになる予定ですが、現状は個人のサーバーなどにアップロードして公開することになります。
なお、パッケージすると 表示名.safariextz というファイルが作られます。ちょっと長い拡張子ですね。このファイルはXARという形式で圧縮されたファイルで、Windowsだと7-Zipなどのアーカイバで解凍することができます。

Toolbar Buttons

ツールバー項目について簡単に。機能拡張ビルダーのツールバー項目で「新規ツールバー項目」からツールバーに表示するボタンを作ることができます。このボタンはデフォルトで表示させることもできますが、ユーザーがカスタマイズできるので、必ず表示されるとは限らないので注意が必要です。また、アイコンなので画像が必須です。
ツールバー項目にはいくつか設定がありますが、パレットラベルはツールバーをカスタマイズする際に表示されるボタンの名前です(なのでユーザーにとってわかりやすい名前をつけると良いでしょう)。ツールヒントはマウスを載せた際に表示されるテキストです。イメージはフォルダ内にある画像から選択します。画像は14x14か16x16pxが望ましいそうです。18pxより大きい場合はトリミングされます(あと8bitアルファチャンネルで透過しておけば色んな環境のツールバーになじみやすいとかなんとか)。また、この画像、グレースケールされるみたいです(Windows7でのデフォルトスキンがグレー基調なのでそこで制約されている感じです)。
さて、このボタン、当然ですがクリックされたときなどにアクションを起こすことができます。
Global Page

safari.application.addEventListener("command", function(evt){
	if (evt.command === 'command-name') {
		evt.target.browserWindow.activeTab.page.dispatchMessage('ButtonPush',data);
	}
}, false);
const ID="net.os0x.autopatchwork-LAM47A73AC onoff";
var button;
safari.extension.toolbarItems.some(function(b){
	return b.identifier===ID && (button=b);
});
// someはtrueを返したところで走査を止める配列のメソッド(ECMAScript5で追加)で、
// identifierが一致したところでbutton=bの代入のが行われ、buttonにidentifierが一致した
// toolbarItemが取得できる

// locationからスキームとドメインを取得して絶対パスを作る
// imageに代入できるのは絶対パスのみ
button.image = location.protocol+'//'+location.host+'/icon2.png';
// badgeに数値を代入するとボタンの右上にその数字が表示される(メールの未読件数などを表示できる)
button.badge = 10;
その他

ちょっと長くなってきたのであとは概要の紹介のみに。

  • Extension Bars
    • いわゆるツールバー。HTMLベースで作る。ウィンドウに対して存在するので、同時に複数存在することもある
    • 画面を占有するのであまり使用しない方が良い
  • Contextual Menu Items
    • 右クリックメニュー拡張API。イベントを拾うのはGlobal Page側なので、DOMを見るにはInjecting Scriptsとやり取りする必要がある。
  • Windows and Tabs API
    • Window開いたりタブを開いたり閉じたりするAPI。タブを移動するAPIはない。イベントも現状無い?(タブの選択が変わったことを検知できないので、表示中のタブに応じてツールバーやボタンの表示を変えるのが面倒)
  • Blocking Unwanted Content
    • Injected Scriptから safari.self.addEventListener("beforeload", blockAds, true); みたいなことができる。(Chrome拡張にはない機能)
  • Updating Extensions
  • MIME Typeは application/x-safari-extension みたいです。via A tip for developers | ephemera (Chromeは application/x-chrome-extension Hosting - Google Chrome)
  • Icon
まとめ

繰り返しますがChrome拡張とよく似てます。まあAPIはちょこちょこと使い勝手が違いますが、HTML,CSSの解釈は基本同じ(バージョン間の誤差はある)だし、JavaScriptもほとんど誤差レベルでしか違わないのでSafari拡張とChrome拡張の両方に対応するのはそれほど苦労しないと思いますし、APIの差を埋めるライブラリが出てくるんじゃないかと思います。こうやってHTMLベースな拡張が広まると自然とHTML5を使えるところが増えていくし、拡張から新しいウェブ標準なAPIとして追加されるケースもどんどん出てくるんじゃないかと思っています。個人的にはOperaの拡張サポートに期待したいところです。
サンプルはSafari5の拡張作ってみた - 0xFFに追加していこうと思います。

*1:拡張機能かと思ったら機能拡張でした。日本語としては機能の拡張で自然のような気もしないことはないですが…

Safari5の拡張作ってみた

Chrome拡張をとりあえず3つだけSafari拡張に移植というか、どちらでも動くようにしてみました。以下からインストールできると思いますが、今のところ拡張は頻繁にクラッシュしますし、正式リリースされてないということは色々と問題が残っているということですから、そのあたりをご理解の上、ご利用は自己責任でお願いします。

Safari拡張はChrome拡張とよく似ているので、Chrome拡張の移植はモノによってはものすごく簡単です。TextURLLinkerとか、実質的な差分はhttp://bitbucket.org/os0x/texturllinker/changeset/6552fac2b1afだけです。plistは拡張機能ビルダーが作ってくれるのでそちらも簡単。手間なのではSafari Developer Program - Apple Developerに登録して証明書を取得するあたりかも…。
拡張の作り方は明日書きます。Safari拡張の作り方 - 0xFF

連載第5回と公開している拡張

続・先取り! Google Chrome Extensions:第5回 Chrome ExtensionのAPI#2|gihyo.jp … 技術評論社

今回から新しい拡張を作り始めました。記事中では本当に作りかけですが、今朝なんとか実用できるレベルにしてギャラリーにアップしました。
Chrome Web Store - Start Tile

New Tabページをこんなふうにする拡張です。
f:id:os0x:20091214134028p:image

一応予定としては、左側のタブ一覧でタブを閉じたり移動させたりとか、またブックマークからドラッグ&ドロップしてタブを開いたりとかをできるようにしようと思っています。

あと、個人的にはChrome Web Store - Gestures for Chrome(TM)と、Chrome Web Store - Keyconfigが動くのもポイントです。なぜ動くのかといえば、単にjsファイルを読み込んでいるだけという…。まあ、なにより自分が便利なので。

ちなみに、今アップしている拡張はChrome Extentions by os0xもしくは、Chrome Web Storeの通りです。
後、近いうちにアップしようと思っているのはText URL Linker for Greasemonkeyword highlight for Greasemonkeyの2つです。もともとGreasemonkey互換機能のおかげで使える状態ですが、折角なので機能を足してアップしようと思っています。うん、作ってる数だけならトップクラスな気がします…。