無駄と文化

実用的ブログ

いまさらまとめるブックマークレットの作り方 〜 2016年版 〜

これまでプライベート・業務の両面で個人的にブックマークレットを作っては利用してきました。
我ながらだいぶ小慣れてきた感じがあります。

このブログでもブックマークレット関連のネタは書いてきましたが、ブックマークレットそのものの作り方に関してまとめていなかったので、あらためて書いてみます。


そもそもブックマークレットって何やー

シンプルに言うと「ブックマークに登録しておいて、自分の好きなタイミングで実行できるJavaScriptのコード」です。


f:id:todays_mitsui:20160229031614j:plain

実例として、
WordPress は文章を引用して素早く記事を作成するための Press This というブックマークレットが標準で付いてきます。


ブックマークレットでできること

長らく草案だった HTML5 がついに勧告され、近年では ECMAScript 2015(ES2015, ES6) がモダンブラウザに着々と実装されてきています。
この状況を見ても、これから先、ブラウザと JavaScript だけで出来る事は増えていくでしょう。

ブックマークレットは、あらかじめブックマークに登録しておいたJavaScriptを実行するだけというシンプルなものですが、応用次第でこれから使い方の幅は広がっていくと思います。


実用例を具体的に挙げてみると、

  • ajax で外部サーバーと通信
  • canvas で画像を動的に生成
  • File API でファイルを動的に生成
  • (iPhoneなどで) 加速度センサーやコンパスからの情報を表示

と、HTML5 の機能を積極的に利用することでかなりいろいろなことが出来そうです。


ブックマークレットの作り方

さて、ここから本編です。
やりたいことに合わせて、いくつかのテンプレートを紹介しようと思います。


基本編

まずは基本、いちばんシンプルなパターンから。

実行したい処理をコードに落とさなければ始まりませんので、

void((function(undefined) {
  /* ここに処理の本体を記述 */
})());

こんな感じに。

コードが書けたら、ブックマークレットとして動作させるために /packer/ などで1行に圧縮して、

f:id:todays_mitsui:20160229022244p:plain


圧縮したら、先頭に javascript: を付け足して、ブックマークに登録すればオッケィです。

f:id:todays_mitsui:20160229023217p:plain


テンプレートについて補足しておきますね。

  1. 元々のページのグローバルを汚染しないように処理全体を無名関数の即時実行でラップしています。

  2. さらに実行結果を void() 演算子に渡しています。
    実行された JavaScript が何らかの値を返してしまうと、それに応じてページ遷移してしまいます。それを抑制するのが目的です。
    void() 演算子に渡された式がどんな値を返そうとも、 void() 演算子自体は確実に undefined を返してくれます。

  3. グローバルの undefined が何らかの値で上書きされている場合を考えて、念のために仮引数を使って undefinedundefined で上書きすることもやっています。


/* ここに処理の本体を記述 */ となっている部分には思い思いの処理を記述してもらえればと思います。
どんなに長くなっても(モダンブラウザであれば)大丈夫です。ブラウザが提供している全ての標準API を使うことができます。

ただし、
特定のフレームワークやライブラリ(例えば jQuery とか)を前提としたコードを書くべきではありません。
ブックマークレットを使おうとしたページに偶然にも jQuery オブジェクトが読み込まれていればそれが利用できますが、全てのページに jQuery オブジェクトが存在していることを期待してはいけません。


jQuery などの外部ライブラリを使いたい場合には次の節で解説するやり方を使ってください。

なお、コードを1行に圧縮する工程と、javascript:を付け足してブックマークに登録する工程は全ての場合で共通なので、これ以降は省略します。


jQuery などのライブラリを使用する場合

近年は脱 jQuery の風潮もありますが、小規模なコードで手っ取り早く目的を達成するなら、なんやかんやで jQuery 便利ですよね。

そんなわけで、外部からjQueryを読み込んで使用したい場合は以下のようにしてください。

