Google Chrome拡張の概要とECMA-262 5th Edition
Shogo Ohta, 2010-4-232010/3/25に
Google Chrome OS 最新技術と戦略を完全ガイド: 小池良次, 中島聡, 伊藤千光, 太田昌吾, まえだひさこ, 向井領治が発売!
Software Design 2010年5月号にChrome拡張の特集25ページが掲載。
Google Chromeの拡張機能について、APIの最新情報や開発のノウハウなどを日本語で共有するGoogle 準公式のコミュニティ
2009年12月にできたばかり。是非ご参加ください。
オープンソースなので、すべてのソースが読める。カスタムしてビルドを作れる。
オープンソースコミュニティのパワーで、日々開発が進んでいる。また、Chromiumのカスタム版が数多く存在する。
今すぐ開発に参加できる chromium - Project Hosting on Google Codeへ!
stable(安定版)、beta(テスト版)、dev(開発版)の3つのリリースがある
安定版は名前の通り、一般ユーザーが使用するリリース、(セキュリティフィックスを除いて)半年に1回程度のリリース
テスト版は、主に安定版のリリース前に、安定化のためにリリースされる、時期によってはまったくリリースされないが、1ヶ月に1回程度のリリース
開発版は、安定版のリリース後、次のバージョンに向けた新機能追加を主目的(そのため不安定になることも多々)にリリースされる、1週間に1回程度のリリース
※開発版もリリース前は安定する。修正が多く適用されている分だけテスト版より安定することも。
JavaScriptエンジンはV8というChromeのために開発したオープンソースのエンジンを搭載
レンダリングエンジンはWebKitのWebCoreを採用
WebCoreは、WebKitのレンダリング周りの実装(JavaScript部分はJavaScriptCore)
V8とWebCoreを繋ぐ部分にバグが潜みやすい…(Safariでは正常なのに、Chromeだとなんかおかしくなるなぁという現象はここに起因することが多い)
最近はChromiumチームが積極的にWebKitにコミットするようになっていて、かなり改善しつつある
HTML/CSS/JavaScriptで作る、ブラウザをもっと便利にするモノ
(2010年末に登場予定の)Chrome OSではアプリケーションの役割を果たす?
とにかく作るのは簡単、でも制約は多い
実物を見てもらうのが一番
公式の拡張ギャラリーには2010年4月9日時点で4000以上の拡張が登録されています。
Google Mail Checker - Google Chrome 拡張機能ギャラリー
高機能版はGoogle Mail Checker Plus - Google Chrome 拡張機能ギャラリー(最近使い始めましたが便利です)
Hatena Bookmark GoogleChrome extension - Google Chrome 拡張機能ギャラリー(はてな公式のChrome拡張、こちらも便利です)
Chrome拡張開発者のリーダー的存在のAaron Boodman氏は元々FirefoxのAdd-onであるGreasemonkeyの創始者
Chrome拡張もGreasemonkeyのコンセプトを引継ぎつつ、開発者のおもちゃ的なGreasemonkeyをより多くのユーザーに安全に使ってもらうために作られた
FirefoxのAdd-onの縮小版ではなく、Greasemonkeyの改良版といえる
APIの詳細は公式ドキュメントとその非公式日本語訳(@nao58)へ
APIはまだまだ少ないが、バックグラウンド処理、クロスドメイン通信、データの永続化をベースに、可能性は無限
Chrome5では履歴を扱うAPIが追加される(chrome.history)
実験段階のAPI(experimental.* APIs)もあります
拡張コンテキスト、コンテントコンテキスト、ページコンテキストの3つのコンテキストが存在し、それぞれは完全に分かれているので、お互いが干渉してしまうことはない。さらに、拡張同士も独立したコンテキストで実行される。
拡張コンテキストはタブ操作やクロスドメイン通信などの特権を実行でき、コンテントコンテキストと通信したり、スクリプトを実行したりといったことができます。
コンテントコンテキスト(Content Scripts)は特権を持っていませんが、読み込んだページのDOMを操作することができ、拡張コンテキストと相互に通信できます。
ページコンテキストは通常のウェブページで実行されるコンテキストで、コンテントコンテキストとはDOM経由でしかやり取りできませんし、拡張コンテキストとは完全に分断されています
「悪意のあるページで拡張の特権を利用されてしまう」といったことが起こりにくい仕様になっているが、開発者から見ると面倒に感じる部分も…
拡張同士はお互いに独立しており、特にBackground Pageのプロセスは完全に分かれている
拡張同士で連携するには、chrome.extension.sendRequestなどのAPIを使用する必要がある
拡張はそれぞれIDを持っており、拡張機能のページでデベロッパー モードにすると確認できる。また、公式ギャラリーのURLにもIDがそのまま使われている。
このIDはドメインに相当するもので、CookieやlocalStorageなどはこのIDに関連付けて保存される。また、拡張コンテキスト上でlocation.hostをみるとやはりIDが使われている。
なお、拡張のスキームはchrome-extension://である。IDと組み合わせて、例えば chrome-extension://dnlfpnhinnjdgmjfpccajboogcjocdla 通常のウェブページと同様で、ドメインが違えばクロスドメインの制約を受けるので、拡張間でのXMLHttpRequestなどはできない。
拡張を定義するjsonファイル。名前、バージョンのほか、アイコンや拡張の持つ権限、ContentScriptsを実行するURLの定義など。
manifest.jsonで宣言していない機能は使用できない。(URLのほか、tabs/bookmarksに、Chrome5からはnotifications/historyとexperimentalを指定できます)
セキュリティを高めつつ、機械的にその拡張の権限を判断して、インストールするユーザーに警告を出すことができる。
拡張を作るにあたって必要なものは特にありません。あえて挙げるなら、アイコンは用意しておきましょう。
まず、manifest.jsonを用意します。
このmanifest.jsonだけで拡張として読み込むことができます。もちろん、何の機能もありません。
browser_actionとiconsを追加しました。popupには以下のhtmlを用意しました。
chrome://extensions/ で「拡張機能のパッケージ化」からパッケージを作成できます。
Constellation's crxmake at master - GitHubでCUI環境でもパッケージングができます。
出来上がったcrxファイルをChromeにドロップすると、インストールダイアログが現れて、拡張としてインストールできます。インストールした拡張は(Windowsの場合)以下のパスに保存されています。
インストールした拡張はIDが開発時と変わってしまいます。その代わりにインストール後のmanifest.jsonには"key"というフィールドが追加されています。このkeyがあるとパッケージ化されていない拡張を読み込んだ際もパッケージ化した拡張のIDと同じものが使用できます。
なお、.crxファイルはzip形式なので、解凍することができます。
Web Inspectorを使用する(参考:続・先取り! Google Chrome Extensions:第6回 Firebug要らずなChromeのWeb Inspector|gihyo.jp … 技術評論社)
ChromeはGreasemonkey Scriptを拡張に変換してインストールする機能を備えています。
twitter.AutoPager for Greasemonkey
ただし、このGreasemonkey対応はContentScriptsとして実行されるだけなので、クロスドメイン通信などに対応していません(Chrome6での対応予定はある)
GM_xmlhttpRequest、GM_getValue/GM_setValue、GM_registerMenuCommandなどのAPI、@requireなどに未対応
そして、開発者にとって重大な問題と言えるのが、ソースを手軽に編集できないという点です
インストールしたソースを探し出し、編集して再起動か、一度無効化して有効にするという手順で編集を反映させることはできますが、Greasemonkeyの編集してリロードすれば反映されるという手軽さには到底及びません
それなら、Chrome拡張でGreasemonkeyを実装してみればいいじゃないかと思い、やってみました
Background Pageにスクリプトを持ち、表示したURLに対応するスクリプトを管理・実行させる
ContentScriptsからBackground PageにsendRequestで通信、Backgroundでクロスドメイン通信、結果をContentScriptに返却
Backgroundページと通信して…だと非同期になってしまうので、APIのインターフェースが変わってしまう
実行前にGM_getValue、GM_setValue用のデータオブジェクトを用意し、参照・保存はそのオブジェクトに対して行い、保存の際は裏でBackground Pageに投げておく
Bespinなども考えたが、独自実装にしました。 現状の成果物はGreased Chromeにあります ChromeのJavaScriptエンジンV8はWebKitのJavaScriptCoreと協調して、ECMAScript5の実装を積極的に進めています。
なお、__defineGetter__、__defineSetter__はECMAScript5に含まれていない
オブジェクトのプロパティを配列で取得する
prototypeを指定して新しいオブジェクトを作るメソッドです
ECMAScript5ではオブジェクトのプロパティに対して、writable、enumerable、configurableという属性が追加されました。
writableはそのプロパティの値を書き換え可能であるか、enumerableはそのプロパティがfor inループで列挙されるかどうか、configurableはこれらの属性を書き換え可能であるかをそれぞれ真偽値で設定します。また、value、writableの代わりにget、setでAccessorとして定義することも可能です。
via ECMAScript 5: The Definitive Slides
これらは2010年4月23日時点でまだ実装されていません。特にStrict modeなどは実際に実装されてみないと正しくstrictな記述ができているか保証できないので、今のうちからuse strictを使うのは避けたほうが良いでしょう。
JavaScriptにStrictモードが追加されました。ただし、文法的なStrictではなく、セマンティックなStrictです(文法的な事前チェック(lint)ができるのではなく、実行時に例外を投げる)。
ソースコード中に"use strict";という何もしない文字列リテラル(将来的にはクォートはなくなる予定)を置くことで、Strictモードになります。
エンジンが独自に"use ~~";な命令を実装してもOK
strictモードはあくまで定義したスコープ内だけで有効であり、ライブラリとして呼び出された関数がその影響を受けることはない
最新の情報は、Web Platform Status (The Chromium Projects)にまとまっています
わざとらしい例ですが、パーサーがsectionに対応しているか否かで、下記のように解釈が変わります。
元々、Chrome拡張のAPIの仕様だったDesktop Notificationsが、2010年1月にW3C Editor's Draftとして(Web Notifications)公開されました。
このように、Chrome拡張は各種APIがHTML5をベースとしており、同時に、Chrome拡張からHTML5にフィードバックする、という相互作用の関係があります
HTML5を使いたい!という方は、是非Chrome拡張を!!
この資料自体がHTML5のサンプルとなるよう作ってあります。
manifest.json
{
"name": "simple note",
"description":"簡易メモ帳です",
"browser_action": {
"default_icon": "pad19.png",
"default_title": "simple note",
"popup": "popup.html"
},
"icons": {
"128": "pad128.png",
"48": "pad48.png"
},
"permissions": ["tabs" ,"http://*/", "https://*/"],
"version": "0.0.1"
}Chrome拡張の作り方
{
"name": "Hello",
"description": "Chrome拡張入門",
"version": "0.1"
}{
"name": "Hello World",
"description": "Chrome拡張入門",
"version": "0.1",
"browser_action": {
"default_icon": "icon.png",
"popup": "popup.html"
},
"icons": {
"48": "icon.png"
}
}<!doctype html>
<html>
<style>
*{
margin: 0px;
padding: 0px;
}
body > h1{
width:300px;
text-align:center;
background: -webkit-gradient(
linear, left top, left bottom, from(#06c), to(#39f)
);
border-radius:4px;
margin:0px;
padding:1em;
color:white;
text-shadow: 3px 3px 3px black;
line-height:1.4;
}
</style>
<h1>Chrome extensions</h1>
<script>
document.querySelector('h1').onclick=function(){
this.innerText = 'Chrome拡張入門';
}
</script>
</html>パッケージ化
//XP
"C:\Documents and Settings\[USERNAME]\Local Settings\Application Data\Google\Chrome\User Data\Default\Extensions"
//7
"C:\Users\[USERNAME]\AppData\Local\Google\Chrome\User Data\Default\Extensions"拡張のデバッグ
yet another greasemonkey
Greasemonkey Scriptへの対応
Greasemonkey on Chrome Extension
Greasemonkeyを実装するうえでの課題と解決策
chrome.extension.onRequest.addListener(function(request, sender, sendResponse){
if (request.href) {
// Content Scripts
function GM_xmlhttpRequest(opt){
var _opt = {
type:'GM_xmlhttpRequest',
url:opt.url,
method:opt.method,
headers:opt.headers,
overrideMimeType:opt.overrideMimeType,
data:opt.data,
isOnload:!!opt.onload,
isOnerror:!!opt.onerror,
isOnreadystatechange:!!opt.onreadystatechange
};
chrome.extension.sendRequest(_opt, function(res){
if (res.state === 'load') {
opt.onload(res.response);
} else if (res.state === 'error') {
opt.onerror(res.response);
} else if (res.state === 'readystatechange') {
opt.onreadystatechange(res.response);
}
});
}
// Background
chrome.extension.onRequest.addListener(function(request, sender, sendResponse){
if (request.type === 'GM_xmlhttpRequest') {
var xhr = new XMLHttpRequest();
var res = {
status:0,
statusText:'',
readyState:0,
responseText:'',
responseHeaders:'',
}
xhr.open(request.method, request.url, false);
if (request.isOnload) {
xhr.onload = function(){
Object.keys(res).forEach(function(k){
res[k] = xhr[k];
});
sendResponse({response:res, state:'load'});
};
}
if (request.isOnerror) {
xhr.onerror = function(){
Object.keys(res).forEach(function(k){
res[k] = xhr[k];
});
sendResponse({response:res, state:'error'});
};
}
if (request.isOnreadystatechange) {
xhr.onreadystatechange = function(){
Object.keys(res).forEach(function(k){
res[k] = xhr[k];
});
sendResponse({response:res, state:'readystatechange'});
};
}
xhr.send(request.data);
}
});
function GM_getValue(key, default_value){
if (__gm_value_store[key]) {
return __gm_value_store[key];
} else {
return default_value;
}
}
function GM_setValue(key, value){
__gm_value_store[key] = value;
var opt = {
type:'GM_setValue',
value:__gm_value_store,
namespace:SCRIPT_NAME_SPACE
};
chrome.extension.sendRequest(opt);
}
if (request.type === 'GM_setValue') {
localStorage[request.namespace] = JSON.stringify(request.value);
}
エディタの問題
Chrome拡張で使えるECMAScript5
ChromeのECMAScript5対応
Chrome4で使用できるECMAScript5
var data = {
_i : 0,
max : 100,
min : 0,
get index() {return this._i},
set index(i) {
if (i < this.min){
this._i = this.min;
} else if (i > this.max){
this._i = this.max;
} else {
this._i = i;
}
}
};
document.__defineGetter__('innerHTML',function(){
return document.documentElement.outerHTML;
});
document.__defineSetter__('innerHTML',function(html){
document.documentElement.outerHTML = html;
});
var data = {
a:1,
b:[1,2],
c:new Date(),
f:function(a){return a;}
};
var json_text = JSON.stringify(data);
console.log(json_text);
//{"a":1,"b":[1,2],"c":"2010-01-18T14:01:36Z"}
var json_text2 = JSON.stringify(data,null,2);
console.log(json_text2);
/*
{
"a": 1,
"b": [
1,
2
],
"c": "2010-01-18T14:01:36Z"
}
*/
if (!Object.keys){
Object.keys = function(object){
if (!(object instanceof Object)) {
throw new TypeError('Object.keys called on non-object')
}
var keys = [];
for (var key in object){
if(object.hasOwnProperty(key)){
keys.push(key);
}
}
return keys;
}
}
Chrome5で使用できるECMAScript5
var nullObject = Object.create(null);
console.log(Object.getPrototypeOf(nullObject)); // null
var object = Object.create(Object.prototype); // var object = {};
console.dir(Object.getPrototypeOf(object));
/*
Object:
__defineGetter__: function __defineGetter__() { [native code] }
__defineSetter__: function __defineSetter__() { [native code] }
__lookupGetter__: function __lookupGetter__() { [native code] }
__lookupSetter__: function __lookupSetter__() { [native code] }
constructor: function Object() { [native code] }
hasOwnProperty: function hasOwnProperty() { [native code] }
isPrototypeOf: function isPrototypeOf() { [native code] }
propertyIsEnumerable: function propertyIsEnumerable() { [native code] }
toLocaleString: function toLocaleString() { [native code] }
toString: function toString() { [native code] }
valueOf: function valueOf() { [native code] }
*/
object = Object.create(Object.prototype,{
v:{
value:1,
writable:false,
enumerable:false,
configurable:false
}
});
object.v = 10;
console.log(object.v); // 1
var data = Object.create(Object.prototype,{
_i : {
value: 0,
writable: true,
enumerable:false,
configurable:false
},
max : {
value: 100,
writable: true,
enumerable:false,
configurable:true
},
min : {
value: 0,
writable: true,
enumerable:false,
configurable:true
},
index: {
enumerable:true,
configurable:true,
get: function() {return this._i},
set: function(i) {
if (i < this.min){
this._i = this.min;
} else if (i > this.max){
this._i = this.max;
} else {
this._i = i;
}
}
}
});
console.dir(data);
/*
Object
_i: 0
index: 0
max: 100
min: 0
__proto__: Object
*/
console.log(Object.keys(data));
/*
["index"]
*/
data.index=10;
console.log(data.index); // 10
data.index=101;
console.log(data.index); // 100
if (Object.defineProperty && !Object.prototype.toSource){
Object.defineProperty(Object.prototype, 'toSource', {
value : function(indent) {
return uneval(this,indent);
},
writable : true,
enumerable : false,
configurable : false
});
}
function uneval(obj,indent){// FIXME
var r = [];
function _uneval(obj, _indent, __indent){
var $indent = '';
if (typeof _indent === 'string') {
$indent = '\n' + _indent;
} else if (isFinite(_indent)) {
$indent = '\n' + new Array(_indent+1).join(' ');
}
if (typeof __indent === 'string') {
__indent = '\n' + __indent;
} else if (isFinite(__indent)) {
__indent = '\n' + new Array(__indent+1).join(' ');
}
__indent || (__indent = '');
var type = typeof obj;
if (type === 'object' && obj){
if (Array.isArray(obj)) {
return '[' + $indent + obj.map(function(v){
return _uneval(v, _indent + indent);
}).join(','+$indent) + __indent + ']';
} else {
return '{' + $indent + Object.keys(obj).map(function(k){
return '"' + k + '":' + _uneval(obj[k], _indent+indent, _indent);
}).join(',' + $indent) + __indent +'}';
}
} else if (type === 'function') {
return obj.toString();
} else if (type === 'undefined') {
return void 0;
} else {
return JSON.stringify(obj,null,indent);
}
}
return '(' + _uneval(obj,indent) + ')';
}
これ以外のECMAScript5での変更点
Strict mode
(function(){
"use strict";
function strictFunc(){
i = 0;// error
}
try {
strictFunc();
} catch(e) {
}
notStrict();
})();
function notStrict(){
i = 1;
}
Strictモードは以下の特徴を持っています
(function(){
"use strict";
eval('var hoge={}');
typeof hoge// 'undefined'
})();
(function (x) {
x = 2;
alert(arguments[0]);
})(1);
function A(a,a){
a;//2
arguments[0] // 1
arguments[1] // 2
}
A(1,2);
//Strictモードかどうかをチェックする方法
var strict = (function() {
return !this;
}());
// Strictモードをサポートしているかチェック
var hasstrict = (function() {
"use strict";
return !this;
}());
その他のECMAScript5での変更点
Chrome拡張で使えるHTML5
ChromeのHTML5対応:関連API
ChromeのHTML5対応:HTMLパーサー
<p>第1パラグラフ
<section>セクション</section>
<p>第2パラグラフ
Chrome Firefox section要素未対応 
Chrome4
Firefox3.6section要素対応 
Chrome5
Firefox3.6(html5.enable)Chrome拡張からHTML5へ
Appendix
Chrome Extensionsの参考URL
ECMAScript5関連の参考URL
この資料についてのお話
(X)HTML5 validation results for http://ss-o.net/chrome/extension/introduction.html
Outline

This work is in the Public Domain.