HTML5 × CSS3 × jQueryを真面目に勉強 – #8 jQueryプラグインの作り方について詳しく

jhc_jqplugin
853件のシェア(とっても話題の記事)

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

僕は人の名前を覚えるのが苦手です。それはさておき、jQueryプラグインの作成方法について頻繁に忘れるので、手順をここにまとめておくことにします。コレさえ読めば急にプラグインを大量に作れといった無茶ぶりをされても大丈夫。
多い日も安心♪(ゝω・)vキャピ

はじめに - jQuery プラグインの構成

細かい差はあれど、基本的にjQueryプラグインは以下のような構成で成り立っています。

// 匿名関数で全体をラップ - (5)
(function($) {
  // このプラグインの名前 - (1)
  $.fn.name_space = function() {

    //要素を退避 - (2)
    var elements = this;

    // 要素をひとつずつ処理 - (3)
    elements.each(function() {
      // 具体的な処理をここに記述
    });

    // method chain用に要素を返す - (4)
    return this;
  };

}) (jQuery);
  1. $.fnオブジェクトをプラグイン名(※カスタムjQueryメソッド)で拡張する
  2. thisには指定された要素が格納されており、これを変数に退避しておく
  3. 指定された要素全てに処理が適用されるように、each()を使って一つずつに同様の処理を行う
  4. method chainが途切れないように、最後にreturn thisを書く
  5. 全体を匿名関数でラップする

ではサンプルを作りながら一つ一つを見ていくとします。

jQueryプラグインの第一歩

まずは最小規模なものからはじめます。

jquery.myplugin.js

// このプラグインの名前
$.fn.myplugin = function() {

  //要素を退避
  var elements = this;

  // 要素をひとつずつ処理
  elements.each(function() {
    $('body').append('<div>Welcome to the ' + this.innerHTML + ' world!</div>');
  });

  // method chain用に要素を返す
  return this;
};

jQueryプラグインは、カスタムjQueryメソッドとしてjQueryを拡張したものになります。定義の仕方は、$.fnオブジェクトをプラグインの名前(カスタムメソッド名)で拡張します。

thisには指定された要素が格納されています。これを変数に退避しておきます。※必須というわけではありません。していないプラグインも沢山あります。

jQueryオブジェクトは要素が複数あることが前提となっています。そのためelementsにも複数の要素が含まれていると考え、それらに対して全て同様に処理しなくてはなりません。ということでeach()を使ってelementsをループに掛けて、要素を一つずつ処理してきます。

jQueryにはMethod chainという、処理を連鎖させるというアーキテクチャに基づいています。

Method chainの例

$('div')          // div要素を取得
  .hide()         // 取得したdiv要素を隠す
  .text('new context')  // 隠したdiv要素にテキストを'new context'にする
  .addClass('update')   // クラスを追加する
  .show();        // div要素を表示する

このような書き方が成立するのは、hide()やtext()といったメソッドが処理の最後に対象の要素を丸ごと返しているからです。jQueryプラグインでカスタムメソッドを定義する際も、最後に要素を返すことで、処理を連鎖(chain)させることが出来ます。返す方法はカスタムメソッド内の最後にreturn this;を書くだけです。決まり文句として覚えておきましょう。

これで新しいjQueryプラグインが出来ました。使い方はjQueryデフォルトの関数とまったく同じです。以下のように新しいメソッド名を指定すれば実行されます。

Method chainの例

$('div').myplugin();

jQueyプラグインにオプションを渡す

カスタムメソッドにオプションを渡すことで、プラグインの利便性を何倍にも高めることが出来ます。オプションの渡す方法は、optionsオブジェクトを通じてカスタムメソッドに渡すのが一般的です。optionsオブジェクトにすることで、複数のパラメータをまとめて渡すようにします。

プラグインにオプション機能を追加する際は、デフォルト値を定義するのがセオリーです。そのデフォルト値に対してプラグインのユーザーは任意のオプションを渡して(※全てではない)上書きできるようにします。

jquery.shake.js

$.fn.shake = function(options) {

  // 要素を退避
  var elements = this;

  // 渡されたオプションとデフォルトをマージする
  var opts = $.extend({}, $.fn.shake.defaults, options);

  // 要素をひとつずつ処理
  elements.each(function() {
    for (var i=0; i<opts.shakes; i++) {
      $(this).animate({marginLeft: opts.x}, opts.speed)
        .animate({marginLeft: opts.x * -1}, opts.speed);
    }

    // 要素を元に戻す
    $(this).animate({marginLeft: 0}, opts.speed);
  });

  // method chain用に要素を返す
  return this;
};

// shakeプラグインのデフォルトオプション
$.fn.shake.defaults = {
  speed: 'slow',
  shakes: 2,
  x: 10
};

