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

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

前提

対象とする読者

  • WordPressを使ってテーマカスタマイズを行ったことがある人。
  • 基礎的なJavascriptの知識(変数の取り扱い、文法)がある人。ES2015以降の新しい記法については知らなくても構わないが、if-elseも書いたことがないというレベルだと厳しいかもしれない。

目的

  • WordPressをアプリケーションバックエンドとして、Webアプリケーションを作成する。

免責事項

  • WordPressにはジョブボードプラグインがすでに存在するので、「手っ取り早くジョブボードを実装したい」という人に本チュートリアルは向かない。

Vue.JSとは

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

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

主な特徴は以下の通り。

  • テンプレート構文を利用して、DOMを描画することができる。
  • データとDOMをバインディングすることができる。
  • 再利用可能なコンポーネントを作ることができる。

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

ジョブボードの設計

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

  • ジョブボードとは、求人票の一覧画面である。
  • ユーザーは求人票を見て、応募することができる。
  • 求人票には職種、要件、募集期限、人数などがある。
  • 求人票を作成できるのは、Capital Pの有料会員のみ。一度に作成できるのは一件まで。
  • 一度公開した求人票は一部(タイトルのみ?)しか編集できない。
  • 求人票には条件(ex. WordPressに関係する仕事)があり、管理人の許可を経てからリリースされる。

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

表示機能

  • ジョブボードページ。絞り込み検索などができるとなおよし。しかし、そんなにたくさん掲載されない気もするので、とりあえずこのようなフィルタリングができればよいだろう。
  • 求人票。求人の詳細が書いてあり、なおかつ応募者がそこから応募できる。次回以降で詳しく説明していくが、このユーザーアクションによって成果報酬(=人を採用できたことへの報酬)やコンタクトポイントの追加(=似たような仕事があったら連絡する)といった導線が作られる。

編集機能

  • 寄稿者のための求人票作成画面。寄稿者と管理者は投稿タイプ「求人票」に対してまったく同じ権限を持たない。一度に一つしか作ることができない、特定の情報(ex. 公開期限)についてはいじることができないなど、様々な制限を持つ。
  • 管理者のための求人票編集画面。管理者は寄稿者がおこなる全ての操作に加え、すべてを操作する権限を持つ。たとえば、違法な求人(ex. 売春を行うサイトをWordPressで作成し、その売春婦を募集する)があったときに、それを速やかに停止できなければならない。

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

  • 求人票はカスタム投稿タイプで実装する。
  • 管理者は通常のWordPress管理画面から編集できるようにする。このケースでは普通の固定ページ(編集者以上しかいじれない投稿タイプ)と同じ実装でよいだろう。
  • 寄稿者は通常の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ディスプレイの調子が悪く、途切れてしまったようである。この点に関しては伏してお詫び申しあげるので、なにとぞご寛恕いただきたい。

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

続きの 5% を読み、添付されたファイルにアクセスするには、Gumroadでライセンスキーを取得してください!

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください