Hatena::ブログ(Diary)

ザリガニが見ていた...。 このページをアンテナに追加 RSSフィード

2014-07-17

ブックマークレットの書き方の段階的な発展の仕方

ブックマークレットとは、JavaScriptコードが保存されたブックマークのことである。クリックすると、そこに保存されているJavaScriptコードが実行される。

  • 通常のブックマークのアドレスには、http:で始まるURLが保存されている。
  • ブックマークレットのアドレスには、javascript:で始まるJavaScriptコードが保存されている。

はじめの一歩

WebブラウザのURLフィールドに以下のように入力して、returnキーを押してみると...

javascript:alert("hello world!!")

f:id:zariganitosh:20140715164844p:image:w450


アラートダイアログが表示された!

f:id:zariganitosh:20140715165106p:image:w428


URLフィールドのアイコンをブックマークバーにドラッグ&ドロップすると、そのJavaScriptはブックマークレットとして保存される。

f:id:zariganitosh:20140715172852p:image:w450


あるいは、aタグのリンク先にJavaScriptを設定しておけば、そのリンクをドラッグ&ドロップしてもブックマークレットとして保存される。

<a href="javascript:alert('hello world!!')">helloアラート</a>

helloアラート ←このリンクをドラッグ&ドロップするのだ。但し...

      • 残念ながら、はてなダイアリーではaタグのリンク先にJavaScriptを設定することが許可されていない。
      • 公開する時に以下のタグに変換されてしまう。よって、上記リンクはブックマークレットにならない...。
<a target="_blank" href="">helloアラート</a>

とにかく、ブラウザのブックマークのアドレス項目に、javascript:で始まるJavaScriptコードを書いておけば、そのコードは実行されるのだ。

変数のスコープ問題

  • ブックマークレットは便利なんだけど、今までの書き方では問題が発生することがある。
  • 例えば、クリックするごとにカウンターを+1する以下のようなページを想像してみる。
<!DOCTYPE html>
<html lang="ja">

<head>
<meta charset="utf-8">
<title>JavaScriptサンプル</title>
<script type="text/javascript">
var counter = 0;
</script>
</head>

<body>
<p id="counter_text">0</p>
<a href="javascript:counter++; document.getElementById('counter_text').innerText = counter;">カウントアップ</a> 
</body>

</html>
  • 上記ページ(count_up.html)を表示すると、カウントアップをクリックする度に、1、2...と順にクリックした回数が表示される。

f:id:zariganitosh:20140716112126p:image:w450


  • では、このページで以下のようなブックマークレットを実行すると、どうなるか?
javascript:var counter=100;alert(counter);

  • 上記ブックマークレット実行後にカウントアップをクリックすると、いきなり101になってしまう...。

f:id:zariganitosh:20140716112616p:image:w450


  • この原因は、count_up.htmlページとブックマークレットでcounter変数を共用する状態になっている所にある。
    • この状況で、counterはグローバル変数として扱われている。
    • グローバル変数は、そのページで実行されるJavaScriptコードなら、どこからでも参照できる。
    • ブックマークレットも、表示されているページに属するJavaScriptコードとして実行される。

グローバル変数とローカル変数

  • JavaScriptの変数には、たった二つのスコープしかない。
    • グローバル変数=そのページのJavaScriptコードのどこからでも参照可能
    • ローカル変数=変数宣言された関数の中でだけ参照可能
  • ローカル変数の作り方
    • 関数の中でvar宣言した変数
    • 関数の仮引数
  • グローバル変数の作り方
    • 関数の外でvar宣言した変数
    • varなし宣言した変数

例:

var out_total;
function sum_alert(a, b){
  var total = a + b;
  global_total = total;
  out_total = total;
  alert("合計: "+total);
}
sum_alert(1, 2);
alert("global合計"+global_total);
alert("out合計"+out_total);
alert(total); //ReferenceError: Can't find variable: total (Line: NaN)
alert(a); //ReferenceError: Can't find variable: a (Line: NaN)
alert(b); //ReferenceError: Can't find variable: b (Line: NaN)
ローカル変数グローバル変数
関数の中でvar宣言した変数=total関数の外でvar宣言した変数=out_total
関数の仮引数=a, bvarなし宣言した変数 global_total

変数を汚染しないブックマークレット

以上のことから...

  • ブックマークレットで使う変数は、すべてローカル変数にすべきなのだ。
  • つまり、すべてのブックマークレットは関数定義の中に書く必要がある。
  • 先ほどのカウンターアラートのブックマークレットも、