void((function(f){
  // <script> タグを生成
  var script = document.createElement('script');

  // cdnjs(https://cdnjs.com) から jQuery 2.2.1 を読み込む
  // プロトコルは省略してドメイン相対パスで
  script.src = '//cdnjs.cloudflare.com/ajax/libs/jquery/2.2.1/jquery.min.js';

  // jQuery の読み込みが完了してから本体の処理が実行されるように
  // onload イベントを設定
  script.onload = function(){
    // 既存サイトに存在するかもしれない $ を汚染しないように
    // jQuery.noConflict(true) を実行しつつ $ を取得
    var $ = jQuery.noConflict(true);

    // $ を渡しつつ本体の処理を実行
    f($);
  };

  // <body> 直下に生成した <script> を差し込む
  document.body.appendChild(script);
})(function($, undefined){
  // バージョン確認
  ;;; console.log('jQuery: ', $().jquery);

  /* ここに処理の本体を記述 */
}));

無名関数を二重にして使っているので、無名関数に慣れていない人は戸惑うかも知れません。

前半で cdnjs から jQuery を読み込んで jQuery オブジェクトを取得しています。
それを後半の無名関数に渡すことで、無名関数の中では $ を自由に使えるという仕組みです。


もちろん jQuery じゃなくて Lodash(Underscore.js) が使いたいんだよ、という場合もほぼ同じ記述で実現できます。

void((function(f){
  var script = document.createElement('script');

  // cdnjs(https://cdnjs.com) から Lodash 4.5.1 を読み込む
  // プロトコルは省略してドメイン相対パスで
  script.src = '//cdnjs.cloudflare.com/ajax/libs/lodash.js/4.5.1/lodash.min.js';

  script.onload = function(){
    // 既存サイトに存在するかもしれない _ を汚染しないように
    // _.noConflict() を実行しつつ _ を取得
    var _ = _.noConflict();

    // _ を渡しつつ本体の処理を実行
    f(_);
  };

  document.body.appendChild(script);
})(function(_, undefined){
  // バージョン確認
  ;;; console.log('Lodash: ', _.VERSION);

  /* ここに処理の本体を記述 */
}));

このように。


jQuery などのライブラリを再利用する場合

CDN から読みこめばあらゆるライブラリを自由に使うことが出来るわけですが、せっかちな人であれば読み込みによる僅かなタイムラグが気になるかも知れません。

実際、jQuery であれば結構な割合のサイトで利用されてますからね。
なので、すでに jQuery オブジェクトが存在しているページでは CDN からの読み込みを省略するようにしてみましょう。

void((function(f){
  // jQueryの存在チェックとバージョンチェック
  if(window.jQuery && jQuery().jquery > '1.8') {
    // jQueryが存在していればそれをそのまま使う
    f(jQuery);
  }else{
    // 存在しない場合は cdnjs(https://cdnjs.com) から読み込み

    var script = document.createElement('script');

    script.src = '//cdnjs.cloudflare.com/ajax/libs/jquery/2.2.1/jquery.min.js';
    script.onload = function(){
      var $ = jQuery.noConflict(true);
      f($);
    };

    document.body.appendChild(script);
  }
})(function($, undefined){
  // バージョン確認
  ;;; console.log('jQuery: ', $().jquery);

  /* ここに処理の本体を記述 */
}));

はい、
jQuery オブジェクトの存在チェックをして条件分岐させています。
未だに v1.6 とかの古い jQuery を使っているサイトもまぁまぁあるのでバージョンチェックもしておいたほうがいいでしょう。


複数のライブラリを使用する場合

ここまでは、何か一つのライブラリを読み込んで使用したい場合について解説してきました。

では、複数のライブラリを使って処理したくなったらどうでしょう。
読み込むスクリプトが一つなら、onload イベントの中で本体処理を呼べばいいんですが、複数となると厄介ですね。

...仕方ない、Promise を使いましょう。


jQuery と Lodash と Moment.js を同時に使う例がこちら、

