jQueryが古い場合にAmazonJSが動かない件について

AmazonJSが動かない、くるくるローディングしたまま表示されてないという報告がよくあります。原因はいろいろあるのですが、他のプラグインやテーマが古いjQueryを差し替えてきて互換性の問題で動かなくなるというケースが増えています。

WordPress 3.6ではjQuery 1.10が同梱されています。ためしにWordPress 3.6 + Twenty ThirteenでjQuery 1.4に置き換えるとどうなるでしょう。

これはGoogle Chromeのツール > Javascript コンソールを表示してページを表示したときの画面です。

Javascript コンソール

何やらエラーがでていますね。このオブジェクトは on なんてメソッドは持ってないよと書いてあります。

jQueryで .on が実装されたのはバージョン 1.7 からなのでそれより古いjQueryを使うとこのようなエラーが起きます。そしてエラーが起きるとその後の処理が中断されてしまい、AmazonJSの処理が実行がされず商品の表示がされない(ローディングしたまま)ということになります。

エラーが起きているthickbox.jsはAmazonJSがAmazonのレビューを表示するために使っています。これもWordPressが同梱しているライブラリなので当然、WordPressが同梱しているjQueryより古いもので動く保証はありません。とは言え、あまりにこの問題が多いならAmazonのレビューを表示する機能(thickbox.jsを使うこと)をオプションにしようかと思っていました。

しかしここで想定外なことが起きましたもう一つのエラー箇所function.jsはTwenty Thirteenテーマのスクリプトです。ユーザさんがTwenty Thirteenテーマを使っているならAmazonJSがThickboxの使用を止めたとしてもそこでエラーがでるので対処できません。

実はAmazonJSもjQueryを置き換えることがあります。AmazonJSではjQuery templateを使っていますがこれにはjQuery 1.4.2以上が必要なので以下のようにして置き換えています。

$v = get_bloginfo('version');
if (version_compare($v, '3.0', '<')) { // jQuery tmpl requires jQuery 1.4.2 or later
    wp_deregister_script('jquery');
    wp_register_script('jquery', self::JQ_URI, array(), self::JQ_VERSION);
}

jQueryを置き換えるというのはできればやりたくありませんでした。重要なライブラリであればあるほど、影響範囲を想定して常識的なソフトウェアエンジニアは互換性を考慮するでしょう。WordPressが同梱するjQueryは以下に記載されています。

A History of jQuery Versions Included in WordPress

WordPress 3.6 includes jQuery 1.10.2
WordPress 3.5 included jQuery 1.8.3
WordPress 3.4 included jQuery 1.7.2
WordPress 3.3 included jQuery 1.7.1
WordPress 3.2 included jQuery 1.6.1
WordPress 3.1 included jQuery 1.4.4
WordPress 3.0 included jQuery 1.4.2
WordPress 2.9 included jQuery 1.3.2
WordPress 2.8 included jQuery 1.3.2
WordPress 2.7 included jQuery 1.2.6
WordPress 2.6 included jQuery 1.2.6
WordPress 2.5 included jQuery 1.2.3
WordPress 2.3 included jQuery 1.1.4
WordPress 2.2 included jQuery (version not listed)

AmazonJSはjQueryが1.4.2より古い場合、つまりWordPress 3.0未満でのみ差し替え、WordPress 3.0以上であれば何もしません。

ソフトウェアエンジニアではない人が一生懸命つくったテーマ、プラグインであれば僕は何も言いません。でもソフトウェアエンジニアが互換性も考えずに自分のコードだけが動けばいいという実装をするなら3時間くらい正座させて説教したいです。

すいません。取り乱しました。

AmazonJSからできることを考えていますが、まず表示されなくて困っているユーザの皆さんにお願いしたいのは原因を特定していただくことです。AmazonJSが動かないと言われるのはまだ僕だけのことなのでいいのですが、他が原因なのにこのテーマとAmazonJSだと動かないなど別のところに火の粉が飛ぶのは心苦しい限りです。(テーマが古いjQueryを差し替えたというケースもあったのでテーマの問題であることもありますが)

Google Chromeのツール > Javascript コンソールでjavascriptのエラーがどこででているかや、HTMLのソースをみて古いjQueryが差し替えられていないか、テーマを変えてみたり、プラグインを1つずつ無効にしてみるなどまず問題の切り分けをしてみてください。