デフォルトオプションは上記のようにして定義します。これをextend()メソッドをつかって、ユーザーが指定したオプションで上書きします。extend()の第一引数に {} (空オブジェクト)を指定すると、デフォルトオプションそのものは上書きされずに、デフォルトオプションとユーザー指定オプションがマージされたものが作られます。

デフォルトオプションを定義することで、プラグインのユーザーは必要なオプションのみを指定することが出来ます。

// オプションを一つだけ指定
$('div').shake({speed: 'fast'});

// オプションを全て指定
$('div').shake({speed: 'fast', shakes: 10, x: 20});

// オプションを指定しない
$('div').shake();

jQueryプラグインを匿名関数でラップする

何気なく使用しているjQueryの$ ショートカットですが、これはjQuery特有のものではなく、prototype.jsといった他のライブラリにおいても普通に使われているものであるため、複数のライブラリを併用していると競合が起きてしまいます。そのため、世に公開されているjQUeryプラグインは一般的に以下のような関数で全体をラップされています。

jquery.shake.jsを匿名関数でラップ

;(function($) {
  $.fn.shake = function(options) {

    // 要素を退避
    var elements = this;

    // 渡されたオプションとデフォルトをマージする
    var opts = $.extend({}, $.fn.shake.defaults, options);

    // 要素をひとつずつ処理
    elements.each(function() {
      for (var i=0; i<opts.shakes; i++) {
        $(this).animate({marginLeft: opts.x}, opts.speed)
          .animate({marginLeft: opts.x * -1}, opts.speed);
      }

      // 要素を元に戻す
      $(this).animate({marginLeft: 0}, opts.speed);
    });

    // method chain用に要素を返す
    return this;
  };

  // shakeプラグインのデフォルトオプション
  $.fn.shake.defaults = {
    speed: 'slow',
    shakes: 2,
    x: 10
  };
}) (jQuery);

このように記述すると、$ショートカットはプラグイン内でのみ有効となり、プラグインの外側ではそちら側の都合で$ショートカットを自由に使うことができるので、互いに競合することがなくなります。また、匿名関数の冒頭にセミコロンをつけると、「大変だ!このプラグインの前に読み込まれたプラグインの末尾にセミコロンが抜けていたせいでJSエラーが起きましたッ!!」といった恥ずかしいミスを防ぐことが出来ます。

もう一つのメリットとして、プラグインを匿名関数でラップするとクロージャが作成されることになります。これによりプラグイン内の変数や関数が適切な名前空間において定義され、他のコードと競合することを防ぐことが出来ます。

jQueryプラグインに内部用(プライベート)関数を追加する

jquery.shake2.js

;(function($) {

  $.fn.shake = function(options) {

    // 要素を退避
    var elements = this;

    // 渡されたオプションとデフォルトをマージする
    var opts = $.extend({}, $.fn.shake.defaults, options);

    // 要素をひとつずつ処理
    elements.each(function() {
      doshake($(this), opts);
    });

    // method chain用に要素を返す
    return this;
  };

  // 内部用関数 - ガクブル実行
  function doshake($obj, opts) {
    for (var i=0; i<opts.shakes; i++) {
      $obj.animate({marginLeft: opts.x}, opts.speed)
        .animate({marginLeft: opts.x * -1}, opts.speed);
    }

    // 要素を元に戻す
    $obj.animate({marginLeft: 0}, opts.speed);
  };

  // shakeプラグインのデフォルトオプション
  $.fn.shake.defaults = {
    speed: 'slow',
    shakes: 2,
    x: 10
  };

}) (jQuery);

一つ前のサンプルで、プラグインは匿名関数でラップされました。これにより匿名関数内では、プライベート関数を普通に定義することが出来ます。当然、外部から直接この関数を呼ぶことは出来ません。パブリックメソッドとプライベートメソッドを分離する事で、プラグインのソースコードを整理することが出来、より精度の高いものに成長させる事が出来ます。

独自データ属性をサポートする

HTML5には、HTMLの名前空間に属さないユーザーオリジナルの属性を定義することが出来る機能が備わっています。これを独自データ属性と呼びます。よく知らない、より詳しく知りたいという方は、こちらのページをご参照ください。

独自データ属性をサポートすると、プラグインのユーザーがオプションを渡す為の方法が一つ増える事になります。また、オプションをHTML要素に持たせる事で、JavaScriptのコードが複雑化するのを軽減する事が出来ます。

jquery.shake3.js

