先ほど拙作WordPressプラグインのNever Let Me Goをアップデートしたので、その際の作業ログです。基本的にはWordPressを知っていて、PHPとかJavascriptとかはまあわかるかなという人を対象にしています。それでは、レッツスタディ。かなり長いですよ。
成果物
Ajaxでユーザーを検索して、IDと名前の一覧を取得して表示、選択すると該当するユーザーIDをinputタグにセットします。
Ajaxを導入した経緯
Never Let Me Go(以下NLMG)はユーザーが自分で退会できるようにするプラグインなのですが(参考)、そのユーザーがコンテンツを作成していた場合、そのコンテンツは消えてしまいます。
Q&Aサイトのようなナレッジサイトを作っていた場合のように、コンテンツが消えちゃうと困るケースがでてきます。ユーザーからすると「自分の書いたものは自分のものなんだから、退会するときは消してほしい」という思いもあるでしょうが、掲示板などのスレッド形式のものは後からコンテンツがなくなると困っちゃいますよね。
で、NLMGはユーザーが退会する場合にコンテンツを特定のユーザーに割り当てる機能を付けることにしました。たとえば、「削除されたユーザー」というユーザーを作っておいて、退会した場合は退会者のコンテンツがすべて「削除されたユーザー」のものになるということですね。こうすればテーマをそれほどいじることなくコンテンツを残すことができます。投稿数ランキングなんかを作っている場合は、「削除されたユーザー」が一位になってしまうという問題がありますが、それぐらいは我慢してコーディングしてもらうことにします。
さて、こうなるとNLMGの設定ページで割当先のユーザーIDを指定する必要があるのですが、素直に考えるとプルダウンにするのが楽ですね。しかしながら、これはこれで問題があります。
ユーザーが3,000人いる場合を想定してもらえばわかると思うのですが、プルダウン(selectタグ)は100件ぐらいが限度で、それ以上になると自分が探しているものを見つけ出すのが困難です。また、3,000件も一気に出力すると、普通はPHPがタイムアウトを起こします。
<?php $users; //3,000件のデータが入った配列 ?> <select name="users"> <?php foreach($users as $user): /*3,000回ループ*/?> <option value="<?php echo esc_attr($user->ID); ?>"> <?php echo esc_html($user->display_name); ?> </option> <?php endforeach; ?> </select>
かといってわざわざ別に検索用ページを設けるのも大変なので、Ajax+インクリメンタル検索によって実装すれば、会員が10万人になっても速やかにユーザーIDが取得できるということにします。
Ajax != DHTML
たまに勘違いしている人がいるのですが、Ajaxというのは単にHTMLページが切り替わることではなく、ページ遷移することなく裏側でデータを取得しにいくことまで含んでいます。たとえばTwitterのタイムラインを取得するだけならJavascriptをちょろっと書けば動きますが、自分の作ったサイトのデータをAjaxで取得する場合、Javascriptを書くのはもちろん、DBからデータを引っこ抜いてJSONとかXMLに変換して出力しなければなりません。
一概に「Ajaxで更新されるUIを作った」といっても、データを取りにいく先がGoogleマップやTwitterなのか、自分のサイトなのかで負担はかなり違います。ここら辺をきちんと把握していないと痛い目を見ることがよくあります。
WordPressでAjaxをやるときの定石
WordPressでは管理画面がAjaxバキバキなので、このための機能が用意されています。基本的なやり方は以下の通りです。
- wp_ajax_xxxというアクションフックに関数を登録する
- POSTまたはGETでリクエストを受け取り、データを返す関数を作成する
- エンドポイントに対してリクエストを発行し、結果を受け取るJSを作成する
これが基本です。
1. アクションフックを登録する
ではまずアクションフックの登録の仕方です。アクションフックの名前はwp_ajaxと自分で決めたアクション名を連結したものになります。今回はユーザー名とIDを検索するものなので、プラグイン接頭辞としてnlmg_という文字列を使い、wp_ajax_nlmg_user_searchとしましょう。
add_action('wp_ajax_nlmg_user_search', '_nlmg_user_search');
なお、WordPressのAjax機能は元々管理画面向けのものだったため、wp_ajax_xxxというフックはログインしたユーザーにしか有効になりません。ログインしていないユーザー向けにはwp_ajax_nopriv_xxxというフックも用意されているのですが、これはこれでログインしているユーザーには有効にならないという不思議な仕様になっているので、「ログインしているユーザーにもログインしていないユーザーにもAjax用のエンドポイントを用意したい」という場合は両方にフックを登録する必要があります。
add_action('wp_ajax_nlmg_user_search', '_nlmg_user_search'); add_action('wp_ajax_nopriv_nlmg_user_search', '_nlmg_user_search');
2. 登録した関数を作成する
さて、上でフックに登録した関数_my_user_searchの機能を作成します。今日日Ajaxで言葉通りにXMLを使うケースはあまりないので、JSONを出力するようにしましょう。
今回の仕様では、「検索クエリを受け取り、ユーザー一覧を返す」という関数を作ります。「検索クエリをスペースで区切ってAND検索」などという複雑な機能は面倒なので実装しません。ちょっと長いですが、一気に書きます。ポイントはJSONを出力したあと、dieかexitを使って必ずプログラムを終了させることです。
function _nlmg_user_search(){ //配列の初期値 $result = array( 'status' => false, 'total' => 0, 'results' => array() ); //ユーザーが管理者でクエリが設定されていたら if(current_user_can('manage_options') && isset($_POST['query'])){ global $wpdb; //データベース接続オブジェクト $query = '%'.(string)$_POST['query'].'%'; $sql = <<<EOS SELECT SQL_CALC_FOUND_ROWS ID, display_name FROM {$wpdb->users} WHERE user_login LIKE %s OR user_email LIKE %s OR display_name LIKE %s LIMIT 10 EOS; $result['results'] = $wpdb->get_results($wpdb->prepare($sql, $query, $query, $query), ARRAY_A); $result['total'] = (int)$wpdb->get_var("SELECT FOUND_ROWS()"); $result['status'] = (boolean)$result['total']; } //PHPの配列をJSONに変換して出力 header('Content-Type: application/json; charset=utf-8'); echo json_encode($result); die(); }
これで、機能はできたのですが、どのURLにアクセスすればこのJSONが得られるのかを知っていないとJSが書けません。これを知っておきましょう。
wp_ajax_のエンドポイントはブログのURL/wp-admin/admin-ajax.phpです。ただし、フックに登録した関数を起動させるには、リクエストのactionパラメータにwp_ajax_の次に指定した文字列(この場合はnlmg_user_search)を指定しないといけません。また、検索を行うためにはクエリを指定する必要があるので、送信するべきデータはaction=nlmg_user_search&query=[検索したい語句]となります。
この時点でデータの取得がきちんと出来ているかどうかを確かめるためには、Firefox拡張とかChrome拡張とか、POSTを投げられる機能を持つものを使ってください。
3. データの取得を行うJSを作成する
まずはJSを読み込みます。今回はonload.jsというファイルを作成し、管理画面だけで読み込むようにします。JSには読み込みを確認するため、alertでも書いておいてください。
//onload.js alert('読み込めたかな?');
//管理画面でJSを読み込むフックを登録 add_action( 'admin_enqueue_scripts', '_nlmg_enqueue_script' ); function _nlmg_enqueue_script(){ wp_enqueue_script('nlmg-onload', "path/to/onload.js", array('jquery')); }
さて、これでとりあえずJSの読みこみはできましたが、一つ問題があります。エンドポイントがブログのURL/wp-admin/admin-ajax.phpだということは書きましたが、これは開発環境(localhost)や本番環境で変わります。プラグインだとなおさら。これをJSにべた書きすることは難しいので、wp_localize_scriptという関数を使ってPHPから値を渡します。
function _nlmg_enqueue_script(){ wp_enqueue_script('nlmg-onload', "path/to/onload.js", array('jquery')); wp_localize_script('nlmg-onload', 'NLMG', array( 'endpoint' => admin_url('admin-ajax.php'), 'action' => 'nlmg_user_search' )); }
これでNLMGというグローバルオブジェクトに値がアサインされます。
alert(NLMG.action); // -> {String} 'nlmg_user_search'
やっと準備が出来ました。まずは試しに単純にAjaxを行うコードを書いてみます。
jQuery(document).ready(function($){ $.post(NLMG.endpoint, { action: NLMG.action, query: 'テスト' }, function(response){ console.log(response); // -> {Object} }); });
きちんとresponseが返ってきていたらオッケーです。
インクリメンタル検索の実装
それでは、インクリメンタル検索の実装をonload.jsにて行っていきます。ベースとなるHTMLはこんな感じにしましょう。
<label> <?php $this->e('User ID'); ?> <input type="text" class="small-text" id="nlmg_assign_to" name="nlmg_assign_to" /> </label> <div class="inc-search-container"> <input type="text" class="regular-text" id="user-inc-search" placeholder="検索してください" /> <img class="loader toggle" src="<?php echo get_template_directory_uri(); ?>/assets/ajax-loader.gif" width="16" height="11" /> <ul id="user-inc-search-result"></ul> </div>
#nlmg_assign_toに文字が入力され、それが2文字以上だったら検索を実行し、その結果を#user-inc-search-result内にliタグで表示。さらに#user-inc-search-result内のリンクがクリックされたら、#nlmg_assign_toのvalueを設定します。また、Ajax検索中であることを示すGIF画像も追加しておきます。
インクリメンタル検索の遅延実行
キーボードのイベントを補足するのはkeyUpなのですが、日本語の場合は日本語変換モードがあるため、ブラウザ間の挙動もバラバラです。また、打った文字すべてで検索されるというのは負荷も高いですし、そもそもユーザーが意図していない可能性が高いです。「高橋」と入力する途中の「高h」で検索されたらうざいでしょう。WordPressのデフォルトタグ入力欄もちょっと挙動が変(漢字変換を確定させるとタグが決定してしまう)なので、日本語特有の問題といえます。
こうした問題に対処するため、イベント処理は下記のフローで行うことにします。
- 入力イベントをすべて補足し、現在の文字数が2文字以上であるかをチェックする
- 2文字以上だった場合は、現在の文字を保存する
- 現在Ajaxリクエストを発生しているかどうかをチェックし、発生していなかったら0.5秒後に検索を行うようにする
- Ajaxリクエストは発生していないが、カウントダウン中だった場合はカウントダウンをリセットする
これらすべてを実装すれば、「ユーザーが一通り検索語句を入力し終えたタイミングでAjaxリクエストが発生する」ようになります。
jQuery(document).ready(function($){ var incSearch = { currentChar: '', timer: null, onloading: false, setUserId: function(e){ e.preventDefault(); $('#nlmg_assign_to').val($(this).attr('href').replace(/[^0-9]/, '')); }, fire: function(){ incSearch.onloading = true; $('.inc-search-container .loader').removeClass('toggle'); $.post(NLMG.endpoint, { action: NLMG.action, query: incSearch.currentChar }, incSearch.result); }, result: function(response){ incSearch.onloading = false; incSearch.timer = null; $('.inc-search-container .loader').addClass('toggle'); //Empty container var container = $('#user-inc-search-result'); container.empty().css({ display: 'block' }); if(response.total > 0){ container.append('<li class="no-result">' + NLMG.found.replace(/%%/, response.total) + '</li>'); for(i = 0, l = response.results.length; i < l; i++){ container.prepend('<a href="#' + response.results[i].ID + '">' + response.results[i].display_name + '</a>'); } container.find('a').click(incSearch.setUserId).fadeIn(); }else{ container.append('<li class="no-result">' + NLMG.noResults + '</li>').fadeIn('fast', function(){ setTimeout(function(){ container.fadeOut(); }, 2000); }); } }, clearResults: function(e){ $(this).val(''); $('#user-inc-search-result').fadeOut('fast', function(){ $(this).css('display', 'none'); }); } }; $('#user-inc-search').keyup(function(e){ incSearch.currentChar = $(this).val(); if(incSearch.currentChar.length > 2 && !incSearch.onloading){ if(incSearch.timer){ //Timer exists, reset clearTimeout(incSearch.timer); } //Enqueue AJAX search incSearch.timer = setTimeout(incSearch.fire, 500); } }); $('#user-inc-search').blur(incSearch.clearResults); });
ポイントとしては、処理の多くをincSearchというオブジェクトにまとめていることでしょうか。jQueryのthisスコープがよくわからないので、いつもこんな感じにしてます。お疲れさまでした。
Ajaxの便利な使い方
これはWordPressに限った話ではありませんし、僕自身試していない方法もあるですが、こんなところに使いどころがあるんじゃないかなと思っています。
スマートフォンサイト対策
スマートフォンは貧弱なネットワーク環境なので、コンテンツを丸っと読み込むと重いなーということがよくあります。コンテンツを一通り読んでもらった後にスクロール位置を判断しておすすめコンテンツやナビゲーションなどをAjaxで出すのはいいんじゃないでしょうか。
キャッシュ対策
最近はNginxとかでプロキシキャッシュを利用することがよくありますが、ユーザーがログインしている場合、これが使えません。ユーザーAとユーザーBの購入履歴ページが混ざっちゃったらヤバいですよね。そこで、ユーザー情報に関してはAjaxで読み込むようにしておけば、サイトの大部分にプロキシキャッシュを適用しつつ、ユーザー最適化されたコンテンツを出力できます。ちなみに高速化の本などを読むと、「AjaxリクエストもGETにしてキャッシュするようにしておけ」とか書いてありますね。
というわけで、一通り書いたのでここで終わります。試してみてください。