そしてテーマやプラグインが古いjQueryを差し替えていそうなことがわかったら、WordPressが同梱するjQueryのバージョン一覧を教えてあげて古いものを入れると良からぬことが起きることを報告してあげてください。

AmazonJSが悪かった場合も、こんなことがいろいろあってWordPressに対する気持ちが冷めつつあるので優しく報告していただけると幸いです。わりとJekyllに移行できるなら移行したいとか考えたりもするので。

jQuery tmplのtemplateでユーザ関数を使ってみた AmazonJS 0.1.3b

AmazonJS 0.1.3bをリリースした。

もともとはWordPress 3.3 BetaでjQueryのバージョンがあがるよーってことで動作確認したけどそもそもAmazonJSはWordPress 3.0以上ではWordPress同梱のjQueryをwp_enqueue_scriptで読み込むので他のプラグイン達が余計なことをしない限り大丈夫なのであった。が、それ以外の問題で動かなかったのでちょこっと修正した。AmazonJSは投稿画面からAmazon商品を検索する際にメディアのアップロード/挿入のダイアログを流用しているのだけれど、そのあたりで3.3 betaだとうまく動かなったので修正した。

あとずっと直したかった問題をようやく修正した。

↑のように販売価格が取れるときに参考価格に取り消し線を入れて、販売価格を強調して表示ということをやっていたのだけど、実は 参考価格 = 販売価格な時もそうなっていた。これを修正するにはtemplateに制御文を入れないといけないんだけど、それが複雑になりそうだったので放置していた。

今回のバージョンではtemplateでユーザ関数を使うようにして対応してみた。

templateは以下の様な感じ。$itemがtemplateに適応されるアイテムにあたる。

'{{if $item.isSale()}}',
    '<b>'+r.ListPrice+'</b><span class="amazonjs_listprice">${ListPrice.FormattedPrice}</span><br/>',
    '{{if OfferSummary.LowestNewPrice}}<b>'+r.Price+'</b>${OfferSummary.LowestNewPrice.FormattedPrice}{{/if}}',
    '<span>'+r.PriceUpdatedat+'</span>',
'{{else}}',
    '<b>'+r.Price+'</b>${ListPrice.FormattedPrice}',
    '<span>'+r.PriceUpdatedat+'</span>',
'{{/if}}'

(このエントリ書いてて気がついたけど strong タグ使うべきだったな・・・)

で、$itemから呼び出す関数は$.tmplを使う場合は第3引数で指定する。

$.tmpl($template, item, {isSale:function(){
    if (this.data) {
        var lp = this.data.ListPrice, la = lp.Amount || 0,
            os = this.data.OfferSummary || {}, sp = os.LowestNewPrice || {}, sa = sp.Amount || 0;
        return (sa != 0 && sa < la);
    }
    return false;}});

複雑な条件表現は関数にして提供した方がtemplateは読みやすくなる。このあたりが全部javascriptでできるのは素敵だ。

ドキュメントはわかりにくいけど http://api.jquery.com/template-tag-equal/ のあたりにある。

jQuery templateでAmazon商品を紹介するAmazonJSをリリースしました

以前、アマゾンの商品を簡単に紹介したいと思いつつ

というところで自分にあったプラグインを探せなかったので結局自分でつくったものをリリースしました。

機能的にはまだまだ足りなくてベータ版という位置づけですが、こちらのページにインストール方法があります。

ポイントとしてはjQuery templateで商品を表示しているので、アニメーションとかいろいろ遊べそうな感じです。Amazon APIで2010/11にあったカスタマーレビューのiFrame変更にも対応しています。

jQuery tmplを使う

WordPressでAmazonの紹介をjavascriptで頑張るプラグインを書いてみたが、他の人のように独自テンプレートで頑張る気もおきず、ましてはsmartyなどはオーバースペックすぎると思ったのでjQuery tmplを使って表示している。なかなかドキュメントがでてこないので自分用備忘録としてメモを書いてみることにする。

jQuery tmplを導入するには

jQuery tmplは2010/12現時点ではβ版で、最近なにかとjQueryの採用に熱心なマイクロソフトのCDNで配布されている。jQuery tmplはjQuery 1.4.2以上が必要なので