;(function($) {

  $.fn.shake = function(options) {

    // 要素を退避
    var elements = this;

    // 要素をひとつずつ処理
    elements.each(function() {
      // 渡されたオプションおよび独自データ属性をデフォルトにマージする
      var opts = $.extend({}, $.fn.shake.defaults, options, $(this).data());

      doshake($(this), opts);
    });

    // method chain用に要素を返す
    return this;
  };

  // 内部用関数 - ガクブル実行
  function doshake($obj, opts) {
    for (var i=0; i<opts.shakes; i++) {
      $obj.animate({marginLeft: opts.x}, opts.speed)
        .animate({marginLeft: opts.x * -1}, opts.speed);
    }

    // 要素を元に戻す
    $obj.animate({marginLeft: 0}, opts.speed);
  };

  // shakeプラグインのデフォルトオプション
  $.fn.shake.defaults = {
    speed: 'slow',
    shakes: 2,
    x: 10
  };

}) (jQuery);

HTML

<div id="square1" data-shakes="4">
    <h2 id="toc-square1">#square1</h2>
    <span>shake x 4</span>
  </div>
  <div id="square2" data-shakes="10">
    <h2 id="toc-square2">#square2</h2>
    <span>shake x 10</span>
  </div>

jQueryプラグインに静的(static)関数を追加する

jQueryプラグインには静的関数を追加する事が出来ます。これによってプラグインにショートカットメソッドや更なる機能の拡張などといったことが可能となり、より柔軟性を高める事が期待できます。

jquery.shake4.js

;(function($) {

  $.fn.shake = function(options) {
    // jquery.shake3.jsと同じ処理・・・
  };

  // 内部用関数 - ガクブル実行
  function doshake($obj, opts) {
    // jquery.shake3.jsと同じ処理・・・
  };

  // 追加するためのベースを定義
  $.shake = {};

  // 静的関数 1
  $.shake.yurayura = function($obj) {
    var opts = {
      speed: 1000,
      shakes: 10,
      x: 20
    };
    doshake($obj, opts);
  };

  // 静的関数 2
  $.shake.gakuburu = function($obj) {
    var opts = {
      speed: 25,
      shakes: 100,
      x: 10
    };
    doshake($obj, opts);
  };

  // shakeプラグインのデフォルトオプション
  $.fn.shake.defaults = {
    // jquery.shake3.jsと同じ処理・・・
  };

}) (jQuery);

jQueryプラグインの静的関数を呼び出すには、以下のようにします。

$.shake.yurayura($('div'));
$.shake.gakuburu($('div'));

イベント駆動型のjQueryプラグイン

jQueryプラグインの動作を制御する方法として、イベントを利用するというのがあります。プラグインが呼び出されて要素に関数をバインドさせ、それぞれのアクションを必要なときに実行させたいとします。この場合のトリガーとしてイベントを発生させて、それで実行させることが出来れば期待通りの動きが実現できそうです。

そんな訳で、シンプルなスライドショーのプラグインを作ってみます。以下はその要件です。

  1. プラグインはimg要素を受け取り、画像のURL配列をオプションとして受け取る
  2. スライドショーには戻るボタンと進むボタンがあり、これらをクリックすることで表示する画像を切り替える事が出来る
  3. 画像の表示は自動的に切り替わり、循環して表示される

手始めに基本的な枠組みを作ります。

jquery.slideshow.js

(function($) {
  $.fn.slideshow = function(options) {
    // 要素を退避
    var elements = this;

    // 渡されたオプションとデフォルトをマージ
    var opts = $.extend({}, $.fn.slideshow.defaults, options);

    // 要素をひとつずつ処理
    elements.each(function() {
      var $img = $(this);
      var current = 0;

      // ここに必要なロジックを記述・・・
    });

    return this;
  };

  // デフォルトオプション
  $.fn.slideshow.defaults = {
    // ここにデフォルト値を記述・・・
  };
}) (jQuery);

渡したURLの画像を表示させ、さらに戻るボタンと進むボタンで表示される画像を切り替える処理を加えていきます。

(function($) {
  $.fn.slideshow = function(options) {
    // 要素を退避
    var elements = this;

    // 渡されたオプションとデフォルトをマージ
    var opts = $.extend({}, $.fn.slideshow.defaults, options);

    // 要素をひとつずつ処理
    elements.each(function() {
      var $img = $(this);
      var current = 0;

      function show(index) {
        var total = opts.images.length;
        while (index < 0) {
          index += total;
        }
        while (index >= total) {
          index -= total;
        }
        current = index;
        $img.attr('src', opts.images[index]);
      }

      function prev() {
        show(current - 1);
      }

      function next() {
        show(current + 1);
      }

      $img.bind('prev', prev)
        .bind('next', next)
        .bind('goto', function(event, index) {
          show(index);
        }
      );
    });

    return this;
  };

  // デフォルトオプション
  $.fn.slideshow.defaults = {
    // ここにデフォルト値を記述・・・
  };
}) (jQuery);