void((function(f){
  // jQuery 読み込み用の Promise オブジェクト
  var jQueryPromise = new Promise(function(resolve, reject) {
    var script = document.createElement('script');
    script.src = '//cdnjs.cloudflare.com/ajax/libs/jquery/2.2.1/jquery.min.js';

    // 読み込み成功時
    script.onload = function() { resolve(jQuery.noConflict(true)); };

    // 読み込み失敗時
    script.onerror = function() { reject(); };

    document.body.appendChild(script);
  });

  // Lodash 読み込み用の Promise オブジェクト
  var lodashPromise = new Promise(function(resolve, reject) {
    var script = document.createElement('script');
    script.src = '//cdnjs.cloudflare.com/ajax/libs/lodash.js/4.5.1/lodash.min.js';

    // 読み込み成功時
    script.onload = function() { resolve(_.noConflict()); };

    // 読み込み失敗時
    script.onerror = function() { reject(); };

    document.body.appendChild(script);
  });

  // Moment.js 読み込み用の Promise オブジェクト
  var momentPromise = new Promise(function(resolve, reject) {
    var script = document.createElement('script');
    script.src = '//cdnjs.cloudflare.com/ajax/libs/moment.js/2.11.2/moment.min.js';

    // 読み込み成功時
    script.onload = function() { resolve(moment); };

    // 読み込み失敗時
    script.onerror = function() { reject(); };

    document.body.appendChild(script);
  });

  // 3つのスクリプトが全て正常に読み込まれた後の処理を設定
  Promise.all([jQueryPromise, lodashPromise, momentPromise])
    .then(function(libs) {
      // Function.prototype.apply() を使って配列を展開しつつ本体処理に渡す
      f.apply(window, libs);
    })
    .catch(function(err) {
      // 読み込みに失敗したときは何かエラー処理を
      console.error(err);
    });
})(function($, _, moment, undefined){
  // バージョン確認
  ;;; console.log('jQuery: ', $().jquery);
  ;;; console.log('Lodash: ', _.VERSION);
  ;;; console.log('Moment.js: ', moment.version);

  /* ここに処理の本体を記述 */
}));

それぞれのスクリプトの読み込みを Promise オブジェクトにして実行。
Promise.all() で全てのスクリプトが読み込まれた後のコールバックとして本体の処理を呼び出しています。

確かに問題なく動くんですが、冗長で醜いコードですね。


複数のライブラリを使用する場合 (Bundle版)

Promise を使った例でなんとなくツラさを感じ取っていただけたかなと思います。

結論から言うと、複数のライブラリを使ってある程度以上の規模のコードを実行する場合には、必要なライブラリを全てまとめた JS ファイルを作っておいてそれを読み込ませる方が早いです。

ライブラリを npm などで管理して、Browserifywebpackrollup.js などで JS をパックしてから使うのが、開発環境的にもモダンなんじゃないでしょうか。


Browserify を使用して、先ほどと同じく jQuery, Lodash, Moment.js を使う処理を記述するとこんな感じになります。

var $ = require('jquery');
var _ = require('lodash');
var moment = require('moment');

// バージョン確認
;;; console.log('jQuery: ', $().jquery);
;;; console.log('Lodash: ', _.VERSION);
;;; console.log('Moment.js: ', moment.version);

/* ここに処理の本体を記述 */

めちゃくちゃシンプルになりましたね。 これを main.js とかの名前で保存して、

$ browserify main.js -o bundle.js

bundel して...

f:id:todays_mitsui:20160229024416p:plain

Web上の適当な場所に置いて、
※ 上記の例では Dropbox の Public ディレクトリに入れました。GitHub Page に置くのもいいかも知れませんね。

で、ブックマークレットの方は bundle.js を読み込むだけのものになります。

void((function(undefined) {
  var script = document.createElement('script');

  // 先ほど作った bundle.js を読み込み
  script.src = '//path.to/bundle.js';

  // <body> 直下に生成した <script> を差し込む
  document.body.appendChild(script);
})());


正直、開発の手順を考えると、このやり方がいちばん楽ですね。
bundle.js を Web 上に永続的に保持しておく必要はありますが...。


まとめ

というわけで、5つのパターンに分けてブックマークレット用のテンプレートをご紹介しました。

普段、JS はバリバリ書いてるけどブックマークレットは作ったことがないという方は、このテンプレートに自慢のコードをぶち込んでいい感じのブックマークレットを作ってみてください。


私からは以上です。