javascript:var counter=100;alert(counter);
  • 関数の中に書いておけば変数を汚染しないはずである。
javascript:function wrap(){var counter=100;alert(counter);} wrap();
  • wrap()という名の関数で包み、直後にwrap()関数を実行している。
  • これでカウントアップするカウンター値に影響を与えなくなった!

ところが、まだ完璧ではない...。

  • もしも元ページのJavaScriptコードにwrap関数が定義されていたら、上記ブックマークレット実行後は動作がおかしくなってしまう。
  • それを避けるためには絶対に重複しない関数名にしておく必要があるのだが、関数名がある限り、絶対に重複しないとは言い切れなくなる。
    • 但し、zariganitosh_bookmarklet_wrapのような関数名にしておけば、実用上問題ないレベルで重複しないはずなのだが。
    • しかし、このような長い関数名は無駄だし、固有名詞を入れたくないし、コードも美しくない...。
  • 関数名があるから重複するわけで、ならば関数名を付けなければいい。そう、無名関数を使うのだ。
javascript:(function(){var counter=100;alert(counter);})()
  • 上記のようにwrapを取り除いて、(関数定義全体)を括弧で囲い、その直後にも()を付ける。
  • すると、その関数は、定義された直後に、実行されるのだ。

イディオムとしては、以下のような書式になる。

javascript:(  function(){...コード...}  )()

( function(){...コード...} )()で何が起こっているのか?

  • 括弧の連打に惑わされて、何が起こっているのか見失いそうになるが、じっくり眺めてみる。
  • JavaScriptには、関数定義の方法が複数用意されている。
  • その一つは、先ほどのfunctionで始まるwrap関数の定義である。
javascript:function wrap(){var counter=100;alert(counter);}

  • もう一つの方法は、無名関数を変数に代入する方法もある。
javascript:wrap = function(){var counter=100;alert(counter);};

  • コードが評価されるタイミングなど若干の違いはあるが、最終的にどちらもグローバルなwrap変数に関数が代入されるという意味では同じ。
  • wrap変数の内容を表示してみると、同じであることが理解できる。
関数定義のブロック
  • コード行頭(;で区切られた先頭)がfunctionで始まると、関数定義のブロックと見なされ、}の後に;は不要。
javascript:function wrap(){var counter=100;alert(counter);} alert(wrap)

f:id:zariganitosh:20140716150553p:image:w428


  • また、関数定義のブロックは、実行に先立ってコード全体が解釈される時に評価される。
  • その証拠に、alert(wrap); を先に書いても、wrapには関数が代入されている。
javascript:alert(wrap); function wrap(){var counter=100;alert(counter);}
関数定義を返す関数
  • 一方、式の途中にfunctionがあると、式の一部と見なされ、}の後に;が必要。
javascript:wrap = function(){var counter=100;alert(counter);}; alert(wrap)

f:id:zariganitosh:20140716150551p:image:w428


  • 式の途中のfunctionは、コード実行中に関数定義を返す関数として実行される。
  • function関数が関数オブジェクトを生成して返し、それを変数wrapに代入しているのである。
javascript:alert(wrap); wrap = function(){var counter=100;alert(counter);};

  • どちらも変数wrapにも、関数定義のコードそのものが代入されている。
    • 但し、関数定義のブロックとしてのfunctionは、事前に評価されるので、コード実行中には何も返さない。
    • 一方、関数定義を返す関数としてのfunctionは、コード実行中に関数オブジェクトを返す。

  • では、関数定義のブロックなのか、関数定義を返す関数なのか、その違いはどこで決まるのだろう?
  • それには、シンプルなルールがある。
    • 行頭がfunctionで始まれば、関数定義のブロック。
    • 行頭がfunctiion以外で始まり、途中にfunctionがあると、関数定義を返す関数。

  • ここでやっと( function(){...コード...} )()の意味を考えてみる。
  • 先頭が(で始まるので、このfunctionは関数定義を返す関数である。
  • よって、( function(){...コード...} )の部分は関数オブジェクトと評価される。
  • 関数オブジェクトに()を付けると、それは関数呼び出しとして実行される。

つまり、無名関数の定義と実行をすることになるのだ!


  • ちなみに、行頭がfunctiion以外で始まればOKなので、()で囲う以外にもいくつかの方法がある。
javascript:+function(){var counter=100;alert(counter);}()
javascript:-function(){var counter=100;alert(counter);}()
javascript:void function(){var counter=100;alert(counter);}()
  • 矛盾のない式と評価されるなら、どのような書き方でもOK。
  • でも、一般的によく使われるのは ( function(){...コード...} )() である。

特に理由がない限り ( function(){...コード...} )() を使っておいた方が良さそう。

長い長いブックマークレット問題

ここまで、独立したブックマークレット実行環境を手に入れられた。

  • 40370文字のブックマークレットは、SafariのブックマークとしてはiCloud経由で同期できないのだけども...
  • OSXのSafariのブックマークにはちゃんと登録できる。実行すると、正常に動作して、ログイン情報を自動入力してくれた。
  • 同様にiOSのSafariのブックマークの編集ページでJavaScriptコードをペーストすると、ちゃんと登録できた。動作も正常。
    • 但し、メモ.appから40370文字のコードをコピーして、iOSのSafariのブックマークにペーストすると、ペーストが完了するまで5分もかかった。
    • さらに、ペースト完了後にSafariのDoneボタンを押して、ブックマークへの保存が完了するまで、やはり5分もかかってしまった。
    • 合わせて10分も待つ必要がある。その間、iPhoneの画面の輝度が低下したら、すかさずタッチしてスリープしないように維持しなければならない。

面倒くさ過ぎる!

  • やはり、ブックマークは勝手に自動で同期して欲しい。
  • では何文字までのブックマークなら自動同期するのか?

調べてみると、3913文字(バイト)までだった。

  • 3913文字(バイト)を1文字でもオーバーすると、自分のiCloud環境ではSafariのブックマークの同期が止まる...。
    • あり得ない不便さである。
    • 以前は3万文字程度のブックマークレットもちゃんと同期していたと思っていたのだが、いつから3913文字になってしまったのだろう?
  • しかし、嘆いていてもしょうがない。この現実に対応するしかない...。

外部ファイルをロードするブックマークレット

  • 最初はログイン情報を圧縮することを考えたが、40370文字のブックマークレットを3913文字に収めるのはどう考えても無理。
    • そもそも、ログイン情報を全部削除しても10000文字程度のサイズであった。

  • 残る手段は、本体のJavaScriptコードを外部ファイルに保存しておくしかない。
  • ブックマークレットに保存するコードは、その外部ファイルをロードするだけ。

  • 例えば、DropboxのPublicフォルダにbookmarklet.jsというファイルを作っておく。
    • 外部ファイルはブックマークレットではないので、javascript:は不要になる。
    • Publicフォルダが存在しない場合は、以下のページから有効にできる。
    • Public フォルダの用途は。 - Dropbox
(function(){var counter=100;alert(counter);})()

  • すかさず、bookmarklet.jsを二本指で選択して、公開リンクをコピーしておく。
    • XXXXXXXの部分は、自分のDropboxのユーザー番号に置き換えるのだ。
https://dl.dropboxusercontent.com/u/XXXXXXX/bookmarklet.js

  • 上記bookmarklet.jsをロードするブックマークレットは、以下のように書ける。
javascript:(function(){var s=document.createElement('script');s.src='//dl.dropboxusercontent.com/u/XXXXXXX/bookmarklet.js';document.body.appendChild(s);})()
  • さらに、仮引数を利用することで、より短く、よりシンプルに洗練させてみる。(あまり変わらない?)
javascript:(function(d,s){s=d.createElement('script');s.src='//dl.dropboxusercontent.com/u/XXXXXXX/bookmarklet.js';d.body.appendChild(s);})(document)

  • やっていることは、Webページのbodyタグの中に、以下のscriptタグを追加しているだけ。
<script src='//dl.dropboxusercontent.com/u/XXXXXXX/bookmarklet.js'></script>
  • 上記scriptタグが追加されると、srcのURLにあるファイルがロードされ、実行されるのだ!
    • URLのhttpあるいはhttpsを省略しておくと、ブラウザ側で良きに計らい使い分けてくれる。
    • ロードするブックマークレット、外部ファイルともにfunctionでラッピングしているので、既存のJavaScriptには影響を与えないはず。(と思っている)
  • ブックマークレット自体は、たった149文字のコードである。iCloud経由で素早く同期されるはず。

これで、長い長いブックマークレット問題も解決!

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/zariganitosh/20140717/bookmarklet_coding
リンク元