サイトアイコン Capital P

WordPress+Vue.JSで作るジョブボード(1) 基礎知識

さて、本連載はWordPressをバックエンドにしてREST APIとJavascriptによるアプリケーションを作成するためのチュートリアルである。利用するライブラリはVue.JS、作成するものはジョブボードである。

前提

対象とする読者

目的

免責事項

Vue.JSとは

Vue.JSは人気のあるライブラリで、日本語ドキュメントも大変充実している。

Reactと比べるとローカライズが進んでおり、日本人のコントリビューターも多い印象。

主な特徴は以下の通り。

さて、以上で前提を終える。これだけだとなんのこっちゃという感じになってしまうので、次節以降で具体的に作り始めていこう。とはいえ、いきなりジョブボードを作り始めるのは敷居が高いので、まずは設計を固めた上で「投稿のリストとその操作」という単純なものから作り始めたい。

ジョブボードの設計

設計といってもしっかりしたものは次回以降に送り、次のような要件を念頭に置いて始めよう。

これらを踏まえると、次のような機能が想定されるだろう。

表示機能

編集機能

今回はとりあえずこんなレベルから始めよう。WordPressでこれらをどう実装するかをちょっと考えると……

表示に関しては通常のWordPressテーマを作成するのと同じ方法を使う。

といった実装になるだろう。この際、二番目の「寄稿者専用編集画面」というのがハードルが高そうだ。ここをVue.JSで実装しよう。

余談: Vue.JS使う意味ある?

「○○を使う意味は本当にあるのか?」という問いを投げられると、筆者のように哲学を真剣に学んだことがある人間は「本質的に○○でなければならないということは絶対に言えない」などと弱気な返答をしてしまうのだが、今回の場合に限ってはVue.JSで専用の編集画面を作る意味がある。

なぜかというと、WordPressの投稿は条件付きの編集画面を作るのが大変なのである。

WordPressには投稿編集画面が備わっているが、そのUIはすべてを一発で作成する巨大なフォームである。Gutenbergにおいてもこのアイデアは本質的に変わっていない。権限に関しても同様であり、「特定の投稿タイプを編集できるか?」という権限設定は簡単だが、もう少し複雑な条件が絡んでくると、とたんに難しくなる。具体的な例を挙げよう。

また、すべてが一画面にあることによって、「情報の重み付けが難しい」というのも投稿を作成する人が抱きがちな感想である。給料を払っているのであれば、「うるせえ、こっちは金払ってんだ、使い方を覚えろ!」と恫喝することもできるが、UGC(=User Generated Contents) サイトの場合はそうもいかないだろう。

というわけで、今回の「求人票」のように複雑な条件を備える投稿タイプについては、専用の画面を用意するのは悪いアイデアではない。次節以降で「寄稿者専用管理画面」を作成しよう。

Vue.JSによる寄稿者専用管理画面の作成

それでは、基本的なWordPressの知識をはしょるが、プラグインまたはテーマに以下の処理を書き込むことからスタートする。

ステップ1:投稿タイプの作成

まずは投稿タイプを作成する。上述した通り、固定ページと同種の投稿タイプを作成すれば、「編集者以上はいじることができるが、寄稿者はいじれない投稿タイプ」が完成する。コードは次の通り。

// Add post type.
add_action( 'init', function() {
  register_post_type( 'recruitment', [
    'label'  => '求人票',
    'public' => true,
    'capability_type' => 'page',
    'menu_icon' => 'dashicons-groups',
    'supports' => [ 'title', 'editor', 'author', 'thumbnail' ],
  ] );
} );

ブラウザのシークレットモードなどを活用し、二つの権限でログインして画面を見比べて欲しい。

ご覧の通り、寄稿者は「求人票」にアクセスできない

ステップ2:寄稿者専用編集画面の作成

寄稿者専用のメニューを追加し、空っぽのページを用意する。コードは次の通り。

// Add menu.
add_action( 'admin_menu', function() {
  add_menu_page( 'job-board', 'ジョブボード', 'contributor', 'job-board', function() {
    ?>
    <div class="wrap">
      <h2>ジョブボード</h2>			
    </div>
    
    <?php
  }, 'dashicons-groups', 50 );
} );
こんなかんじの空っぽページが作成される。

ステップ3:Vue.JSを使ってみる

Vue.JSは「はじめる」に書いてある通り、特定の要素(普通はIDをふったdiv)にマウントすることでアプリを初期化する。

<!-- 上記の add_menu_page 内に追加 -->
<div class="wrap">
    <h2>ジョブボード</h2>
      
    <div id="job-board-container" class="jb-container">
    </div>
</div>

さらに、このページにJSを読み込もう。もちろん、依存関係にVue.JS本体を含めることを忘れずに。なお、今回は簡略化のためにCDNを使っているが、公式リポジトリに配信する場合はバンドルする必要がある。

add_action( 'admin_enqueue_scripts', function( $page ) {
  if ( 'toplevel_page_job-board' !== $page ) {
    return;
  }
  wp_enqueue_script( 'vue-js', 'https://cdn.jsdelivr.net/npm/vue', [], 'latest', true );
  wp_enqueue_script( 'capitalp-job-board-admin', get_stylesheet_directory_uri() . '/assets/js/job-board-admin.js', [ 'jquery', 'vue-js' ], wp_get_theme()->get( 'Version' ), true );
} );

今回はCapital Pテーマの中のJavascriptファイルを読み込む形にした。JSの中にalertなどを書き込み、きちんと読み込まれているか確認しよう。jQueryも使うので、JSコード全体をクロージャで囲むことを忘れずに。

(function($){
  alert('よみこまれた?');
})(jQuery);
読み込み忘れがないよう、念のため確認しよう。

読み込まれていることが確認できたら、HTML(実際はPHP)とJSに次のようなコードを書いて、Vue.JSのバインドを確認しよう。

// Javascript
var app = new Vue({
    el: '#job-board-container',
    data: {
      message: 'こんにちは! ジョブボードです!'
    },
});
<div id="job-board-container" class="jb-container">
  {{message}}
</div>

HTML中のmessageが「こんにちは! ジョブボードです!」に変わったのがわかるだろうか。さらに、双方向バインディングを確認するために、次のようなコードにしてみよう。

<div id="job-board-container" class="jb-container">
  <p>
    {{message}}
  </p>
  <p>
    <input type="text" v-model="message"/>
  </p>
</div>

inputタグに入力したそばからデータが変更されるのがわかるはずである。

画像だと雰囲気は出ないが、実際にコードを書いてみるとリアルタイム更新の威力がわかるはずだ。

ステップ4:REST APIとの通信を作成

さて、では「求人票」のデータをどうやってとってくるかの? それはREST APIによるAjax通信である。これは以前紹介したチュートリアル「WordPressでREST APIから投稿させる機能を作る」で紹介したのと同様の方法ではあるが、今回は簡便化のために$.ajaxのラッパーを用意しよう。

/**
 * Ajax request.
 *
 * @param {String} method
 * @param {String} endpoint
 * @param {Object} args
 * @return $.ajax
 */
var restApi = function( method, endpoint, args ) {
  var url = JobBoardVars.endpoint + endpoint;
  method = method.toUpperCase();
  var config = {
    method    : method,
    beforeSend: function (xhr) {
      xhr.setRequestHeader('X-WP-Nonce', JobBoardVars.nonce);
    }
  };
  switch ( method ) {
    case 'POST':
    case 'PUSH':
      // Add data as post body.
      config.data = args;
      break;
    default:
      // Add query string.
      var queryString = [];
      for(var prop in args){
        if(args.hasOwnProperty(prop)){
          queryString.push(prop + '=' + encodeURIComponent(args[prop]));
        }
      }
      if(queryString.length){
        url += '?' + args;
      }
      break;
  }
  config.url = url;
  return $.ajax(config);
};

これで毎回のリクエストが楽になる。

ちなみに、Vue.JSに限らず、最近のモダンJavascript開発では、axiosfetch APIを使うことが多いようだが、新しい情報が増えると混乱するという判断からWordPress同梱のjQueryに含まれるAjax機能を使う。一応説明するが、基本は次の通り。

$.ajax(setting).done(function(response){
  // 成功した場合の処理
}).fail(function(response){
  // 失敗した場合の処理
}).always(function(){
  // すべてが終わった後にかならず実行される処理
});

以上でREST APIとの通信は可能になったが、肝心の受け取りがまだである。そこで、次のようなコードを書き、「現在ログインしているユーザーが寄稿者なら求人票をすべて返す」というAPIを作成しよう。

add_action( 'rest_api_init', function() {
  register_rest_route( 'job-board/v1', 'recruitment', [
    [
      'methods' => 'GET',
      'permission_callback' => function() {
        return current_user_can( 'contributor' );
      },
      'args' => [
        'page' => [
          'required' => false,
          'default'  => 1,
          'sanitize_callback' => function( $var ) {
            return max( 1, (int) $var );
          },
        ],
      ],
      'callback' => function( WP_REST_Request $request ) {
        $user_id = get_current_user_id();
        $posts = get_posts( [
          'author' => $user_id,
          'post_type' => 'recruitment',
          'posts_per_page' => 20,
          'post_status' => 'any',
          'paged' => $request->get_param( 'page' ),
          'orderby' => [
            'date' => 'DESC',
          ],
          'suppress_filters' => false,
        ] );
        return new WP_REST_Response( $posts );
      },
    ],
  ] );
} );

上記のエンドポイントが追加されているかどうかは、 example.com/wp-json/job-board/v1 にアクセスすることでわかる。

さら、以前説明した通り、nonceが設定されていない場合、ログインしたユーザーと認められないので、こちらもJS経由で渡すようにする。

wp_localize_script( 'capitalp-job-board-admin', 'JobBoardVars', [
  'endpoint'  => rest_url(),
  'nonce' => wp_create_nonce( 'wp_rest' ),
] );

これでJSからJobBoardVarsを参照すると、エンドポイントやnonceが取得できるようになる。

さて、次節以降はシークレットウィンドウなどで寄稿者としてログインするようにしてほしい。「現在のユーザー」が重要になるからである。

ステップ5:投稿の一覧を作成

まずは、受け取り部分を作成しよう。データはVueアプリケーションのrecruitementというプロパティに保存されるようにする。これは配列である。

var app = new Vue({
    el: '#job-board-container',
    data: {
      recruitment: []
    },
});

そして、HTML側を次のように変更する。

<div id="job-board-container" class="jb-container">
  <div v-if="!recruitment.length">
    データがありません。
  </div>
  <div v-if="recruitment.length">
    <ul>
      <li v-for="item in recruitment">
        #{{item.ID}} <strong>{{item.post_title}}</strong>
        <p>
          <button type="button>編集</button>
          <button type="button>削除</button>
        </p>
      </li>
    </ul>
  </div>
</div>

v-forは繰り返しのためのディレクティブだ。recruitmentの数だけリピートされるのだが、いまは空っぽなのでリピートされない。

それでは、画面が読み込まれると同時に、REST APIにAjax通信を行い、投稿のリストを取得するようにしよう。この場合はVueアプリケーションのライフサイクルを利用し、マウント(アプリの初期化プロセスの一部)された瞬間にAjax通信を行う。

var app = new Vue({
  el: '#job-board-container',
  data: {
    recruitment: [],
  },
  mounted: function () {
    // $.ajaxの場合、thisのスコープが失われてしまうのでselfに保存する。
    var self = this;
    restApi('GET', 'job-board/v1/recruitment', {}).done(function(response){
      self.recruitment = response;
    }).fail(function(response){
      alert('error');
      console.log(response);
    });
  }
});

これで次のようなリストが取得できるはずだ。

投稿がまだ作成されていない場合は表示されないので、管理者アカウントで作成しておこう。

ステップ6:追加と削除

さて、それでは追加のアクションを作成しよう。これはVueアプリケーションのmethodsというものを利用する。まず、次のようにREST APIを追加し、新しいタイトルで新たな求人票を作成できるようにしよう。

add_action( 'rest_api_init', function() {
  
  register_rest_route( 'job-board/v1', 'recruitment', [
    // すでに追加したGETは略
    [
      'methods' => 'POST',
      'permission_callback' => function() {
        return current_user_can( 'contributor' );
      },
      'args' => [
        'title' => [
          'required' => true,
          'validate_callback' => function( $var ) {
            return ! empty( $var );
          },
        ],
      ],
      'callback' => function( WP_REST_Request $request ) {
        $title = $request->get_param( 'title' );
        $result = wp_insert_post( [
          'post_type'   => 'recruitment',
          'post_status' => 'draft',
          'post_author' => get_current_user_id(),
          'post_title'  => $title,
        ], true );
        if ( is_wp_error( $result ) ) {
          return $result;
        } else {
          return new WP_REST_Response( get_post( $result ) );
        }
      }
    ],
  ] );
});

このAPIでは、求人票を追加し、その投稿オブジェクトを返す。続いて、このAPIを叩くメソッドをJSに追加しよう。

var app = new Vue({
  el: '#job-board-container',
  data: {
    recruitment: [],
    newTitle: ''
  },
  methods: {
    addNew: function(){
      var self = this;
      restApi('POST', 'job-board/v1/recruitment/', {
        title: this.newTitle
      }).done(function(response){
        self.recruitment.push(response);
        self.newTitle = '';
      }).fail(function(response){
        alert('失敗しました。');
        console.log(response);
      });
    }
  }
});

addNewはREST APIにPOSTリクエストを送ってデータを作成するとともに、その戻り値(投稿オブジェクト)を求人票のリストrecruitmentに追加している。

続いて、ユーザーインターフェースの作成に移る。新しいプロパティnewTitleが追加されたので、送信用のフォームにバインドしよう。

<div class="wrap">
  <h2>ジョブボード</h2>
  
  <div id="job-board-container" class="jb-container">
    <p>
      <input type="text" v-model="newTitle"/>
      <button type="button" v-on:click="addNew">新規追加</button>
    </p>
  </div>
  <!-- 中略 -->
</div>

これでボタンをクリックするたびに新しい投稿がリストに追加される。では、続いて「削除」を実装しよう。このために、REST APIを追加する。

add_action( 'rest_api_init', function() {
  register_rest_route( 'job-board/v1', 'recruitment/(?P<id>\d+)', [
    [
      'methods' => 'DELETE',
      'permission_callback' => function( WP_REST_Request $request ) {
        $post_id = $request->get_param( 'id' );
        $post = get_post( $post_id );
        if ( ! $post || ( 'recruitment' !== $post->post_type ) || ( get_current_user_id() != $post->post_author ) ) {
          return new WP_Error( 'permission_error', 'この求人票を修正する権限がありません。', [
            'status' => 403,
          ] );
        }
        return true;
      },
      'args' => [
        'id' => [
          'required' => true,
          'validate_callback' => function( $var ) {
            return is_numeric( $var ) ?: new WP_Error( 'invalid_argument', 'IDは数字です。', [
              'status' => 400,
            ] );
          },
        ],
      ],
      'callback' => function( WP_REST_Request $request ) {
        $post_id = $request->get_param( 'id' );
        $result = wp_delete_post( $post_id, true );
        if ( ! $result ) {
          return new WP_Error( 'failed_delete', '削除に失敗しました。', [
            'status' => 500,
          ] );
        } else {
          return new WP_REST_Response( [
            'success' => true,
            'message' => '求人票を削除しました。',
          ] );
        }
      },
    ],
  ] );
} );

このAPIを叩くと、投稿が削除される。それでは、JSにもメソッド removePost を実装しよう。

methods: {
  removePost: function(id){
    if(!confirm('本当に削除してよろしいですか?')){
      return;
    }
    var self = this;
    restApi('DELETE', 'job-board/v1/recruitment/' + id, {}).done(function(){
      var index;
      for(var i = 0; i < self.recruitment.length; i++){
        if(id == self.recruitment[i].ID){
          index = i;
          break;
        }
      }
      self.recruitment.splice(index, 1);
    }).fail(function(response){
      alert('失敗しました。');
      console.log(response);
    });
  }
}

このJSをボタンから呼び出せば完了だ。

<p>
  <button type="button">編集</button>
  <button type="button" v-on:click="removePost(item.ID)">削除</button>
</p>

ステップ7:そして編集へ

さて、これでVue.JSの基本的な動作を学んだので、次回以降、ウィザード式の編集スクリーン作成へと以降しよう。

以下、専用コンテンツとしては以下のものを提供している。

ただ、お詫びせねばならないのだが、ライブコーディングのビデオを1.5時間録画したのだが、35分ほどで画面が真っ黒になっていることが判明した。最近、ThunderBoldディスプレイの調子が悪く、途切れてしまったようである。この点に関しては伏してお詫び申しあげるので、なにとぞご寛恕いただきたい。

とはいえ、最初の設定の部分が紹介されているので、ちんぷんかんぷんだったという人には大いなる助けになると思う。

この投稿の続きを読むためには、Capital Pでログインする必要があります。

続きを読む
モバイルバージョンを終了