デフォルトオプションはひとまず置いておくとして、これでボタンクリックで画像を切り替える処理の実装は出来ました。プラグインの呼び出し側は次のように実装します。

eventdriven.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Slideshow sample</title>
<link rel="stylesheet" href="css/eventdriven.css"  />
<script src="http://code.jquery.com/jquery-1.8.2.min.js"></script>
<script src="js/jquery.slideshow.js"></script>
<script>
$(function() {
  var $image = $('#slideshow');

  $image.slideshow({
    images: ['img/jhc_less1.png', 'img/jhc_selector.png', 'img/parallax.png']
  });

  $('#prev').click(function(event) {
    event.preventDefault();
    $image.trigger('prev');
  });

  $('#next').click(function(event) {
    event.preventDefault();
    $image.trigger('next');
  });

  $image.trigger('goto', 0);
});
</script>
</head>
<body>
  <div>
    <h1>Event-driven Plugin</h1>
    <p>jQuery samples</p>
    <ul>
      <li><a id="prev" href="#">戻る</a></li>
      <li><img id="slideshow" /></li>
      <li><a id="next" href="#">進む</a></li>
    </ul>
  </div>
</body>
</html>

ここまでのコードをWebブラウザ上で確認してみます。配列で渡したURLがプラグイン側に読み込まれ、最初の画像が表示されたかと思います。画像は進むボタン戻るボタンで切り替えることが出来ます。この二つのボタンは、prevイベントとnextイベントを発火させているだけで、切り替え処理はプラグインがそれらのイベントを受け取って内部で行っています。

次に画像が自動的に切り替わる処理を実装します。以下のプラグインに以下のコードを追加します。

// 要素をひとつずつ処理
elements.each(function() {
  var $img = $(this);
  var current = 0;

  function show(index) {
    var total = opts.images.length;
    while (index < 0) {
      index += total;
    }
    while (index >= total) {
      index -= total;
    }
    current = index;
    $img.attr('src', opts.images[index]);

    if (auto) {
      start();
    }
  }
  ・
  ・
  ・
  var auto = false;
  var id;

  function start() {
    stop();
    auto = true;
    id = setTimeout(next, opts.interval);
  }

  function stop() {
    auto = false;
    clearTimeout(id);
  }

  $img.bind('start', start).bind('stop', stop);
});

デフォルトオプションにもコードを追加します。

// デフォルトオプション
$.fn.slideshow.defaults = {
  images: [],
  interval: 2000
};

プラグインの呼び出し側(HTML)には以下のコードを追加します。

$image.trigger('start');

おわりに

既存のjQueryプラグインを探したけど、自分がホントに欲しい機能とは微妙に違っているものしか見つからないといったシチュエーションはよくあります。またjQueryは、prototype拡張という考え方がほとんど無いかわりに、足りない機能はプラグインを作ってそれで補完する$.fnを使って自作関数をjQueryに組み込む事で、prototype拡張を実現するというポリシーで成り立っています。世の中にはとんでもないような高機能のプラグインがドッサリとありますが、基本的な骨格はこれまでに挙げたサンプルコードとそこまでの違いはありません。足りない機能、欲しい機能はプラグインを自作するという習慣を少しずつ積み重ねていけば、やがては大規模で高性能なプラグインを作れるだけのスキルが身についてきます。

はじめは凄く小さなものから少しずつ。

参考URL

  • http://twitter.com/ukyo ukyo

    jQuery.prototype === jQuery.fnですよ。
    なので、プラグイン作成はprototype拡張と同義かと思います。

    • 山田 直樹

      これは失礼しました。jQuery.fnを使って自作関数を組み込んで拡張するという仕様でしたね。
      ご指摘頂いた内容を元に記事を修正いたしました。
      誠にありがとうございます。

    • 山田 直樹

      > ukyo さん

      これは失礼しました。$.fnを使ってjQueryオブジェクトに自作関数を組み込むのは、正しくprototype拡張でしたね。
      ご指摘頂いた内容を元に記事を修正しました。
      誠にありがとうございます。

  • 山田 直樹

    > ukyo さん

    これは失礼しました。$.fnを使って自作関数をjQueryオブジェクトに組み込むという仕様は、正にptorotype拡張そのものでしたね。
    ご指摘頂いた内容をもとに記事を修正しました。
    誠にありがとうございます。

  • 山田 直樹

    > ukyo さん

    これは失礼しました。$.fnを使ってjQueryオブジェクトに自作関数を組み込むのは、正しくprototype拡張でしたね。

    ご指摘頂いた内容を元に記事を修正しました。
    誠にありがとうございます。