さて、前回まででボタンを出力することに成功したのですが、ボタンがどんな風になるかはtwitterのUIをパクる参考にすることでわかりましたね。ボタンの遷移はこんな感じになります。
- フォローしていない場合は「フォロー」と表示
- フォローしている場合は「フォロー中」という表示
- 「フォロー中」にマウスオーバーすると、「解除」という表示になる
あと、どんなアクションがあればいいでしょうか。ちょっとここで立ち止まって考えましょう。
そうですね、まずログインしていないユーザーにはどんな風に表示すべきでしょうか。twitterにログアウトした状態でアクセスすると、「フォロー」というボタンを押すことでユーザー登録ダイアログが開きます。下の方には「アカウントを持っている人はログイン」というリンクが存在しますね。
これは参考にしたいところです。が、ダイアログを開くのが大変なのでやめます。
とりあえず簡単な方法として、ログインしていないユーザーが「フォローする」ボタンを押した場合は、WordPressのログイン画面にリダイレクトさせ、その際にredirect_toクエリを渡して、同じページに戻ってきてもらう、ということにしましょう。
あと、何も考えずに「マウスオーバーすると」と書いてしまいましたが、スマホだとマウスオーバーないですね。試しにtwitterのアプリを見てみると、「フォロー中」とだけ表示され、クリックすると「解除・キャンセル」のダイアログが出ます。
実装を分けるのがめんどくさいので、すでにフォローしているユーザーの場合は表示を「フォロー中」にしましょう。クリックしたら「このユーザーのフォローを解除しますか?」と確認ダイアログを出して、オッケーだったら解除するようにします。
マウスオーバーはPC用のおまけ程度ですね。
キャッシュ対策
最近、WordPressサイトを運用する場合、キャッシュプラグインを入れることがわりかし多いと思います。この場合、ページを出力する画面でログインを判定するのはあまり得策ではありません。キャッシュの提供機会が減ってしまいますからね。
つまり、ボタンは誰が見ても問題ないような仕組になっていないといけません。そうすると、ボタンが画面に表示された時ではなく、表示されたあとにいろんな判定を行えばいいんではないでしょうか。
<!-- 初期表示 --> <a href=“/path/to/wp-login.php?redirect=/current/page” class=“disabled"> <span>フォロー</span> </a> <!-- ログインしていなかったら…… --> <a href=“/path/to/wp-login.php?redirect=/current/page”> <span>フォロー</span> </a> <!-- ログインしていたら…… --> <!-- まだフォローしてない…… --> <a href=“/path/to/ajax-endpoint”> <span>フォロー</span> </a> <!-- フォロー済み…… --> <a href=“/path/to/ajax-endpoint” class=“following"> <span>フォロー中</span> </a>
上記のような出力ルールにしておき、次のような実装にします。
- disabledクラスがあったら、クリックしても反応しない
- 画面が表示されたあと、画面に存在するすべてのフォローボタンを取得し、現在のユーザーがその投稿者IDをフォローしているか、Ajaxで問い合わせ。
- 問い合わせ完了後、それぞれのボタンからdisabledクラスを取り、表示をフォロー状況に応じて変更する。
ボタンごとに問い合わせを行うと、リクエストが増えて面倒くさいので、一括で取得します。また、よく考えたら各ボタンがユーザーIDを知っていないといけないですね。これはHTML5のdata属性で行います。
他に知っておかないといけないのは……
- CSRF脆弱性対策用のnonce。これはユーザーごとに一意であればよい。
- Ajaxのエンドポイント。これはどのボタンでも共通なので、JSと一緒に書き出します。
これぐらいでしょうか。これでいまのところ、問題ないっぽいです。
ボタンを出力する
では、出力するボタンを変更しましょう。まず、初期状態では次の条件を満たしている必要があります。
- クラスにdisabled
- リンク先はログインページ
- data属性に投稿者のIDを追加
前回作ったfreundschaft_btnという関数をこんな風に変えましょう。cssのクラスには接頭辞fsをつけてみました。
/** * フォローボタンを出力する */ function freundschaft_btn(){ $redirect_to = get_permalink(); $author_id = get_the_author_meta('ID'); printf('<a class="fs-btn fs-disabled" data-author-id="%d" href="%s">フォローする</a>', $author_id, wp_login_url($redirect_to)); }
うーん、これだけだと気分が出ないですね。やはりCSSとJavascriptも読み込む必要があるようです。
CSSおよびJSの読み込み
さて、今回はプラグインのルートフォルダにassetsフォルダを置き、その中にjs, sass, cssと配置しましょう。Compassで管理します。僕はCodekitというアプリを使っていますが、Gruntでもなんでも構いません。
ルールとしては、sassフォルダのscssがcssフォルダにコンパイルされます。また、jsのJavascriptは*.min.jsという名前にミニファイされます。
それでは、freundschaft.jsとfreundschaft.cssを作成し、それを読み込むようにします。
/**! * Freundshaft JS */ (function ($) { 'use strict'; // ボタンの初期値 var $btns; // DOM READYでボタンを取得 $(window).ready(function(){ $btns = $('.fs-btn'); // 読み込みを確認するために、ボタンの個数を吐く console.log($btns.length); }); })(jQuery);
@charset "UTF-8"; // freundschaft.scss @import "compass"; $disabled-color: #999; /* 共通 */ .fs-btn{ display: inline-block; font-size: 0.85em; border-width: 1px !important; border-style: solid !important; @include border-radius(2px); padding: 0.25em 0.5em; &:link, &:visited{ text-decoration: none; } &:hover, &:active{ text-decoration: none; } } /* 未使用時 */ .fs-disabled{ border-color: $disabled-color !important; &:link, &:visited, &:hover, &:active{ color: $disabled-color; background-color: white; cursor: not-allowed; } }
では、これらのアセットを読み込むコードをfreundschaft.phpに書きます。
/** * JSとCSSを読み込み */ add_action('wp_enqueue_scripts', function(){ // assetsディレクトリのURLを取得 $assets_url = plugin_dir_url(__FILE__).'assets'; $asset_version = '1.0'; // JSを読み込み。WP_DEBUGがtrueじゃなければ圧縮ファイル。 // jQueryに依存するので、それを指定 wp_enqueue_script('freundschaft', $assets_url.'/js/freundschaft'.(WP_DEBUG ? '' : '.min').'.js', array('jquery'), $asset_version); // CSSを読み込み。 wp_enqueue_style('freundschaft', $assets_url.'/css/freundschaft.css', array(), $asset_version); });
どうでしょう。Twenty Fifteenのリンクにborder-bottomが指定されていたので、!importantを連発することになってしまいました……。まあ、しょうがないですね。
こうすると、それっぽいボタンが出てきます。では、JSをカスタマイズして、ログインの抑止から始めましょう。
リンクのクリックを制御
さて、JSにあまり詳しくない人が「fs-disabledクラスがついたリンクをクリックしても何も起きない」を普通に実現とこんな感じになると思います。
jQuery(document).ready(function($){ // DOMREADYイベントでクリックイベントを添付 $(‘.fs-disabled’).click(function(e){ // デフォルトイベントの抑止 e.preventDefault(); }); });
結論から先に言うと、これはダメです。それは、この先実装しようとしている機能に関連しています。次の画像はtwitterで自分のフォロワー一覧を表示した画面なのですが、下に行くとローディング用のインジケーターが出て、動的にフォロワーが追加されていきます。
こういったUIの場合、DOMREADYイベントが毎回発生するわけではありません。別の方法で管理する必要があります。
jQueryの場合はonというメソッドを使うことで、オシャレに管理できます。先ほど書いたJSのクロージャ内に以下のコードを追加してください。
(function($){ // ……中略…… $(document).on('click', '.fs-btn', function(e){ if( $(this).hasClass('fs-disabled') ){ e.preventDefault(); } }); })(jQuery);
Javascriptにはイベント伝播という仕組みがあって、ある要素でイベントが発生すると、DOMツリーを辿ってどんどん上の要素に伝わっていくんですね。$().onはこの仕組みをうまく利用したイベント管理システムです。
監視対象はdocumentなので、これがなくなることはまずないでしょう。で、そこを監視した上で、さらに.fs-btnに発生したクリックイベントのみに反応します。こうすれば、後からボタンを追加しても問題なく動くんですね。
パフォーマンスがががーという人もいるかもしれませんが、WordPressプラグインからDOM構造を規定することはできない(=テーマが自由に決める)ので、こんな感じになります。
これでとりあえずクリックしても遷移しなくなりました。
ログイン判定
WordPressでのログイン判定はPHPで行えますが、前述した理由(キャッシュ対策)のとおり、AJAX経由でログイン判定を行います。
ボタン全部に対して一個ずつ行うのはカッコ悪いので、こんな処理にしましょう。
- 画面に存在するボタンを全部取得する
- それぞれの投稿者IDを全部まとめてPOSTでAjaxする
- ログインしている場合はログインOKと指定された投稿者をフォローしているかのオブジェクト、nonceが返ってくる
- ログインしていない場合は、NGのデータが帰ってくる
JSONで見ると、こんな感じです。
// OK { “logged_in”: true, “users”: { “user_1”: true, “user_2”: false, “user_3”: true }, “nonce”: “0987623487" } // NG { “logged_in”: false, “users”: {}, “nonce”: “" }
これだけならCSRFをぶっこまれても特に問題無いはずです。まずは、NGの場合の処理を実装しましょう。
NGの場合
ログインしていないユーザーのAjaxリクエストはすべてwp_ajax_nopriv_*
フックを通過します。freundschaft.phpにこんなコードを追加してください。
/** * Ajaxを実装 */ add_action('admin_init', function(){ if( defined('DOING_AJAX') && DOING_AJAX ){ // Ajaxリクエストのときだけ実行 add_action('wp_ajax_nopriv_fs_status', '_freundschaft_not_logged_in'); } }); /** * ログインしていないユーザーのAjaxレスポンス */ function _freundschaft_not_logged_in(){ wp_send_json(array( 'logged_in' => false, 'users' => array(), 'nonce' => '', )); }
これで、 example.jp/wp-admin/admin-ajax.php?action=fs_status へアクセスすると、JSONが表示されるはずです。ログアウトした状態でお試しください。
ちなみに、wp_send_json
は配列をJSON出力してヘッダーもつけてくれる関数です。
それでは、JSも実装してしまいましょう。JSからはWordPressのAjaxエンドポイントを簡単に知るために、wp_localize_script
を使います。freundschaft.phpでwp_enqueue_scriptを実行している部分に追記してください。
/** * JSとCSSを読み込み */ add_action('wp_enqueue_scripts', function(){ // assetsディレクトリのURLを取得 $assets_url = plugin_dir_url(__FILE__).'assets'; $asset_version = '1.0'; // JSを読み込み。WP_DEBUGがtrueじゃなければ圧縮ファイル。 // jQueryに依存するので、それを指定 wp_enqueue_script('freundschaft', $assets_url.'/js/freundschaft'.(WP_DEBUG ? '' : '.min').'.js', array('jquery'), $asset_version); // JSに変数を渡す wp_localize_script('freundschaft', 'Freundschaft', array( 'endpoint' => admin_url('admin-ajax.php'), 'action' => 'fs_status', )); // CSSを読み込み。 wp_enqueue_style('freundschaft', $assets_url.'/css/freundschaft.css', array(), $asset_version); });
これでJSから変数にアクセスできます。詳しくはこのブログの記事いまさらだけどWordPressでAjaxのやり方などをごらんください。
それでは、JSに追記しましょう。やるべきことは上に書いた通りですね。とりあえず、ログインしていなかった場合の処理をボタンを取得する処理のあとに書きます。
$(window).ready(function(){ // DOM READYでボタンを取得 $btns = $('.fs-btn'); var authorIds = []; // ボタン全部の投稿者IDを取得して、ログイン判定 $btns.each(function(index, btn){ authorIds.push($(btn).attr('data-author-id')); }); // 投稿者IDがあればAjax if( authorIds.length ){ $.post(Freundschaft.endpoint, { action: Freundschaft.action, author_ids: authorIds }).done(function(result){ if( result.logged_in ){ // ログインしている // ここは後で。 }else{ // ログインしていないので、 // ボタンをログインに $btns.removeClass('fs-disabled').addClass('fs-login'); } }).fail(function(xhr, status, message){ alert(message); }); } });
ついでにCSSも書きましょう。freundschaft.scssに追記します。
// ログインリンク $active-color: #0074a2; .fs-login{ border-color: $active-color !important; &:link, &:visited{ color: white; background-color: $active-color; } &:hover, &:active{ color: white; background-color: lighten($active-color, 10); } }
ログインしない状態で該当するページを見ると、一瞬でリンクの色が切り替わるのがわかります。
OKの場合
続いて、ログインしていた場合の処理を書きましょう。ただし、実際にログインしているユーザーを取得する処理は次回に持ち越しなので、常にtrueの場合と、常にfalseの場合を試して今回はお茶を濁します。
それでは、freundschaft.phpにログイン済みユーザー用のAjax処理を書きましょう。
/** * Ajaxを実装 */ add_action('admin_init', function(){ if( defined('DOING_AJAX') && DOING_AJAX ){ // Ajaxリクエストのときだけ実行 add_action('wp_ajax_nopriv_fs_status', '_freundschaft_not_logged_in'); add_action('wp_ajax_fs_status', '_freundschaft_logged_in'); } }); /** * ログイン済みユーザーのAjax */ function _freundschaft_logged_in(){ $users = array(); if( isset($_POST['author_ids']) && is_array($_POST['author_ids'])){ foreach( array_unique($_POST['author_ids']) as $author_id ){ if( is_numeric($author_id) ){ // とりあえず全部true $users['user_'.$author_id] = true; } } } wp_send_json(array( 'logged_in' => true, 'users' => $users, 'nonce' => wp_create_nonce('freundschaft'), )); }
これで、JSに以下の処理を追記します。さきほど、「後で」と書いた場所に追記してください。
if( result.logged_in ){ // ログインしているので、ユーザーに応じたボタンを出力 for( var prop in result.users ){ if( result.users.hasOwnProperty(prop) ){ var userId = prop.replace(/user_/, ''); if( result.users[prop] ){ // フォローしている $('.fs-btn[data-author-id=' + userId + ']').removeClass('fs-disabled').addClass('fs-following'); }else{ // フォローしていない $('.fs-btn[data-author-id=' + userId + ']').removeClass('fs-disabled').addClass('fs-follow'); } } } }else{ // ログインしていないので、 // ボタンをログインに $btns.removeClass('fs-disabled').addClass('fs-login'); }
この結果、フォローしている場合はfs-following、フォローしていない場合はfs-followというクラスがつくようになりました。それでは、scssを修正しましょう。ちなみに、ログインリンクとフォローリンクは同じ見栄えでいいですね。
// ログインリンク, フォローリンク .fs-login, .fs-follow{ /* さっきと同じ */ } // フォロー中 .fs-following{ span{ display: none; } border-color: $active-color !important; &:before{ display: inline-block; content: 'フォロー中'; } &:link, &:visited{ background-color: white; color: $active-color; } &:hover, &:active{ &:before{ content: "フォロー解除"; } background-color: $negative-color; color: white; border-color: $negative-color !important; } }
擬似要素で中身を変えていたりしてちょっと変ですが、当初予定していたものには近づきました。
先ほどはログアウトして試したと思うので、今度はログインしてみてください。うまくいったら、上のコードでとりあえず全部trueにしたのをfalseに変更して、見栄えが変わるかどうかを試してみましょう。
どうでしょう。とりあえず、できたのではないでしょうか。実際の取引は行わないが、それを買ったことにして資産運用(仮)をしている中学生の投資家レベルまではきましたね。タイトルはwpdb道場なのに、最近一回も$wpdbって書いてないですね。
まとめ
- ボタンの遷移状態をまとめた
- Ajaxを実装して、ログイン状況に応じた出しわけができた
最初のゴールまで残された課題は次の通りです。
- Ajaxアクション内でDBに書き込む関数を作成する
- その関数をAjaxで実行する
ソースはGithubでどうぞ。wpdb-dojo/step2というブランチが該当します。
この後はこんな課題が控えています。
- 自分のフォロワーとフォローしている人数を確認してうっとりするための画面を作る
- 現在の構成だとユーザーからアクセスがあるたびにいちいちAjaxリクエストを送っていてウザいので、パフォーマンスを向上させる
- 関数がたくさん増えてくるとうんざりするので、リファクタリングする
土日を普通に休んでしまったので、またどこかで挽回しないと……。俺ベンターの朝は早い……。
[429] [429] Client error: `POST https://webservices.amazon.co.jp/paapi5/getitems` resulted in a `429 Too Many Requests` response: {"__type":"com.amazon.paapi5#TooManyRequestsException","Errors":[{"Code":"TooManyRequests","Message":"The request was de (truncated...)