<script type='text/javascript' src='http://ajax.microsoft.com/ajax/jquery/jquery-1.4.2.min.js'></script>
<script type='text/javascript' src='http://ajax.microsoft.com/ajax/jquery.templates/beta1/jquery.tmpl.min.js'></script>

をhtmlに追加すればよい。(jQuery本体は別にjQuery tmpl用に変更されているわけじゃないのでGoogleのCDNからでもjQueryのサイトからダウンロードして自分のサイトにおいたやつでもよい)

テンプレートの作成

jQuery tmplでは表示用のテンプレートと、データを分けて管理できる。テンプレートを作成する方法はいくつかがあるけど、テンプレートに名前をつけて管理できるjQuery.templateを使うようにしている。

$.template('hellowWorldTempalte', '<p>Hello, ${name}</p>" );

というようにデータから展開したいものを${propertyName}でいれた普通のhtmlを設定すればよい。

データのバインディング

事前に作成しておいたテンプレートをつかってデータを表示するには jQuery.tmpl() を使う。第一引数にjQuery.templateで作成したテンプレートの名前、第二引数で表示するデータ(Object)を入れる。ちなみにいきなり第一引数にテンプレートを書き出しもできるので詳しくはドキュメントを参照のこと。

$.tmpl('hellowWorldTempalte', {name:'Makoto'});

この結果はいつものjQuery要素なので $.tmpl(...).appendTo('#result'); だとか、$('#result').append($.tmpl(...)) とかして表示したい位置に突っ込んでやれば良い。

テンプレートの制御文

jQuery tmplでは分岐とか繰り返しといった制御に対応している。

分岐は{{if}}で行える。よく使うのはあるプロパティがあれば表示するといったときに

{{if SalesRank}}<li><b>商品ランキング</b>${SalesRank}位</li>{{/if}}'

というようなやり方。(値がない(null)のと0のときを分けないといけないときは厳密に比較する必要がある)

ポイントは条件文では${}は必要なくてそのままプロパティ名を記述するというところ、ドキュメントにも載っていないので比較演算子がどこまで使えるのかは試していない。

繰り返しは{{each}}でおこなえるがまだ使ったことがない。ちなみに文字列の配列を格納したプロパティは勝手にjoinされて展開された。これが意図した動作なのかは不明である。

展開結果をテキストで取得する

jQuery tmplを使ったものの、ケースバイケースで静的に表示したいので展開した結果をhtml文字列として取得したくなった。結局いい方法がわからなかったので適当なDOM要素に追加してその要素からhtmlを取り出している。

var $item = $.tmpl(tplName, selectedItem);
$preview.empty().append($item);
$previewCode.val($preview.html());

しかしこの方法でもjavascriptの実行が必要になるので、RSSフィードのときはどうしたものかと悩んでいるところ。jQuery tmplを展開するだけのWebサービスをつくらないといけないのだろうか。。。キャッシュを無期限にしてよいならWordPressの管理ページを開いているときに裏でつくって保存のためにXHRでサーバに送りつけるとかできるけど。

jQueryの無名関数の組み立て方

なんかWordPressとjQueryでググるとやたら$が使えないとかnoConflictがどうたらで気になったんですが、jQueryでは無名関数のローカル引数で$を使うのがスマートだと思います。

(function($){
     alert('nanashi');
})(jQuery);

でもこのjQueryの無名関数の書き方ってよく忘れます。でも仕組みを知っていると方程式みたいに組み立てることができます。僕は暗記科目が苦手で数学が得意だったのでこれから説明する方法をよく使います。

function nanashi($) {
  alert('nanashi');
}
nanashi(jQuery);

これなら意味がわかる人は多いと思います。
nanashiという関数を定義して呼び出しているだけです。

nanashi(jQuery)という呼び出しではnanashi関数の$はjQueryとして実行されますが、この$は定義したnanashi関数のスコープ内のローカル変数(引数)なのでglobalな$よりも優先されます。

function nanashi() {
  var $ = arguments[0];
  alert('nanashi');
}
nanashi(jQuery);

ようはこういうことなのでprototype.jsなどが宣言するglobalな$とは区別され、この関数内ではnoConflictを呼び出したかどうかに関わらず$=jQueryとして扱えます。

さて、なぜ$がjQueryとして使えるか理解したところでここから無名関数を導きます。

まず上記のプログラムでは以下の式が成り立っていることになります。

nanashi = function($){
     alert('nanashi');
};

なのでこれをnanashi(jQuery);へ代入し二つの式をひとつにまとめます。

すると

function nanashi($) {
  alert('nanashi');
}
nanashi(jQuery);

function($){
     alert('nanashi);
}(jQuery);

になります。

でもこれだとプログラムの解釈上まずいので代入した等文を()で括ってこいつは関数ですよと教えてあげます。

(function($){
     alert('nanashi);
})(jQuery);

これでjQueryの無名関数が導けました。

WordPressでjQueryをスマートに使う方法

このブログでは自作テーマをつかっているのでテーマのテンプレート内でjQueryとjQuery UIをハードコーディングして読み込んでいました。

で、はてなブックマークコメントプラグインの中でjQueryを使いたくなったんですが、みんながみんなプラグインの中でjQueryを読んだら多重読み込みしてしまったおかしなことになる恐れがあるわけで、ちょっと調べてみたら

function wp_enqueue_script( $handle, $src = false, $deps = array(), $ver = false, $in_footer = false ) 

という便利な関数があることを知りました。

wp_enqueue_scriptはで3番目の引数に依存する他のスクリプトを指定することでWordPressが適切な順番でscriptタグを出力してくれます。WordPressではデフォルトでjQuery(handle="jquery")を持っているの3番目の引数にarray("jquery")って渡してあげると良いです。

なおはてなブックマークコメントプラグインではjQueryの他にブログパーツのjavascriptのも依存するので

wp_enqueue_script('hatena_bookmark_comment_blogparts','http://b.hatena.ne.jp/js/bookmark_blogparts.js');
wp_enqueue_script('hatena_bookmark_comment_blogparts_patch', $this->url.'/bookmark_blogparts_patch.js', array('hatena_bookmark_comment_blogparts','jquery'));

な感じでやってます。

jQuery 1.4になったのでGoogle CDNから拝借する(英語のページから)

いつのまにかjQueryが1.4に更新されていたのでGoogle CDNから拝借する。

http://code.google.com/intl/en/apis/ajaxlibs/documentation/index.html#jquery

日本語は現時点(2/15)でまだjQuery1.3.1とjQueryUI1.5.3とかを載せているのだが。このやる気のナサはなんなんだ。トラップすぎる。
http://code.google.com/intl/ja/apis/ajaxlibs/documentation/index.html#jquery

jQuery + Bing APIで動画検索するコード書いてみた

Bing APIでjsonpを使って動画検索するコードを書いてみた。個人的にはちょっと使えないかもなーって感じだったのだけどせっかくなのでまとめて公開してみる。

まずBing APIを使うにはアプリケーションIDが必要になる。下記の Create an AppID ってとこから取得できる。Live IDが必要になるが簡単にとれる。

http://www.bing.com/developers/ とったアプリケーションIDをクエリのパラメータに含めることになる。API Basicってドキュメントの終わりの方にやんないといけないこと、やっちゃいけないこととか書いてあり、その中にリクエストはIPごとに1秒間に7回以下に抑えろとある。このアプリケーションIDとIPで計測されたりするのだろう。

Restrict your usage to less than 7 queries per second (QPS) per IP address. You may be permitted to exceed this limit under some conditions, but this must be approved through discussion with api_tou@microsoft.com.

IPごととか言われると、サーバサイドでやるよりクライアントサイドでやった方がいいよね。って話になるので今回はjsonpを使う。

まずjQueryにjQuery.ajaxライクなbing関数を拡張。

/*
* jquery.bing
*
* Copyright (c) 2009 makoto_kw (makoto.kw@gmail.com)
* Dual licensed under the MIT and GPL licenses.
*
* Version: 1.0
*/
(function($) {
  $.extend({
    /* options = {
    * appId: 'xxxxx',
    * query: 'sushi',
    * sources: 'web',
    * offset: 0,
    * count: 10,
    * callback: function(data){}
    * }
    */
    bing: function(options) {
      var sources = options.sources || 'web';
      var params = {
        AppId: options.appId,
        query: options.query,
        sources: sources
      };
      if (sources.indexOf('+')==-1) {
        params[sources+'.offset'] = options.offset || 0;
        params[sources+'.count'] = options.count || 10;
      } else {
        // TODO:
      }
      // video
      if (sources.indexOf('video')!=-1) {
        params['video.sortby'] = options.video_sortby || 'Relevance'; // Relevance or Date
      }
      $.getJSON('http://api.search.live.net/json.aspx?Jsontype=callback&Jsoncallback=?', params, options.callback);
    }
  });
})(jQuery);

一応こんな感じで使う。callbackではBing APIのレスポンスを特に整形していないまま渡す。ちなみにリクエスト先はhttp://api.search.live.net/になっていて、bingってブランドはついてるけど実際にはlive searchのmajor version upって扱いかな。

$.bing({appid:\'Your AppID\', query:\'sushi\',callback:function(data){
//
});

で、動画の検索をするときはsourceにvideoを指定する(デフォルトはweb)。特定の動画サイトで検索する場合はクエリに(filterui:msite-nicovideo.jp OR filterui:msite-dailymotion.com OR filterui:msite-youtube.com)を追加すればよい。

フォームと合わせてサンプルを書いてみる。

<table><tr><td width="100%">
  <p>bingで動画検索</p>
  <form id="search_form" method="POST">
    <input id="search_keyword" name="search_keyword" value="Janne Da Arc"/>
    <select id="sortby" name="sortby">
      <option value=Relevance>Relevance</option>
      <option value="Date">Date</option>
    </select>
    <input type="submit" value="SEARCH"/>
    <br style="clear:both"/>
  </form>
  <div id="search_results"></div>
</td>
<td width="200"></td></tr></table>
<script>
(function($) {
  $(document).ready(function(){
    var $form = $('#search_form'), $input = $('#search_keyword'), $results = $('#search_results');
    var $sortby = $('#sortby');
    $form.bind('submit', function() {
      $form.find('.error_list').remove();
      var keyword = $input.val();
      if (keyword=='') {
        $form.prepend('<ul class="error_list"><li>キーワードを入力してください</li></ul>');
        return false;
      }
      $results.html('<div class="video_loading">NOW LOADING...</div>');
      $.bing({
        appId:'Your AppID',
        query:keyword+' (filterui:msite-nicovideo.jp OR filterui:msite-dailymotion.com OR filterui:msite-youtube.com)',
        sources:'video',
        video_sortby:$sortby.val(),
        count:20,
        callback: function(data) {
          var $videos = $('<ul class="video_list"/>');
          var video = data.SearchResponse.Video;
          var total = video.Total || 0, offset = video.Offset || 0, items = video.Results || [];
          for (var i=0, len=items.length; i<len; i++) {
            var v = items[i];
            var thumb = v.StaticThumbnail || {};
            var thumbWidth = 160, thumbHeight = 120;
            if (thumb.Height>0) {
              thumbHeight = Math.min(120, thumb.Height);
              thumbWidth = Math.floor(120*thumb.Width/thumb.Height);
            }
            var $a = $('<a/>').attr('href',v.PlayUrl).attr('rel',v.SourceTitle).
            click(function(e){
              e.stopImmediatePropagation();
              return false;
            });
            var date = new Date(v.RunTime);
            var tm = date.getMinutes(), ts = date.getSeconds();
            if (tm<10) tm = '0'+tm;
            if (ts<10) ts = '0'+ts;
            var duration = tm  + ':' + ts;
            $videos.append($('<li class="rowvideo"/>')
              .append($('<div/>').append($a.clone(true).html('<img src="'+thumb.Url+'" width="'+thumbWidth+'" height="'+thumbHeight+'"/>')))
              .append($('<div/>').append($a.clone(true).html(v.Title))).append('<span class="source">'+v.SourceTitle+'</span><span class="duration">'+duration+'</span>')
              );
          }
          $results.empty().append($videos);
        }
      });
      return false;
    });
  });
})(jQuery);
</script>

これはニコ動とYouTubeとDailyMotionの動画を検索するサンプル。
サクサク動いて良い感じなんだけど、ソートがRelevanceかDateしかないのが不便なところ。動画の場合再生数とかでソートしたくなるのでものによってはやっぱりYouTube DATA APIを使うほうが良い場合もありそう。