WordPress Logo WordPress で AJAX

WordPress で AJAX を使う方法について。

基本的な使い方や投稿を追加で読み込むサンプルなどを掲載しています。AJAX は jQuery 以外にも XMLHttpRequest や Fetch API を使ったサンプルも掲載しています。

関連ページ:

更新日:2024年01月18日

作成日:2023年6月19日

WordPress AJAX の概要

一般的な AJAX ではレスポンスを返す処理やデータが記述されたファイルにリクエストを送信しますが、WordPress の場合、AJAX のリクエストは /wp-admin/admin-ajax.php に送信します。

WordPress は admin-ajax.php でリクエストを受け取ると、実行する処理を特定するためのアクション名を $_REQUEST['action'] で取得して、AJAX 用のアクションを呼び出して処理を実行します。

そのため、AJAX のリクエストにはキー名が action で値がアクション名(呼び出すアクションを識別する値)のパラメータを含めて、/wp-admin/admin-ajax.php に送信する必要があります。

以下は WordPress で AJAX を利用する基本的な使い方の概要です。

  1. JavaScript ファイルを WordPress に登録する際に AJAX の送信先 URL を JavaScript に渡す

  2. AJAX のリクエストに AJAX 用アクションを特定するためのアクション名を含むパラメータを指定

  3. リクエストに対して実行する関数(AJAX ハンドラ)を定義して AJAX 用のアクションに登録

以下はボタンをクリックすると入力された値を AJAX で送信して、サーバー側でその値に Hello という文字列を追加して返す例です。

テンプレートファイルに入力用の input 要素と button 要素を記述します。
<div>
  <label for="name">名前: </label>
  <input id="name" value="" size="20">
  <button id="js-btn" type="button">送信</button>
</div>

AJAX の JavaScript ファイルの登録と 送信先 URL の出力

以下を functions.php に記述します。

wp_enqueue_script() で JavaScript ファイルを登録し、その際に wp_add_inline_script() を使って AJAX のリクエスト送信先の URL と nonce の値をページの script タグに出力します。

nonce はセキュリティ対策用の値です。

functions.php
function my_enqueue_ajax_scripts() {

  // AJAX を記述する JavaScript ファイルの登録
  wp_enqueue_script(
    'my-ajax-script',
    get_theme_file_uri('/js/my-script.js'),
    array('jquery'), // jQuery を使わない場合は空の配列
    filemtime(get_theme_file_path('/js/my-script.js')),
    true
  );

  // AJAX のリクエスト送信先 URL と nonce を出力
  wp_add_inline_script(
    // 上記 JavaScript のハンドル名
    'my-ajax-script',
    // script タグに出力する内容(変数名 my_ajax_params に設定したオブジェクト)
    'const my_ajax_params = '.json_encode( array(
      'ajaxurl' => admin_url('admin-ajax.php'),
      'my_ajax_nonce' => wp_create_nonce('my-ajax-nonce'),
    )),
    // 上記内容の script タグを JavaScript の読み込みの前に出力
    'before'
  );
}
// wp_enqueue_scripts アクションに上記関数をフック
add_action('wp_enqueue_scripts', 'my_enqueue_ajax_scripts');

AJAX の JavaScript ファイルを作成

AJAX の処理を記述した JavaScript ファイルを作成します。

データには action というキーの値にアクション名を、_ajax_nonce というキーの値に nonce の値を指定したクエリパラメータを含めて送信します。

AJAX の送信先 URL と nonce の値は前述の wp_add_inline_script() で出力された値(変数 my_ajax_params)を参照します。

ボタンがクリックされるとデータが送信され、サーバー(WordPress)側ではアクション名から対応する AJAX アクションに登録された関数(今はまだ登録していない)を呼び出します。

jQuery(function($){
  $( '#js-btn' ).on( 'click', function(){
    $.ajax({
      type: 'POST',
      // 送信先(出力された変数 my_ajax_params の ajaxurl プロパティ)
      url: my_ajax_params.ajaxurl,
      // 送信するデータ
      data: {
        action : 'my_action', // アクション名
        _ajax_nonce: my_ajax_params.my_ajax_nonce, // nonce
        name: $( '#name' ).val(),
      }
    }).done( function( response ) {
      // レスポンスの取得成功時の処理
      console.log( response );
    }).fail( function( error ) {
      console.warn( `失敗: ${error.status} (${error.statusText})` );
    });
  });
});

サーバー側の処理(AJAX ハンドラの定義と AJAX アクションの登録)

以下を functions.php に記述して、リクエストに対して処理を実行する関数(AJAX ハンドラ)を定義して AJAX 用のアクションに登録します。

AJAX 用のアクションの名前には、AJAX のパラメータで指定したアクション名を末尾に指定します。

この例の場合、アクション名は my_action なので、ログインユーザー用のアクションは wp_ajax_my_action、非ログインユーザー用のアクションは wp_ajax_nopriv_my_action になります。

functions.php
// AJAX ハンドラの定義
function my_ajax_handler() {
  // nonce の値を検証
  check_ajax_referer('my-ajax-nonce');
  // POST 送信された値 $_POST['name'] をサニタイズ
  $name = esc_html($_POST['name']);
  // Hello と ! を前後に追加し、echo でレスポンスとして返す
  echo 'Hello '. $name . '!';
  // 完了したら wp_die() で終了
  wp_die();
}

// 上記で定義した AJAX ハンドラを AJAX アクションに登録
add_action('wp_ajax_my_action', 'my_ajax_handler');
add_action('wp_ajax_nopriv_my_action', 'my_ajax_handler');

ページに挿入した入力欄に文字列を入力してボタンをクリックすると、コンソールに Hello xxxx !(xxxx は入力された文字)と出力されます。

WordPress Developer Resources 参考ドキュメント

以下はそれぞれの手順の詳細です。また、jQuery を使わずに XMLHttpRequest や Fetch API を使って AJAX を実装する例もあわせて掲載しています。

AJAX の JavaScript ファイルを登録

AJAX の JavaScript ファイルを WordPress に登録し、その際に送信先 URL と nonce など JavaScript に渡す必要がある値をページの script タグに出力します。

functions.php
function my_enqueue_ajax_scripts() {

  // AJAX を記述する JavaScript ファイルの登録
  wp_enqueue_script(
    'my-ajax-script', // ハンドル名
    get_theme_file_uri('/js/my-script.js'),
    array('jquery'), // jQuery を使わない場合は空の配列 array() を指定
    filemtime(get_theme_file_path('/js/my-script.js')),
    true
  );

  // AJAX のリクエスト URL と nonce を出力
  wp_add_inline_script(
    // データを渡す対象の JavaScript のハンドル名(上記で登録したハンドル名を指定)
    'my-ajax-script',
    // script タグに出力する JavaScript(変数 my_ajax_params に格納されたオブジェクト)
    'const my_ajax_params = ' . json_encode(array(
      'ajaxurl' => admin_url('admin-ajax.php'),
      'my_ajax_nonce' => wp_create_nonce('my-ajax-nonce'),
    )),
    // script タグを対象のスクリプトの前に出力するための指定
    'before'
  );
}
// wp_enqueue_scripts アクションに上記関数をフックして AJAX ファイルを登録及び変数を出力
add_action('wp_enqueue_scripts', 'my_enqueue_ajax_scripts');

JavaScript ファイルを wp_enqueue_script() を使って wp_enqueue_scripts アクションで登録します。

wp_enqueue_script() は以下のパラメータを受け取ります。

  • $handle : スクリプトを識別するためのハンドル名
  • $src : スクリプトの URL
  • $deps : 依存するスクリプトのハンドル名を配列で指定(ない場合は空の配列を指定)
  • $ver : スクリプトのバージョン番号(上記では filemtime() を使ってファイルの更新日時を指定)
  • $in_footer : true の場合 </body> 終了タグの前に配置

wp_enqueue_scripts アクションでは、wp_enqueue_script() による JavaScript ファイルの登録と一緒に wp_add_inline_script() などを使って送信先 URL と nonce の値を JavaScript に渡します。

リクエスト送信先 URL と nonce を出力

WordPress では AJAX のリクエスト送信先は /wp-admin/admin-ajax.php になります。

JavaScript ファイル内で上記のパスを動的に指定することはできないので、リクエスト送信先の URL を変数として出力して JavaScript に渡します。

この例では WordPress 4.5 で導入された wp_add_inline_script() を利用します。

wp_add_inline_script() はインラインの script タグに JavaScript コードを出力することで、登録された JavaScript に PHP 側からデータを渡すことができる関数です。

同様のことは wp_localize_script() を使ってもできますが、PHP 側から JavaScript にデータを渡す場合は wp_add_inline_script() の方が適しています。wp_add_inline_script/User Contributed Notes

wp_add_inline_script()

以下が wp_add_inline_script() の書式と引数です。

wp_add_inline_script( string $handle, string $data, string $position )
wp_add_inline_script() の引数
引数 説明
$handle データを渡す対象の JavaScript ファイルのハンドル名(JavaScript ファイルを wp_enqueue_script() で登録する際に第1引数に指定したハンドル名)
$data JavaScript のコードなどのデータを表す文字列
$position script タグを $handle で指定したファイルの読み込みの前に出力する場合は before、後に出力する場合は after を指定。省略時(デフォルト)は after
wp_create_nonce()

リクエスト先 URL を出力する際に、セキュリティ対策用の nonce も生成して出力します。

nonce を設定することで AJAX のリクエストが不正なものかどうかを検証することができるので、セキュリティ対策として設定することが望ましいです。

nonce の生成には wp_create_nonce() を使います。以下が書式です。

wp_create_nonce( string|int $action )
wp_create_nonce() の引数
引数 説明
$action アクション文字列(ユニークな文字列を指定)。デフォルト: -1

wp_create_nonce() は使い捨てのランダムな値(暗号化トークン)を生成して返します。

AJAX の場合、JavaScript 側でこの値(暗号化トークン)を送信し、PHP 側で check_ajax_referer()に引数のアクション文字列を指定して検証します。

wp_add_inline_script() により 出力される script タグ

以下のような script タグが、登録した JavaScript の読み込みの前に出力されます。

<!--出力される script タグ-->
<script id='my-script-js-before'>
  const my_ajax_params = {
    "ajaxurl":"http:\/\/localhost\/wp\/wp-admin\/admin-ajax.php",
    "my_ajax_nonce":"697985ac05"
  }
</script>
<!--登録したJavaScriptの読み込み-->
<script src='http://localhost/wp/wp-content/themes/my-theme/js/my-script.js?ver=1686966413' id='my-script-js'></script>;

これにより JavaScript では変数(オブジェクト)my_ajax_params 経由で設定した値を参照できます。

上記の場合、リクエスト URL は my_ajax_params.ajaxurl で 、nonce の値は my_ajax_params.my_ajax_nonce でアクセスできるようになります。

値を PHP から JavaScript に渡す

リクエスト送信先の URL や nonce などの値を PHP から JavaScript に渡す方法としては、上記以外にも以下のような方法もあります。

特定のページにのみ登録(出力)

上記の場合、すべてのページで AJAX の JavaScript が読み込まれ、script タグが出力されます。

必要に応じて、条件分岐タグを使って特定のページでのみ AJAX の JavaScript を読み込み、script タグを出力することができます。

function my_enqueue_ajax_scripts() {

  // フロントページの場合にのみ AJAX の JavaScript を読み込み、script タグを出力
  if(is_front_page()) {

    wp_enqueue_script(
      'my-ajax-script',
      get_theme_file_uri('/js/my-script.js'),
      array('jquery'),
      filemtime(get_theme_file_path('/js/my-script.js')),
      true
    );
    wp_add_inline_script(
      'my-ajax-script',
      'const my_ajax_params = ' . json_encode(array(
        'ajaxurl' => admin_url('admin-ajax.php'),
        'my_ajax_nonce' => wp_create_nonce('my-ajax-nonce'),
      )),
      'before'
    );
  }
}
add_action('wp_enqueue_scripts', 'my_enqueue_ajax_scripts');

すべてのページで AJAX を利用する以外では、存在しない要素に Javascript でアクセスしてエラーなどにならないように、必要なページでのみ JavaScript を読み込むようにします。

JavaScript 側の処理(AJAX)

AJAX 側ではアクション名と nonce のクエリパラメータを含む必要なデータを送信し、レスポンスの取得に成功したら何らかの処理を実行します。

以下は jQuery の $.ajax() メソッドを利用して POST メソッドで送信する例です。

/js/my-script.js
jQuery(function($){
  $( '#js-btn' ).on( 'click', function(){
    $.ajax({
      // 使用するHTTPメソッド
      type: 'POST',
      // 送信先( script タグに出力された値を参照)
      url: my_ajax_params.ajaxurl,
      // 送信するデータ(クエリパラメータ)を指定
      data: {
        action : 'my_action', // アクション名
        _ajax_nonce: my_ajax_params.my_ajax_nonce, // nonce(出力された値を参照)
        name: $( '#name' ).val(), // 入力された値
      }
    }).done( function( response ) {
      // 成功時の処理
      console.log( response );
    }).fail( function( error ) {
      // 失敗時の処理
      console.warn( `失敗: ${error.status} (${error.statusText})` );
    });
  });
});

送信するデータを別途定義して、以下のように記述することもできます。

jQuery(function($){
  $( '#js-btn' ).on( 'click', function(){

    // 送信するデータ
    const data = {
      action : 'my_action', // アクション名
      _ajax_nonce: my_ajax_params.my_ajax_nonce, // nonce(出力された値を参照)
      name: $( '#name' ).val(), // 入力された値
    };

    $.ajax({
      // 使用するHTTPメソッド
      type: 'POST',
      // 送信先( script タグに出力された値を参照)
      url: my_ajax_params.ajaxurl,
      // 上記で定義したデータをリクエストボディに指定
      data,  //data: data と同じこと(プロパティの短縮構文)
    }).done( function( response ) {
      // 成功時の処理
      console.log( response );
    }).fail( function( error ) {
      // 失敗時の処理
      console.warn( `失敗: ${error.status} (${error.statusText})` );
    });
  });
});

リクエスト送信先

url に指定する送信先は wp_add_inline_script() により script タグに出力される値を参照します。

アクション名と nonce

送信するデータには以下のキー名のパラメータを含めます。

キー名
action アクション名(AJAX アクションを特定するための文字列)
_ajax_nonce

wp_create_nonce() で生成した nonce の値(script タグに出力された値を参照)。

キー名をデフォルトの _ajax_nonce 以外にする場合は、サーバー側の検証で check_ajax_referer() の第2引数にそのキー名の文字列を指定します。

リクエストが /wp-admin/admin-ajax.php に送信されると、サーバー側(WordPress)では $_REQUEST['action'] の値から対応する AJAX アクションを呼び出し、 $_REQUEST['_ajax_nonce'] の値でそのリクエストが不正なものでないかを検証します。

$_REQUEST['action'] の値のアクション名に対応する AJAX アクションが存在しない(アクション名が正しくない)場合は、 400 Bad Request エラーになります。

また、$_REQUEST['_ajax_nonce'] の値が正しくない場合は、AJAX ハンドラに記述されている check_ajax_referer() による検証で、403 (Forbidden) エラーになります。

レスポンスの取得

レスポンスの取得が成功した場合の処理は、done() メソッドや success オプションに記述し、失敗した場合の処理は fail() メソッドや error オプションに記述します。

jQuery $.post() メソッド

$.ajax() のショートハンドメソッド $.post() を使って以下のように記述することもできます。

jQuery(function($){
  $( '#js-btn' ).on( 'click', function(){

    // 送信するデータ
    const data = {
      action : 'my_action', // アクション名
      _ajax_nonce: my_ajax_params.my_ajax_nonce, // nonce(出力された値を参照)
      name: $( '#name' ).val(), // 入力された値
    };

    // $.post を使って送信
    $.post( my_ajax_params.ajaxurl, data )
      .done( function( response ) {
        // 成功時の処理
        console.log( response );
      }).fail( function( error ) {
        // 失敗時の処理
        console.warn( `失敗: ${error.status} (${error.statusText})` );
      });
  });
});

jQuery を使わずに AJAX を記述する方法は後述の jQuery を使わない場合 を御覧ください。

サーバー側の処理

サーバー側ではリクエストを受け取ると、アクション名に対応する AJAX 用のアクション(AJAX アクション)を呼び出すことで、そのアクションに定義された関数(AJAX ハンドラ)が呼び出されます。

そのため、functions.php にリクエストに対して処理を行う関数(AJAX ハンドラ)を定義して、AJAX 用のアクション(AJAX アクション)に登録します。

functions.php
// AJAX ハンドラの定義
function my_ajax_handler() {
  // nonce の値を検証
  check_ajax_referer('my-ajax-nonce');
  // POST 送信された値 $_POST['name'] をサニタイズ
  $name = esc_html($_POST['name']);
  // Hello と ! を前後に追加してレスポンスとして返す
  echo 'Hello '. $name . '!';
  // 完了したら wp_die() で終了
  wp_die();
}

// 上記で定義した AJAX ハンドラを AJAX アクションに登録
add_action('wp_ajax_my_action', 'my_ajax_handler');
add_action('wp_ajax_nopriv_my_action', 'my_ajax_handler');

AJAX ハンドラの定義

AJAX ハンドラでは、受け取ったリクエストに対して何らかの処理を行い、必要に応じて echo などで値を出力することでレスポンスを返す処理を記述します。

この例では、キー名が name で POST された値を $_POST['name'] で受け取り、esc_html() でエスケープ処理して、文字列を追加して echo でレスポンスとして返しています。

nonce を検証

AJAX ハンドラの定義では、まず最初に AJAX で送信されたデータの nonce の値を検証します。

nonce の検証には check_ajax_referer() を使います。

check_ajax_referer()

check_ajax_referer() は AJAX リクエスト時に nonce を検証する関数です。

以下が check_ajax_referer() の書式と引数です。

check_ajax_referer( int|string $action, false|string $query_arg, bool $stop )
check_ajax_referer() の引数
引数 説明
$action nonce 生成時の文字列または数値(省略時は-1)。wp_create_nonce() の引数に指定した文字列を指定します。
$query_arg AJAX リクエストの nonce のクエリ名(キー名)。リクエストで送信する際に nonce のキー名に指定した値。省略した場合は、$_REQUEST['_ajax_nonce'] と $_REQUEST['_wpnonce'] が順番に検索されます。言い換えると、nonce のキー名を _ajax_nonce とした場合は省略できます。
$stop nonce の検証結果が無効な場合に強制終了するかどうかの真偽値を指定。デフォルトは true で、403 (Forbidden) を返して強制終了します。
戻り値
nonce が有効で 0~12 時間前に生成された場合は 1、nonce が有効で 12~24 時間前に生成された場合は 2。nonce が無効な場合は False。

この例の場合、wp_create_nonce('my-ajax-nonce') として nonce の値を生成しているので、check_ajax_referer() の第1引数に 'my-ajax-nonce' を指定して nonce を検証します。

// nonce の値を検証
check_ajax_referer('my-ajax-nonce');

上記の場合、第2引数を省略しているので、AJAX リクエストの nonce の値のクエリ名(キー名)は '_ajax_nonce' を指定します。

第2引数を指定した場合は、その値を AJAX リクエストの nonce の値のクエリ名(キー名)とします。

また、上記の場合、第3引数を省略しているので、検証が無効な場合は処理は終了します。

wp_die() で終了

すべてのタスクを完了したら終了する必要があります。

WP_Ajax_Response や wp_send_json() などの wp_send_json* 関数を使用している場は自動的に終了処理されますが、そうでない場合は wp_die() を使用して終了する必要があります。

functions.php
// AJAX ハンドラの定義
function ajax_function() {
  // nonce の値を検証
  check_ajax_referer('my-ajax-nonce');

  // 必要な処理を記述(echo などで出力することでレスポンスを返すことができます)

  // 処理が完了したら wp_die() で終了
  wp_die();
}

AJAX アクションの登録

AJAX 用のアクション(AJAX アクション)には、ログインユーザー用と非ログインユーザー用の以下の2種類が用意されています。

wp_ajax_{$action} と wp_ajax_nopriv_{$action} の {$action} に任意のアクション名を指定します。

アクション名

{$action} の部分に独自のアクション名を指定して、AJAX アクションを識別できるようにします。

アクション名は AJAX のリクエストデータの action キーに指定した文字列になります。

アクション名を設定した AJAX アクションに AJAX ハンドラを add_action() で登録します。

functions.php
// ログインユーザー用アクションに定義した AJAX ハンドラを登録
add_action('wp_ajax_アクション名', 'AJAX ハンドラ名');

// 非ログインユーザー用アクションに定義した AJAX ハンドラを登録
add_action('wp_ajax_nopriv_アクション名', 'AJAX ハンドラ名');

この例の場合、AJAX 側でアクション名を my_action としているので以下のようになります。

// wp_ajax_my_action に my_ajax_handler を登録
add_action('wp_ajax_my_action', 'my_ajax_handler');

// wp_ajax_nopriv_my_action に my_ajax_handler を登録
add_action('wp_ajax_nopriv_my_action', 'my_ajax_handler');

jQuery を使わない場合

jQuery を必要としないテーマでは、XMLHttpRequest や Fetch API を使って素の JavaScript で AJAX を記述すれば AJAX のためだけに jQuery を読み込む必要がなくなります。

依存するスクリプトで jquery を指定しない

jQuery を使わない場合、 wp_enqueue_script() を使った JavaScript の登録では、第3引数の $deps(依存するスクリプトのハンドル名)に空の配列を指定するなど、jquery を指定しなければ jQuery は読み込まれません。

functions.php
function my_enqueue_ajax_scripts() {

  // AJAX を記述する JavaScript ファイルの登録
  wp_enqueue_script(
    'my-ajax-script',
    get_theme_file_uri('/js/my-script.js'),
    array(), // 空の配列を指定
    filemtime(get_theme_file_path('/js/my-script.js')),
    true
  );

  wp_add_inline_script(
    ・・・中略・・・
  );
}
add_action('wp_enqueue_scripts', 'my_enqueue_ajax_scripts');

但し、使用しているプラグイン等が jQuery に依存している場合は、jQuery は読み込まれます。

要素が存在するかを判定

jQuery の場合、存在しない要素にリスナーを設定してもエラーにはなりませんが、素の JavaScript では存在しない要素にリスナーを設定したり、参照するとエラー(Uncaught TypeError)になります。

必要に応じて、条件分岐タグを使って特定のページで JavaScript を読み込むようにするか、JavaScript の記述で要素が存在するかを判定してから処理を実行します。

const btn = document.getElementById("js-btn");
const nameInput = document.getElementById("name");

// btn(#js-btn)の要素が存在すれば
if(btn) {
  btn.addEventListener("click", () => {
    const data = {
      action: "my_action",
      _ajax_nonce: my_ajax_params.my_ajax_nonce,
      name: nameInput.value,
    };
    const xhr = new XMLHttpRequest();
    xhr.open("POST", my_ajax_params.ajaxurl);
    xhr.addEventListener('readystatechange', ()=> {
      if(xhr.readyState === 4){
        if(xhr.status >= 200 && xhr.status < 300){
          console.log(xhr.responseText);
        }else{
          console.log(`失敗: ${xhr.status} (${xhr.statusText})`);
        }
      }
    });
    xhr.send(new URLSearchParams(data));
  });
}

※ 以降では対象の要素が存在することを前提にして、要素の存在を確認する記述は省略しています。

処理を関数にまとめる

処理を関数にまとめることで、JavaScript のグローバル変数を減らすことができます。

以下は前述の例を関数に書き換えた例です。このように記述すればグローバル変数を生成しません。

document.addEventListener('DOMContentLoaded', () => {
  // 定義した処理を呼び出す(ここの中の変数はローカル)
  mySetupAjaxAction();
});

// 処理を関数としてまとめる
function mySetupAjaxAction() {
  // ここの中の変数はローカル
  const btn = document.getElementById("js-btn");
  const nameInput = document.getElementById("name");

  if(btn) {
    btn.addEventListener("click", () => {
      const data = {
        action: "my_action",
        _ajax_nonce: my_ajax_params.my_ajax_nonce,
        name: nameInput.value,
      };
      const xhr = new XMLHttpRequest();
      xhr.open("POST", my_ajax_params.ajaxurl);

      xhr.addEventListener('readystatechange', ()=> {
        if(xhr.readyState === 4){
          if(xhr.status >= 200 && xhr.status < 300){
            console.log(xhr.responseText);
          }else{
            console.log(`失敗: ${xhr.status} (${xhr.statusText})`);
          }
        }
      });
      xhr.send(new URLSearchParams(data));
    });
  }
}

以下も DOMContentLoaded イベントのコールバック関数で処理を定義しているのでグローバル変数を生成しません。

document.addEventListener('DOMContentLoaded', () => {
  // ここの中の変数はローカル
  const btn = document.getElementById("js-btn");
  const nameInput = document.getElementById("name");

  if(btn) {
    btn.addEventListener("click", () => {
      const data = {
        action: "my_action",
        _ajax_nonce: my_ajax_params.my_ajax_nonce,
        name: nameInput.value,
      };
      const xhr = new XMLHttpRequest();
      xhr.open("POST", my_ajax_params.ajaxurl);

      xhr.addEventListener('readystatechange', ()=> {
        if(xhr.readyState === 4){
          if(xhr.status >= 200 && xhr.status < 300){
            console.log(xhr.responseText);
          }else{
            console.log(`失敗: ${xhr.status} (${xhr.statusText})`);
          }
        }
      });
      xhr.send(new URLSearchParams(data));
    });
  }
});

以下のように即時関数にしても、グローバル変数を生成しません。

// 即時関数として定義
(function() {
  // ここの中の変数はローカル
  const btn = document.getElementById("js-btn");
  const nameInput = document.getElementById("name");

  if(btn) {
    btn.addEventListener("click", () => {
      const data = {
        action: "my_action",
        _ajax_nonce: my_ajax_params.my_ajax_nonce,
        name: nameInput.value,
      };
      const xhr = new XMLHttpRequest();
      xhr.open("POST", my_ajax_params.ajaxurl);

      xhr.addEventListener('readystatechange', ()=> {
        if(xhr.readyState === 4){
          if(xhr.status >= 200 && xhr.status < 300){
            console.log(xhr.responseText);
          }else{
            console.log(`失敗: ${xhr.status} (${xhr.statusText})`);
          }
        }
      });
      xhr.send(new URLSearchParams(data));
    });
  }
})();

以下の場合、変数 foo はそれぞれローカルなので衝突しません。即時関数の方が先に実行されるので Foo の後に foo が出力されます。

document.addEventListener("DOMContentLoaded", function () {
  const foo = 'foo';
  console.log(foo);
});

(function() {
  const foo = 'Foo';
  console.log(foo);
})();

//Foo
//foo

但し、以降では関数にまとめる記述は省略しています。

XMLHttpRequest を使う

以下は AJAX を XMLHttpRequest で実装し、POST メソッドで送信する例です。

送信するデータはオブジェクト形式で作成し、URLSearchParams に変換して send() メソッドの引数(リクエストボディ)に指定しています。

js/my-script.js
// ボタン
const btn = document.getElementById("js-btn");
// 名前入力欄
const nameInput = document.getElementById("name");

// ボタンのイベントリスナ
btn.addEventListener("click", () => {

  // 送信するデータ(オブジェクト形式のパラメータ)
  const data = {
    // キー名に action を指定し、値にアクション名を指定
    action: "my_action",
    // キー名に _ajax_nonce を指定し、値にインライン script に出力された nonce を指定
    _ajax_nonce: my_ajax_params.my_ajax_nonce,
    // 名前入力欄に入力された値
    name: nameInput.value,
  };

  // XMLHttpRequest のインスタンスを生成
  const xhr = new XMLHttpRequest();

  // POST メソッドでインライン script に出力された値(リクエスト先 URL)に送信
  xhr.open("POST", my_ajax_params.ajaxurl);

  xhr.addEventListener('readystatechange', ()=> {
    if(xhr.readyState === 4){
      if(xhr.status >= 200 && xhr.status < 300){
        // 取得操作が完了し、成功したらレスポンスのテキストをコンソールに出力
        console.log(xhr.responseText);
      }else{
        // 失敗時の処理
        console.log(`失敗: ${xhr.status} (${xhr.statusText})`);
      }
    }
  });

  // オブジェクト形式のパラメータを URLSearchParams に変換してリクエストボディに指定
  xhr.send(new URLSearchParams(data));
});

FormData を使う

以下は上記のリクエストボディに指定するデータを FormData を使って書き換えた例です。

js/my-script.js
// ボタン
const btn = document.getElementById("js-btn");
// 名前入力欄
const nameInput = document.getElementById("name");

// ボタンのイベントリスナ
btn.addEventListener("click", () => {

  // FormData オブジェクトを生成
  const data = new FormData();
  // FormData オブジェクトの append() メソッドでパラメータ(キー/値のペア)を追加
  data.append("action", "my_action");
  data.append("_ajax_nonce", my_ajax_params.my_ajax_nonce);
  data.append("name", nameInput.value);

  // XMLHttpRequest のインスタンスを生成
  const xhr = new XMLHttpRequest();

  // POST メソッドでインライン script に出力された値(リクエスト先 URL)に送信
  xhr.open("POST", my_ajax_params.ajaxurl);

  xhr.addEventListener('readystatechange', ()=> {
    if(xhr.readyState === 4){
      if(xhr.status >= 200 && xhr.status < 300){
        // 取得操作が完了し、成功したらレスポンスのテキストをコンソールに出力
        console.log(xhr.responseText);
      }else{
        // 失敗時の処理
        console.log(`失敗: ${xhr.status} (${xhr.statusText})`);
      }
    }
  });

  // FormData オブジェクトをリクエストボディに指定
  xhr.send(data);
});

関連ページ:AJAX XMLHttpRequest の基本的な使い方

Fetch API を使う

以下は前述の XMLHttpRequest の例を fetch() で書き換えた例です。

送信するデータは、前述の例同様、オブジェクト形式で作成し、URLSearchParams に変換してリクエストボディに指定しています。

js/my-script.js
const btn = document.getElementById('js-btn');
const nameInput = document.getElementById("name");

btn.addEventListener('click', ()=> {

  // 送信するデータ(オブジェクト形式のパラメータ)
  const data = {
    // キー名に action を指定し、値にアクション名を指定
    action: "my_action",
    // キー名に _ajax_nonce を指定し、値にインライン script に出力された nonce を指定
    _ajax_nonce: my_ajax_params.my_ajax_nonce,
    // 名前入力欄に入力された値
    name: nameInput.value,
  };

  // 送信先に my_ajax_params.ajaxurl を指定
  fetch(my_ajax_params.ajaxurl, {
    // POST を指定
    method: 'POST',
    // データを URLSearchParams に変換してリクエストボディに指定
    body: new URLSearchParams(data)

  }).then((response) => {
    if(response.ok) {
      return response.text();
    } else {
      throw new Error(`失敗: ${response.status} ${response.statusText}`);
    }
  })
  .then((text) => {
    // レスポンスのテキストを出力(成功時の処理)
    console.log(text);
  })
  .catch((error) => {
    console.warn(error);
  });
});

FormData を使う

以下は上記を FormData オブジェクトを使って書き換えた例です。

js/my-script.js
const btn = document.getElementById("js-btn");
const nameInput = document.getElementById("name");

btn.addEventListener("click", () => {

  // FormData オブジェクトを生成
  const data = new FormData();
  // FormData オブジェクトの append() メソッドでパラメータを追加
  data.append("action", "my_action");
  data.append("_ajax_nonce", my_ajax_params.my_ajax_nonce);
  data.append("name", nameInput.value);

  fetch(my_ajax_params.ajaxurl, {
    method: "POST",
    // FormData オブジェクトをリクエストボディに指定
    body: data,
  })
    .then((response) => {
      if (response.ok) {
        return response.text();
      } else {
        throw new Error(
          `失敗: ${response.status} ${response.statusText}`
        );
      }
    })
    .then((text) => {
      console.log(text);
    })
    .catch((error) => {
      console.warn(error);
    });
});

キーと値のペアで指定

以下は、データを「キー名=値&キー名=値&...」形式のキーと値のペアで指定する例です。

この場合、リクエストヘッダーで Content-Type を application/x-www-form-urlencoded に指定します。

js/my-script.js
const btn = document.getElementById('js-btn');
const nameInput = document.getElementById("name");

btn.addEventListener('click', ()=> {

  // データを「キー名=値&キー名=値...」形式で指定
  const data =
    'action=' + encodeURIComponent('my_action') +
    '&_ajax_nonce=' + encodeURIComponent(my_ajax_params.my_ajax_nonce) +
    '&name=' + encodeURIComponent(nameInput.value);

  fetch(my_ajax_params.ajaxurl, {
    method: 'POST',
    // リクエストヘッダーで Content-Type を指定
    headers: {
      "Content-Type": 'application/x-www-form-urlencoded'
    },
    body: data,

  }).then((response) => {
    if(response.ok) {
      return response.text();
    } else {
      throw new Error(`失敗: ${response.status} ${response.statusText}`);
    }
  })
  .then((text) => {
    console.log(text);
  })
  .catch((error) => {
    console.warn(error);
  });
});

関連ページ:Fetch API fetch() の使い方

JSON データの送信(リクエスト)

通常の AJAX では JSON データをリクエストボディに指定して POST する場合、リクエストヘッダーで Content-Type に application/json を指定すれば送信できますが、WordPress の場合、400 (Bad Request) エラーになってしまいます。

例えば、前述の XMLHttpRequest の例を以下のように Content-Type に application/json を指定して、リクエストボディに JSON データを設定して送信するように書き換えるとエラーになります。

400 (Bad Request) エラーになる例(MLHttpRequest)
const btn = document.getElementById("js-btn");
const nameInput = document.getElementById("name");

btn.addEventListener("click", () => {

  const data = {
    action: "my_action",
    _ajax_nonce: my_ajax_params.my_ajax_nonce,
    name: nameInput.value,
  };
  // データを JSON に変換
  const json_data = JSON.stringify(data);

  const xhr = new XMLHttpRequest();
  xhr.open("POST", my_ajax_params.ajaxurl);

  // Content-Type に application/json を指定してリクエストを送信
  xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');

  xhr.addEventListener("readystatechange", () => {
    if (xhr.readyState === 4) {
      if (xhr.status >= 200 && xhr.status < 300) {
        console.log(xhr.responseText);
      }
    }
  });

  // JSON データをリクエストボディに指定
  xhr.send(json_data);
});
400 (Bad Request) エラーになる例(jQuery を使った場合)
jQuery(function($){
  $( '#js-btn' ).on( 'click', function(){

    const data = {
      action: "my_action",
      _ajax_nonce: my_ajax_params.my_ajax_nonce,
      name: $( '#name' ).val(),
    };
    const json_data = JSON.stringify(data);

    $.ajax({
      type: 'POST',
      url: my_ajax_params.ajaxurl,
      data: json_data,
      // コンテントタイプを application/json に指定
      contentType:'application/json; charset=utf-8',
    })
    .done( function( response ) {
      console.log( response );
    })
    .fail( function( error ) {
      console.warn( `失敗: ${error.status} (${error.statusText})` );
    });
  });
});

エラーの原因

PHP では コンテントタイプが application/x-www-form-urlencoded または multipart/form-data の場合にのみ POST されたデータを $_POST で受け取ることができます。それ以外のコンテントタイプ(application/json など)の場合、 POST されたデータを $_POST で受け取ることができません。

WordPress はリクエストされた AJAX アクションを呼び出すために $_POST['action'](実際には $_REQUEST['action'])からアクション名を取得する必要があります。

コンテントタイプが application/json で POST 送信した場合、PHP では $_POST['action'] で受け取れないため WordPress は $_REQUEST['action'] からアクション名を取得できず 400 Bad Request エラーをスローします(All AJAX requests return a 400 error)。

また、nonce の検証に使用する nonce の値を $_REQUEST['_ajax_nonce'] で取得できないので、上記の例の場合、403 Forbidden エラーにもなり得ます(実際には 400 Bad Request で処理が終了するので 403 Forbidden は発生しません)。

エラーを回避する方法

エラーを回避するには、アクション名や nonce のパラメータと共に JSON 形式のデータを1つのパラメータの値として送信するのが簡単です。

どうしてもコンテントタイプを application/json にして JSON データとして送信したい場合は、リクエスト URL にパラメータとしてアクション名と nonce を追加する方法もあります。

URLSearchParams として送信

以下は XMLHttpRequest で JSON データを URLSearchParams のパラメータの1つとして追加して送信する例です。

js/my-script.js
const btn = document.getElementById("js-btn");
const nameInput = document.getElementById("name");

btn.addEventListener("click", () => {

  // JSON 形式に変換するデータ
  const input = {
    name: nameInput.value,
  };
  // JSON 形式に変換
  const json_input = JSON.stringify(input);

  // オブジェクト形式のデータ(送信する際に URLSearchParams で変換)
  const data = {
    action: "my_action",
    _ajax_nonce: my_ajax_params.my_ajax_nonce,
    // JSON 形式のデータ(サーバーでは $_POST['input'] で取得)
    input: json_input,
  };

  const xhr = new XMLHttpRequest();
  xhr.open("POST", my_ajax_params.ajaxurl);

  xhr.addEventListener("readystatechange", () => {
    if (xhr.readyState === 4) {
      if (xhr.status >= 200 && xhr.status < 300) {
        console.log(xhr.responseText);
      }
    }
  });

  // データ全体を URLSearchParams としてリクエストボディに指定して送信
  xhr.send(new URLSearchParams(data));
});

FormData として送信

以下は、前述の例を FormData として送信するように書き換えたものです。

js/my-script.js
const btn = document.getElementById("js-btn");
const nameInput = document.getElementById("name");

btn.addEventListener("click", () => {

  // JSON 形式に変換するデータ
  const input = {
    name: nameInput.value,
  };
  // JSON 形式に変換
  const json_input = JSON.stringify(input);

  // FormData オブジェクトを生成
  const data = new FormData();
  // FormData オブジェクトの append() メソッドでパラメータを追加
  data.append("action", "my_action");
  data.append("_ajax_nonce", my_ajax_params.my_ajax_nonce);
  // JSON 形式のデータ
  data.append("input", json_input);

  const xhr = new XMLHttpRequest();
  xhr.open("POST", my_ajax_params.ajaxurl);

  xhr.addEventListener("readystatechange", () => {
    if (xhr.readyState === 4) {
      if (xhr.status >= 200 && xhr.status < 300) {
        console.log(xhr.responseText);
      }
    }
  });

  // FormData オブジェクトをリクエストボディに指定して送信
  xhr.send(data);
});

jQuery の場合

以下は jQuery の例です。単に値に JSON 形式のデータを指定するだけです。

js/my-script.js
jQuery(function($){
  $( '#js-btn' ).on( 'click', function(){

    // JSON 形式に変換するデータ
    const input = {
      name: $( '#name' ).val(),
    };
    // JSON 形式に変換
    const json_input = JSON.stringify(input);

    $.ajax({
      type: 'POST',
      //インライン script に出力された値(リクエスト先 URL)に送信
      url: my_ajax_params.ajaxurl,
      data: {
        // キー名に action 、値にアクション名を指定
        action : 'my_action',
        // キー名に _ajax_nonce、値にインライン script に出力された nonce を指定
        _ajax_nonce: my_ajax_params.my_ajax_nonce,
        // JSON 形式のデータ(サーバーでは $_POST['input'] で取得)
        input: json_input,
      }
    })
    .done( function( response ) {
      console.log( response ); //成功した時の処理
    })
    .fail( function( error ) {
      console.warn( `失敗: ${error.status} (${error.statusText})` );
    });
  });
});

サーバー側(AJAX ハンドラ)

上記の例の場合、AJAX ハンドラでは送信された JSON データは $_POST['input'] で取得できます。

但し、WordPress では、$_POST などのすべてのグローバル入力変数にバックスラッシュを自動的に追加するため、この例の $_POST['input'] の値は、"{\"name\":\"入力された値\"}" のように不要なバックスラッシュが追加されていて、json_decode() を適用すると null になってしまいます。

そのため、json_decode() で PHP の値に変換する前に wp_unslash() を使って不要なバックスラッシュを取り除く必要があります。

wp_unslash() は再帰的な関数で、配列が指定されるとすべてのサブ配列内のスラッシュも削除してくれます。内部的には PHP の stripslashes() を使ってバックスラッシュを取り除いています。

functions.php
function ajax_function() {
  // nonce の値を検証
  check_ajax_referer('my-ajax-nonce');
  // PHP の値に変換
  $json = json_decode( wp_unslash($_POST['input']) );
  // 値をサニタイズ
  $name = esc_html($json->name);
  // テキストをレスポンスとして返す
  echo 'Hello ' . $name . '!';
  //処理を終了させる
  wp_die();
}
add_action('wp_ajax_my_action', 'ajax_function');
add_action('wp_ajax_nopriv_my_action', 'ajax_function');

以下のように json_decode() の第2引数に true を指定して、連想配列形式のオブジェクトを取得することもできます。

function ajax_function() {
  check_ajax_referer('my-ajax-nonce');
  // json_decode() の第2引数に true を指定して連想配列形式で取得
  $json = json_decode( wp_unslash($_POST['input']), true );
  // 値は連想配列でアクセス
  $name = esc_html($json['name']);
  echo 'Hello ' . $name . '!';
  wp_die();
}
add_action('wp_ajax_my_action', 'ajax_function');
add_action('wp_ajax_nopriv_my_action', 'ajax_function');

リクエスト URL にパラメータを追加

何らかの理由で、どうしてもコンテントタイプを application/json にして JSON データを送信したい場合は、リクエスト URL に action と nonce のパラメータをクエリ文字列として追加すれば、400 Bad Request や 403 (Forbidden) を回避することはできます。

但し、サーバー側では $_POST で POST されたデータを受け取れないので、file_get_contents() に php://input を指定してデータを取得する必要があります。

以下は XMLHttpRequest で送信する例です。

この例ではデータ(data)に action と _ajax_nonce も含めていますが、サーバー側ではリクエスト URL に 追加したパラメータのキーと値が使われるので省略することができます。

js/my-script.js(XMLHttpRequest)
const btn = document.getElementById("js-btn");
const nameInput = document.getElementById("name");

btn.addEventListener("click", () => {

  const data = {
    action: "my_action",
    _ajax_nonce: my_ajax_params.my_ajax_nonce,
    name: nameInput.value,
  };

  // データを JSON に変換
  const json_input = JSON.stringify(data);

  const xhr = new XMLHttpRequest();

  // リクエスト URL に action と nonce のパラメータを追加
  xhr.open("POST", my_ajax_params.ajaxurl + '?action=my_action&_ajax_nonce=' + my_ajax_params.my_ajax_nonce);

  // Content-Type に application/json を指定してリクエストを送信
  xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');

  xhr.addEventListener("readystatechange", () => {
    if (xhr.readyState === 4) {
      if (xhr.status >= 200 && xhr.status < 300) {
        console.log(xhr.responseText);
      }
    }
  });

  // JSON データをリクエストボディに指定
  xhr.send(json_input);
});

以下は jQuery を使う場合の例です。

js/my-script.js(jQuery)
jQuery(function($){
  $( '#js-btn' ).on( 'click', function(){

    const data = {
      action : 'my_action',
      _ajax_nonce: my_ajax_params.my_ajax_nonce,
      name: $( '#name' ).val(),
    };
    const json_data = JSON.stringify(data);

    $.ajax({
      type: 'POST',
      // リクエスト URL に action と nonce のパラメータを追加
      url: my_ajax_params.ajaxurl + '?action=my_action&_ajax_nonce=' + my_ajax_params.my_ajax_nonce,
      // リクエストボディに JSON 形式のデータを指定
      data: json_data,
      // コンテントタイプを application/json に指定
      contentType:'application/json; charset=utf-8',
    })
    .done( function( response ) {
      console.log( response );
    })
    .fail( function( error ) {
      console.warn( `失敗: ${error.status} (${error.statusText})` );
    });
  });
});
サーバー側(AJAX ハンドラ)

サーバー側では $_POST で POST されたデータを受け取れないので、file_get_contents() に php://input を指定してデータを取得します。

前述の例では json_decode() でデコードする際に、オブジェクトに変換しましたが、この例では第二引数に true を指定して連想配列形式に変換しています(特に理由はありません)。

functions.php
function ajax_function() {
  check_ajax_referer('my-ajax-nonce');
  // POST リクエストのデータを取得
  $json = file_get_contents('php://input');
  // JSON エンコードされた文字列を受け取り PHP の値に変換(連想配列形式)
  $data = json_decode( $json, true);
  // 値をサニタイズ
  $name = esc_html($data['name']);
  // テキストをレスポンスとして返す
  echo 'Hello ' . $name . '!';
  //処理を終了
  wp_die();
}
add_action('wp_ajax_my_action', 'ajax_function');
add_action('wp_ajax_nopriv_my_action', 'ajax_function');

関連項目:JSON データを POST で送信

JSON データの受信(レスポンス)

XMLHttpRequest で JSON としてレスポンスを取得するには responseType プロパティに "json" を設定し、response プロパティを使ってレスポンスを JSON オブジェクトとして取得します。

以下は最初の例のレスポンスを JSON データとして受信するように書き換えた例です。

response プロパティを使うと、responseType プロパティで指定した形式でレスポンスを取得できます。

js/my-script.js(XMLHttpRequest)
const btn = document.getElementById("js-btn");
const nameInput = document.getElementById("name");

btn.addEventListener("click", () => {

  // オブジェクト形式のデータ(送信する際に URLSearchParams で変換)
  const data = {
    action: "my_action",
    _ajax_nonce: my_ajax_params.my_ajax_nonce,
    name: nameInput.value,
  };

  const xhr = new XMLHttpRequest();

  // responseType プロパティに 'json' を設定
  xhr.responseType = 'json';

  xhr.open("POST", my_ajax_params.ajaxurl);

  xhr.addEventListener("readystatechange", () => {
    if (xhr.readyState === 4) {
      if (xhr.status >= 200 && xhr.status < 300) {
        // response で JSON オブジェクトを取得
        const json = xhr.response;
        // name プロパティを出力
        console.log(json.name);
      }
    }
  });
  xhr.send(new URLSearchParams(data));
});

fetch()

以下は上記を fetch() を使って書き換えた例です。

js/my-script.js(Fetch API)
const btn = document.getElementById("js-btn");
const nameInput = document.getElementById("name");

btn.addEventListener("click", () => {

  // オブジェクト形式のデータ(送信する際に URLSearchParams で変換)
  const data = {
    action: "my_action",
    _ajax_nonce: my_ajax_params.my_ajax_nonce,
    name: nameInput.value,
  };

  // 送信先に my_ajax_params.ajaxurl を指定
  fetch(my_ajax_params.ajaxurl, {
    // POST を指定
    method: 'POST',
    // データを URLSearchParams に変換してリクエストボディに指定
    body: new URLSearchParams(data)

  }).then((response) => {
    if(response.ok) {
      //  レスポンスの json() メソッドで JSON として解析
      return response.json();
    } else {
      throw new Error(`失敗: ${response.status} ${response.statusText}`);
    }
  })
  .then((json) => {
    // JSON として解析されたオブジェクトの name プロパティを出力
    console.log(json.name);
  })
  .catch((error) => {
    console.warn(error);
  });
});

jQuery

以下は上記を jQuery を使って書き換えた例です。

js/my-script.js(jQuery)
jQuery(function($){
  $( '#js-btn' ).on( 'click', function(){
    $.ajax({
      type: 'POST',
      url: my_ajax_params.ajaxurl,
      // 受信するデータの形式に json を指定
      dataType: 'json',
      data: {
        action : 'my_action',
        _ajax_nonce: my_ajax_params.my_ajax_nonce,
        name: $( '#name' ).val()
      },
    })
    .done( function( response ) { //成功した時の処理
      // dataType に json を指定しているのでレスポンスは JSON(name プロパティを出力)
      console.log( response.name ); //成功した時の処理
    })
    .fail( function( error ) {
      console.warn( `失敗: ${error.status} (${error.statusText})` );
    });
  });
});

サーバー側(AJAX ハンドラ)

以下の AJAX ハンドラでは wp_send_json() を使って値を JSON に変換して出力しています。

wp_send_json() は引数に配列やオブジェクトを受け取り、JSON レスポンス(JSON に変換した値)を AJAX リクエストに送り返す関数で、 wp_send_json() を使う場合は、終了処理が自動的に呼び出されるので、wp_die() を省略することができます。

functions.php
function ajax_function() {
  // nonce の値を検証
  check_ajax_referer('my-ajax-nonce');
  // 取得した値をサニタイズ
  $name = esc_html($_POST['name']);
  // 返す値を連想配列で作成
  $return = array(
    'name' => 'My name is ' . $name . '!'
  );
  // JSON レスポンスを出力(JSON に変換して出力)
  wp_send_json($return);
}
add_action('wp_ajax_my_action', 'ajax_function');
add_action('wp_ajax_nopriv_my_action', 'ajax_function');

以下は、上記を wp_send_json() を使わずに書き換えた例です。header() 関数でヘッダーの Content-type に application/json を指定し、json_encode() を使って値を JSON 形式に変換して echo しています。

この場合は、wp_die() を使用して終了する必要があります。

functions.php
function ajax_function() {
  // nonce の値を検証
  check_ajax_referer('my-ajax-nonce');
  // 値をサニタイズ
  $name = esc_html($_POST['name']);
  // 返す値を連想配列で設定
  $return = array(
    'name' => 'My name is ' . $name . '!'
  );
  // header() 関数でコンテントタイプに application/json を指定
  header("Content-type: application/json; charset=UTF-8");
  // JSON 形式に変換して出力
  echo json_encode($return);
  // 終了する
  wp_die();
}
add_action('wp_ajax_my_action', 'ajax_function');
add_action('wp_ajax_nopriv_my_action', 'ajax_function');

form 要素の利用

form 要素を使ってデータを送信する場合、form 要素の action 属性にリクエスト先 URL を指定し、input 要素にアクション名や nonce を指定すれば、それらの値を JavaScript から参照することができます。

リクエスト先 URL や nonce の値は PHP を使って出力することができ、それらの値はフォームのデータとしてまとめて AJAX で送信することができます。

以下は form 要素を使った例です。

テンプレートに記述
<form action="<?php echo admin_url('admin-ajax.php'); ?>" method="POST" id="ajax-form">
  <label for="name">名前: </label>
  <input name="name" id="name" value="" size="20">
  <button type="submit">送信</button>
  <!-- name 属性に action、value 属性にアクション名 -->
  <input type="hidden" name="action" value="my_action">
  <!-- name 属性に _ajax_nonce、value 属性に nonce の値 -->
  <input type="hidden" name="_ajax_nonce" value="<?php echo wp_create_nonce('my-ajax-nonce'); ?>">
</form>

action 属性の値にリクエスト先 URL を admin_url('admin-ajax.php') と PHP で指定します。

フォームを送信する際に input 要素の name 属性と value 属性の値はシリアライズして「キー名=値」のパラメータとして送信されます。

name="action" を指定した input 要素の value 属性にアクション名を指定し、name="_ajax_nonce" を指定した input 要素の value 属性に nonce の値を指定します(これらの要素は type="hidden" を指定して非表示にします)。

以下は JavaScript ファイルの登録と AJAX アクションの登録(サーバー側の処理)です。

JavaScript 側では form 要素を参照してデータを取得できるので、wp_add_inline_script() などを使って PHP 側から JavaScript に値を渡す(script タグを出力する)必要はありません。

functions.php
function my_enqueue_ajax_scripts() {
  // AJAX を記述する JavaScript ファイルの登録
  wp_enqueue_script(
    'my-ajax-script', // ハンドル名
    get_theme_file_uri('/js/my-script.js'),
    array('jquery'), // jQuery を使わない場合は空の配列 array() を指定
    filemtime(get_theme_file_path('/js/my-script.js')),
    true
  );
}
// AJAX を記述する JavaScript ファイルを登録
add_action('wp_enqueue_scripts', 'my_enqueue_ajax_scripts');


// AJAX ハンドラの定義(今までの例と同じ)
function my_ajax_handler() {
  // nonce の値を検証
  check_ajax_referer('my-ajax-nonce');
  // POST 送信された値 $_POST['name'] をサニタイズ
  $name = esc_html($_POST['name']);
  // Hello と ! を前後に追加してレスポンスとして返す
  echo 'Hello '. $name . '!';
  // 完了したら wp_die() で終了
  wp_die();
}
// AJAX アクションの登録
add_action('wp_ajax_my_action', 'my_ajax_handler');
add_action('wp_ajax_nopriv_my_action', 'my_ajax_handler');

以下が jQuery を使った AJAX の記述例です。

submit イベントを使っているので、ページが再読み込みされないように return false でデフォルトのフォーム送信動作をキャンセルします(19行目)。

js/my-script.js(jQuery)
jQuery(function($){
  $('#ajax-form').submit(function(){
    const ajaxForm = $(this);
    $.ajax({
      // リクエスト送信先はフォームの action 属性を参照
      url: ajaxForm.attr('action'),
      // フォームデータをシリアライズしてデータに指定
      data: ajaxForm.serialize(),
      // 使用する HTTP メソッドはフォームの method 属性を参照
      type: ajaxForm.attr('method'),
    }).done( function( response ) {
      // 成功時の処理
      console.log( response );
    }).fail( function( error ) {
      // 失敗時の処理
      console.warn( `失敗: ${error.status} (${error.statusText})` );
    });
    // デフォルト動作のフォーム送信をキャンセル
    return false;
  });
});

リクエスト送信先はフォームの action 属性に指定した admin_url('admin-ajax.php') を参照します。

jQuery の場合、フォームのデータをリクエストパラメータとして送信するには serialize() を使用します。serialize() は form 内のコントロールからキーと値のペアを生成(シリアライズ)します。

※ シリアライズでは name 属性を利用するので、データとして送信する form 内の要素(コントロール)には name 属性が設定されている必要があります。

form 内の name 属性が設定されている要素はシリアライズによりフォーマットされたクエリ文字列に変換されます。上記の場合、以下のパラメータが生成されるので data に指定します。

キー名(name 属性の値) 値(value 属性の値)
name 入力された値(例 Foo)
action my_action(アクション名)
_ajax_nonce wp_create_nonce() で生成された nonce の値

例えば、テキストフィールドに Foo と入力して送信すると、以下のようなパラメータが送信されます。

シリアライズされたフォームのデータ
name=Foo&action=my_action&_ajax_nonce=e09c15c091

Fetch API

以下は jQuery を使わずに Fetch API を使って上記を書き換えた例です。

フォームのデータは new FormData() でまとめてシリアライズすることができます。

js/my-script.js(Fetch API)
const ajaxForm = document.getElementById('ajax-form');
ajaxForm.addEventListener('submit', (e)=> {
  // デフォルト動作のフォーム送信をキャンセル
  e.preventDefault();
  // リクエスト送信先はフォームの action 属性を参照
  fetch( ajaxForm.getAttribute('action'), {
    // 使用する HTTP メソッドはフォームの method 属性を参照
    method: ajaxForm.getAttribute('method'),
    // form 要素から FormData オブジェクト(キーと値のペア)を生成してリクエストボティに指定
    body: new FormData(ajaxForm)
  }).then((response) => {
    if(response.ok) {
      return response.text();
    } else {
      throw new Error(`失敗: ${response.status} ${response.statusText}`);
    }
  })
  .then((text) => {
    // レスポンスのテキストを出力(成功時の処理)
    console.log(text);
  })
  .catch((error) => {
    console.warn(error);
  });
});

XMLHttpRequest

以下は XMLHttpRequest を使って書き換えた例です。

js/my-script.js(XMLHttpRequest)
const ajaxForm = document.getElementById('ajax-form');

ajaxForm.addEventListener('submit', (e)=> {
  // デフォルト動作のフォーム送信をキャンセル
  e.preventDefault();

  // XMLHttpRequest のインスタンスを生成
  const xhr = new XMLHttpRequest();

  // メソッドはフォームの method 属性を、送信先はフォームの action 属性を参照
  xhr.open(ajaxForm.getAttribute('method'), ajaxForm.getAttribute('action'));

  xhr.addEventListener('readystatechange', ()=> {
    if(xhr.readyState === 4){
      if(xhr.status >= 200 && xhr.status < 300){
        // 取得操作が完了し、成功したらレスポンスのテキストをコンソールに出力
        console.log(xhr.responseText);
      }else{
        // 失敗時の処理
        console.log(`失敗: ${xhr.status} (${xhr.statusText})`);
      }
    }
  });
  // form 要素から FormData オブジェクト(キーと値のペア)を生成してリクエストボティに指定
  xhr.send(new FormData(ajaxForm));
});

カスタムデータ属性の利用

form 要素以外でも、テンプレートに記述する HTML の属性やカスタムデータ(data-*)属性の値に PHP で値を出力することで、PHP から JavaScript に値を渡すことができます。

以下はテンプレートの HTML にカスタムデータ属性を指定して、値を PHP(WordPress の関数など)で値を出力して JavaScript に渡す例です。

<div id="ajax-div"
  data-action="<?php echo admin_url('admin-ajax.php'); ?>"
  data-nonce="<?php echo wp_create_nonce('my-ajax-nonce'); ?>"
>
  <label for="name">名前: </label>
  <input id="name" value="" size="20">
  <button id="ajax-send" type="button">送信</button>
</div>

JavaScript では HTML のカスタムデータ属性の値を参照することで PHP から渡された値にアクセスすることができます。

jQuery では data() や attr() メソッドを使って data 属性の値を取得することができます。

js/my-script.js(jQuery)
jQuery(function($){
  $('#ajax-send').click(function(){
    const ajaxDiv = $('#ajax-div')
    $.ajax({
      // #ajax-div の data-action 属性を参照
      url: ajaxDiv.data('action'),
      // または url: ajaxDiv.attr('data-action'),
      type: 'POST',
      data: {
        action : 'my_action', // アクション名
        // #ajax-div の data-nonce 属性を参照
        _ajax_nonce:  ajaxDiv.data('nonce'), // nonce
        name: $( '#name' ).val(),
      }
    }).done( function( response ) {
      // 成功時の処理
      console.log( response );
    }).fail( function( error ) {
      // 失敗時の処理
      console.warn( `失敗: ${error.status} (${error.statusText})` );
    });
  });
});

以下は jQuery の代わりに Fetch API を使った例です。JavaScript では data 属性の値を取得には dataset プロパティまたは getAttribute() メソッドを使用することができます。

js/my-script.js(Fetch API)
const ajaxDiv = document.getElementById('ajax-div');
const nameInput = document.getElementById("name");

document.getElementById('ajax-send').addEventListener('click',()=>{

  // 送信するデータ(オブジェクト形式のパラメータ)
  const data = {
    action: "my_action", // アクション名
    // #ajax-div の data-nonce 属性を参照
    _ajax_nonce: ajaxDiv.dataset.nonce,
    // または _ajax_nonce: ajaxDiv.getAttribute('data-nonce'),
    name: nameInput.value,
  };

  // #ajax-div の data-action 属性を参照
  fetch(ajaxDiv.dataset.action, {
    method: 'POST',
    // データを URLSearchParams に変換してリクエストボディに指定
    body: new URLSearchParams(data)

  }).then((response) => {
    if(response.ok) {
      return response.text();
    } else {
      throw new Error(`失敗: ${response.status} ${response.statusText}`);
    }
  })
  .then((text) => {
    // レスポンスのテキストを出力(成功時の処理)
    console.log(text);
  })
  .catch((error) => {
    console.warn(error);
  });
});
functions.php
function my_enqueue_ajax_scripts() {
  // AJAX を記述する JavaScript ファイルの登録
  wp_enqueue_script(
    'my-ajax-script', // ハンドル名
    get_theme_file_uri('/js/my-script.js'),
    array('jquery'), // jQuery を使わない場合は空の配列 array() を指定
    filemtime(get_theme_file_path('/js/my-script.js')),
    true
  );
  /* JavaScript を出力する処理は不要 */
}
// AJAX ファイルを登録
add_action('wp_enqueue_scripts', 'my_enqueue_ajax_scripts');


// AJAX ハンドラの定義(今までの例と同じ)
function my_ajax_handler() {
  // nonce の値を検証
  check_ajax_referer('my-ajax-nonce');
  // POST 送信された値 $_POST['name'] をサニタイズ
  $name = esc_html($_POST['name']);
  // Hello と ! を前後に追加してレスポンスとして返す
  echo 'Hello '. $name . '!';
  // 完了したら wp_die() で終了
  wp_die();
}
add_action('wp_ajax_my_action', 'my_ajax_handler');
add_action('wp_ajax_nopriv_my_action', 'my_ajax_handler');

投稿を追加で読み込むサンプル

一覧ページで設置したボタンをクリックすると、AJAX を使って続きの投稿を読み込んで表示する例です。

その一覧ページで表示する投稿の情報(投稿タイプや投稿数、一覧ページの種類など)はメインループのクエリ情報($wp_query->query_vars)から取得します。

例えば読み込む投稿数は「表示設定」の「1ページに表示する最大投稿数」の値が使われ、投稿の種類やカテゴリーなどはそのページと同じものが使われます。

この例では、テーマフォルダに js というフォルダを作成し、その中に AJAX を記述する load-more.js という JavaScript ファイルを配置します。以下はUnderscores.me を利用したテスト用のテーマの例です。

以下のボタンをテンプレートに記述します。上記の構成の場合、一覧ページのテンプレートは index.php と archive.php なのでそのページに記述します(テーマにより異なります)。

ボタンはページの総数($wp_query->max_num_pages)が2以上の場合にのみ表示します。

一覧ページのテンプレートにボタンを追加
<?php
global $wp_query;
if (  $wp_query->max_num_pages > 1 ){
  echo '<button type="button" id="load-more">More Posts</button>';
}
?>

以下を functions.php に記述し、JavaScript ファイルの登録と JavaScript に必要な情報を渡します。

JavaScript に渡すデータとしてはリクエスト先 URL と nonce の他に、グローバル変数 $wp_query(メインクエリ)の query_vars プロパティや get_query_var() でクエリに関する情報を取得して渡します。

$wp_quer の query_vars プロパティには、そのページの URL から解析されたキーと値のペアが入っています(表示されているページの種類やパーマリンク設定により取得される値は異なります)。

functions.php
function my_load_more_scripts_setup() {
  // $wp_quer を関数内で使用するためのグローバル宣言
  global $wp_query;

  // AJAX を記述する JavaScript ファイルの登録
  wp_enqueue_script(
    'load-more', // ハンドル名
    get_theme_file_uri('/js/load-more.js'),
    array('jquery'), // jQuery を使わない場合は空の配列 array() を指定,
    filemtime(get_theme_file_path('/js/load-more.js')),
    true
  );
  // JavaScript に渡すデータを script タグ内に出力
  wp_add_inline_script(
    // データを渡す JavaScript のハンドル名(上記で登録した JavaScript)
    'load-more',
    // script タグに出力する JavaScript のコード
    'const load_more = ' . json_encode(array(
      // リクエスト URL
      'ajaxurl' => admin_url('admin-ajax.php'),
      // nonce
      'my_ajax_nonce' => wp_create_nonce('load-more-nonce'),
      // ページ(クエリ)の情報
      'posts' => json_encode( $wp_query->query_vars ),
      // アーカイブページ分割時のページ番号
      'current_page' => get_query_var( 'paged' ) ? get_query_var('paged') : 1,
      // ページの合計数
      'max_page' => $wp_query->max_num_pages
    )),
    // インラインの script タグを対象のスクリプトの前に出力
    'before'
  );
}
add_action('wp_enqueue_scripts', 'my_load_more_scripts_setup');

以下は jQuery を使った AJAX の記述です。

送信するデータにはアクション名と nonce の他に、上記の wp_add_inline_script() で出力された AJAX ハンドラの処理で必要なクエリの情報やページ番号(AJAX ハンドラでのサブループで取得する投稿の条件)を含めています。

まず、jQuery の beforeSend オプション(リクエストを発行する前に呼び出される関数)を使って、ボタンのテキストを「Loading...」に変更しています。

そしてリクエストが完了しデータを取得したらボタンのテキストを戻してデータを挿入し、current_page の値を1増加させます。この例では prev().before(data) でボタンの前の兄弟要素の前にデータを挿入していますが、挿入する位置はテンプレートの構造により異なります。

js/load-more.js(jQuery)
jQuery(function($){
  $('#load-more').click(function(){

    const button = $(this);

    const data = {
      // アクション名
      'action': 'load_more',
      // wp_add_inline_script() で出力された値を参照
      '_ajax_nonce': load_more.my_ajax_nonce,
      'query': load_more.posts,
      'page' : load_more.current_page
    };

    $.ajax({
      // リクエスト先
      url : load_more.ajaxurl,
      data,
      type : 'POST',
      beforeSend : function () {
        // 読み込みが完了するまでボタンのテキストを変更
        button.text('Loading...');
      },
    }).done( function( data ) {
      if( data ) {
        // データが取得できたらボタンのテキストを戻してデータを挿入
        button.text( 'More Posts' ).prev().before(data);
        // current_page の値を1増加
        load_more.current_page++;

        if ( load_more.current_page == load_more.max_page )
          // 最後のページであればボタンを削除
          button.remove();

      } else {
        // データがなければボタンを削除
        button.remove();
      }
    }).fail( function( error ) {  // 失敗時の処理
      console.warn( `失敗: ${error.status} (${error.statusText})` );
    });
  });
});

以下は jQuery を使わずに XMLHttpRequest で書き換えたコードです。

XMLHttpRequest や Fetch API には jQuery の beforeSend に該当するオプションはないので、別途関数を作成し、XMLHttpRequest のインスタンスを生成する前に呼び出します。

js/load-more.js(XMLHttpRequest)
const loadMoreBtn = document.getElementById('load-more');

if(loadMoreBtn) {

  // リクエストを送信する前に実行する処理(jQuery の beforeSend に相当)
  const beforeSend = () =>{
    loadMoreBtn.textContent = 'Loading...';
  }

  // ボタンのイベントリスナ
  loadMoreBtn.addEventListener('click', () => {

    const data = {
      'action': 'load_more',
      '_ajax_nonce': load_more.my_ajax_nonce,
      'query': load_more.posts,
      'page' : load_more.current_page
    };

    // ボタンのテキストを変更
    beforeSend();

    // XMLHttpRequest のインスタンスを生成
    const xhr = new XMLHttpRequest();

    // POST メソッドでインライン script に出力された値(リクエスト先 URL)に送信
    xhr.open('POST', load_more.ajaxurl);

    xhr.addEventListener('readystatechange', ()=> {
      if(xhr.readyState === 4) {
        if(xhr.status >= 200 && xhr.status < 300){
          const data = xhr.responseText;
          if( data ) {
            // ボタンのテキストを戻す
            loadMoreBtn.textContent = 'More Posts';
            // 取得したデータを挿入
            loadMoreBtn.previousElementSibling.insertAdjacentHTML('beforebegin', data);
            // current_page の値を1増加
            load_more.current_page++;
            if ( load_more.current_page == load_more.max_page )
              // 最後のページであればボタンを削除
              loadMoreBtn.remove();
          } else {
            // データがなければボタンを削除
            loadMoreBtn.remove();
          }
        }else{
          // 失敗時の処理
          console.log(`失敗: ${xhr.status} (${xhr.statusText})`);
        }
      }
    });
    // オブジェクト形式のパラメータを URLSearchParams に変換してリクエストボディに指定
    xhr.send(new URLSearchParams(data));
  });
}

以下の AJAX ハンドラを functions.php に記述します。

最初に nonce の値を検証します。

$_POST['query'] には読み込まれたページのクエリ情報($wp_query->query_vars)が入っているので、サブループを生成する条件の配列 $args に代入しています。

$args の paged に次のページを読み込むため $_POST['page'] を1増加させた値を指定します。

また、post_status は publish を指定し、公開状態の投稿のみを対象とする必要があります。

作成した $args でサブループの WP_Query オブジェクトを生成してループ出力します。

以下では、ループでリンクを付けたタイトルと抜粋を表示していますが、そのページのループ部分(コンテンツ)のテンプレートパーツがあれば get_template_part() で呼び出すこともできます。

functions.php
function load_more_handler(){

  // nonce の値を検証
  check_ajax_referer('load-more-nonce');

  // 受信した$_POST['query']から不要なスラッシュを取り除きPHPの配列に変換
  $args = json_decode( wp_unslash( $_POST['query'] ), true );
  // 次のページを読み込むために受信した$_POST['page']に1増加した値を設定
  $args['paged'] = $_POST['page'] + 1;
  // 投稿の状態(post_status)は公開されているもの(publish)を指定
  $args['post_status'] = 'publish';

  // 更新した上記クエリ情報を使って WP_Query オブジェクトを生成
  $my_query = new WP_Query( $args );

  if( $my_query->have_posts() ) :
    // ループ
    while( $my_query->have_posts() ): $my_query->the_post();
      // タイトルにリンクを付けて出力
      the_title('<h2><a href="'.esc_url( get_permalink() ).'">', '</a></h2>');
      // 抜粋を出力
      the_excerpt();
      // またはループ部分のテンプレートを呼び出すこともできます
      // get_template_part( 'template-parts/content', get_post_type() );
    endwhile;
  endif;
  wp_reset_postdata();
  wp_die();
}
// AJAX アクションへの登録
add_action('wp_ajax_load_more', 'load_more_handler');
add_action('wp_ajax_nopriv_load_more', 'load_more_handler');

Load More Posts with AJAX を参考にさせていただきました。

参考元のコードでは、WP_Query の代わりに query_posts() を使用し、ループ部分では get_template_part() を使ってテンプレートを呼び出しています。

query_posts() を使用する理由としては、query_posts() はメイン ループをより適切にエミュレートするため、get_template_part() に含まれるすべてのコードが正常に動作する可能性が高くなるとのことです。

また、通常、query_posts() の使用は推奨されませんが、AJAX 処理関数内で使用する場合、query_posts() はページ上の何にも影響を与えないとのことです(未確認)。

限られた条件でしか試していませんが、WP_Query を使って get_template_part() でテンプレートを呼び出しても正常に動作しました。

上記の例の場合、ループが記述されているテンプレートは template-parts/content.php になります。

カスタムループ(サブループ)

前述の例は一覧ページのメインループのクエリ情報を取得して投稿を追加で読み込みますが、以下は一覧表示用のテンプレートを別途作成してサブループを記述して、AJAX で投稿を追加で読み込む例です。

この例では固定ページの slug を ajax として page-ajax.php というテンプレートファイルを作成し、投稿の条件を指定してサブループで投稿の一覧を表示します。

例えば、ローカル環境で http://localhost/wp に WordPress がインストールされている場合、この固定ページには http://localhost/wp/ajax/ でアクセスします。固定ページに記述した内容を表示する必要がある場合は、メインループも記述します(この例では一覧を表示するサブループのみ)。

以下は固定ページのテンプレートの例です。

$args に表示する投稿の条件を設定して WP_Query でサブループを作成して投稿のタイトルと抜粋を表示します。ページ番号を表す paged は1にします。

posts_per_page は数値を指定していますが、「表示設定」の「1ページに表示する最大投稿数」の値を使用する場合は、 get_option( 'posts_per_page' ) を指定します。

生成したクエリオブジェクト($my_posts)の max_num_pages プロパティから合計のページ数を取得し、カスタムデータ(data-* )属性に指定して JavaScript で値を参照できるようにしています。

また、ボタンは合計のページ数が2以上の場合にのみ表示するようにしています。

page-ajax.php
<?php
get_header();
?>

<?php
// 取得する投稿の条件を指定(必要に応じて変更)
$args = array(
  'post_type' => 'post',
  'post_status' => 'publish',
  'posts_per_page' => 3, //「表示設定」の値は get_option('posts_per_page')で取得可能
  'paged' => 1,
);
// WP_Query オブジェクトの生成
$my_posts = new WP_Query($args);
// 合計のページ数を取得
$max_num_pages = $my_posts->max_num_pages;
?>

<main>
  <!-- data-max-num-pages 属性を設定して合計のページ数を指定 -->
  <div class="entry-content" data-max-num-pages="<?php echo esc_attr($max_num_pages); ?>">
    <!-- サブループ -->
    <?php if ($my_posts->have_posts()) : ?>
      <div class="my-posts">
        <?php while ($my_posts->have_posts()) : $my_posts->the_post(); ?>
          <?php the_title('<h2><a href="'.esc_url( get_permalink() ).'">', '</a></h2>'); ?>
          <?php the_excerpt(); ?>
        <?php endwhile; ?>
        <?php wp_reset_postdata(); ?>
      </div>
      <!-- 合計のページ数が2以上の場合にボタンを表示 -->
      <?php if ($max_num_pages > 1) : ?>
        <button class="loadmore" type="button">Load More...</button>
      <?php endif; ?>
    <?php endif; ?>
  </div><!-- .entry-content -->
</main><!-- #main -->

<?php
get_sidebar();
get_footer();

JavaScript ファイルの登録と script タグへの出力

以下を functions.php に記述し、JavaScript ファイルの登録と JavaScript に必要な情報を渡します。

functions.php
function my_enqueue_load_more_ajax_scripts() {

  if(is_page('ajax')) {
    // AJAX を記述する JavaScript ファイルの登録
    wp_enqueue_script(
      'my-load-more-custom', // ハンドル名
      get_theme_file_uri('/js/load-more-custom.js'),
      array('jquery'), // jQuery を使わない場合は空の配列 array() を指定
      filemtime(get_theme_file_path('/js/load-more-custom.js')),
      true
    );

    // JavaScript に渡すデータ(リクエスト URL と nonce)を script タグに出力
    wp_add_inline_script(
      // データを渡す対象の JavaScript のハンドル名(上記で登録したハンドル名を指定)
      'my-load-more-custom',
      // 出力する値(変数 load_more_custom に格納)
      'const load_more_custom = ' . json_encode(array(
        'ajaxurl' => admin_url('admin-ajax.php'),
        'ajax_nonce' => wp_create_nonce('load-more-custom'),
      )),
      // script タグを対象のスクリプトの前に出力するための指定
      'before'
    );
  }
}
// wp_enqueue_scripts アクションに上記関数をフックして AJAX ファイルを登録及び変数を出力
add_action('wp_enqueue_scripts', 'my_enqueue_load_more_ajax_scripts');

AJAX の記述

以下は jQuery を使った AJAX の記述です。

リクエストのデータには、必須のアクション名の他に nonce の値とページ番号(paged)を含めています。

ページ番号の初期値は2にし、ボタンをクリックする度に値を1増加させます。

この例では nonce のキー名をデフォルト以外の security_nonce としているので、AJAX ハンドラでは検証の際に check_ajax_referer() の第2引数に 'security_nonce' を指定します。

js/load-more-custom.js(jQuery)
// ページ番号の初期値
let paged = 2;

jQuery(function ($) {
  // カスタムデータ属性 data-max-num-pages が設定されている要素
  const entry_content = $('.entry-content');
  // data-max-num-pages の値(合計のページ数)を取得
  const max_num_pages = parseInt(entry_content.data('maxNumPages'));

  $('.loadmore').on('click', function () {
    const button = $(this);

    const data = {
      // アクション名
      action: 'load_more_custom',
      // nonce(キー名はデフォルト以外なので check_ajax_referer の第2引数に指定して検証)
      security_nonce: load_more_custom.ajax_nonce,
      // ページ番号
      paged: paged,
    };

    $.ajax({
      // リクエスト先
      url: load_more_custom.ajaxurl,
      data,
      type: 'POST',
      beforeSend: function () {
        // 読み込みが完了するまでボタンのテキストを変更
        button.text('Loading...');
      },
    })
    .done(function (response) {
      if (response) {
        // ボタンのテキストを戻す
        button.text('Load More...');
        // レスポンスを HTML に追加
        $('.my-posts').append(response);
        // ページ番号がページ数と同じ場合はボタンを削除
        if (paged === max_num_pages) {
          button.remove();
        }
        paged++;
      } else {
        // データがなければボタンを削除
        button.remove();
      }
    })
    .fail(function (error) {
      console.warn(`失敗: ${error.status} (${error.statusText})`);
    });
  });
});

以下は jQuery を使わずに Fetch API で書き換えたコードです。

jQuery の beforeSend オプションと同様のことをするために、別途実行する処理を記述した関数 beforeSend() を作成し、fetch() でリクエストを送信する前に呼び出しています。これにより、この処理が AJAX のリクエストの前に実行されます。

js/load-more-custom.js(Fetch API)
let paged = 2;
const button = document.querySelector('.loadmore');
const my_posts = document.querySelector('.my-posts');
// data-max-num-pages の値(合計のページ数)を取得
const max_num_pages = parseInt(
  document.querySelector('.entry-content').dataset.maxNumPages
);

// リクエストを送信する前に実行する処理(jQuery の beforeSend に相当)
const beforeSend = () =>{
  button.textContent = 'Loading...';
}

button.addEventListener('click', ()=> {

  // 送信するデータ
  const data = {
    // アクション名
    action: 'load_more_custom',
    // nonce(キー名はデフォルト以外の値を指定)
    security_nonce: load_more_custom.ajax_nonce,
    // ページ番号
    paged: paged,
  };

  // ボタンのテキストを変更
  beforeSend();

  // 送信先に load_more_custom.ajaxurl を指定
  fetch(load_more_custom.ajaxurl, {
    // POST を指定
    method: 'POST',
    // データを URLSearchParams に変換してリクエストボディに指定
    body: new URLSearchParams(data)

  }).then((response) => {
    if(response.ok) {
      return response.text();
    } else {
      throw new Error(`失敗: ${response.status} ${response.statusText}`);
    }
  })
  .then((response) => {
    if (response) {
      // ボタンのテキストを戻す
      button.textContent = 'Load More...';
      // レスポンスを HTML に追加
      my_posts.insertAdjacentHTML('beforeend', response);
      // ページ番号がページ数と同じ場合はボタンを削除
      if (paged === max_num_pages) {
        button.remove();
      }
      paged++;
    } else {
      // データがなければボタンを削除
      button.remove();
    }
  })
  .catch((error) => {
    console.warn(error);
  });
});

サーバー側の処理

以下の AJAX ハンドラを定義し、AJAX アクションに登録します。

最初に nonce の値を検証します。この例の場合、AJAX のリクエストの nonce のキー名をデフォルト以外の security_nonce としているので、check_ajax_referer() の 第2引数にその値を指定しています。

取得する投稿の条件はテンプレートに記述した条件と基本的に同じですが、ページ番号(paged)はリクエストで送信された値を指定します。

post_status には publish を指定して公開状態の投稿のみを表示するようにします。テンプレートファイルでのループ生成のパラメータの場合は、post_status のデフォルトは publish なので省略できますが、ここでは publish を指定しないと下書きなども対象となってしまうので publish を指定します。

テンプレートのサブループの生成で posts_per_page に「表示設定」の「1ページに表示する最大投稿数」の値をした場合は get_option('posts_per_page') を指定して同じ値にします。

コンテンツはテンプレートに合わせます。この例の場合は投稿のタイトルと抜粋を表示します。

また、以下では $_POST['paged'] の代わりに PHP の filter_input() を使ってリクエストを取得し、取得した値が整数であることを検証しています。

functions.php
function load_more_custom_callback() {
  // nonce の値を検証
  check_ajax_referer('load-more-custom', 'security_nonce');

  // 取得する投稿の条件を指定(テンプレートの条件と同じ)
  $args = array(
    'post_type' => 'post',
    'post_status' => 'publish', //公開状態の投稿のみを表示
    'posts_per_page' => 3, //または get_option('posts_per_page') テンプレートに合わせる
    'paged' => filter_input( INPUT_POST, 'paged', FILTER_VALIDATE_INT ),
  );

  // クエリオブジェクトを生成してループ
  $my_posts = new WP_Query( $args );
  ?>
  <?php if ( $my_posts->have_posts() ) : ?>
    <?php while ( $my_posts->have_posts() ) : $my_posts->the_post(); ?>
    <?php the_title('<h2><a href="'.esc_url(get_permalink()).'">','</a></h2>');?>
      <?php the_excerpt(); ?>
    <?php endwhile; ?>
    <?php wp_reset_postdata(); ?>
  <?php endif; ?>
  <?php
  wp_die();
}
// AJAX アクションに登録
add_action('wp_ajax_load_more_custom', 'load_more_custom_callback');
add_action('wp_ajax_nopriv_load_more_custom', 'load_more_custom_callback');

上記は以下のように記述することもできます。

function load_more_custom_callback() {

  check_ajax_referer('load-more-custom', 'security_nonce');

  $args = array(
    'post_type' => 'post',
    'post_status' => 'publish',
    'posts_per_page' => 3, // get_option('posts_per_page')
    'paged' => filter_input( INPUT_POST, 'paged', FILTER_VALIDATE_INT ),
  );

  $my_posts = new WP_Query( $args );

  if ( $my_posts->have_posts() ) {
    while ( $my_posts->have_posts() ) {
      $my_posts->the_post();
      the_title('<h2><a href="'.esc_url(get_permalink()).'">','</a></h2>');
      the_excerpt();
    }
    wp_reset_postdata();
    wp_die();
  }
}
add_action('wp_ajax_load_more_custom', 'load_more_custom_callback');
add_action('wp_ajax_nopriv_load_more_custom', 'load_more_custom_callback');

また、必要であれば、ob_start() を使って以下のように記述することもできます。

function load_more_custom_callback() {

  check_ajax_referer('load-more-custom', 'security_nonce');

  $args = array(
    'post_type' => 'post',
    'post_status' => 'publish',
    'posts_per_page' => 3, // get_option('posts_per_page')
    'paged' => filter_input( INPUT_POST, 'paged', FILTER_VALIDATE_INT ),
  );

  $my_posts = new WP_Query( $args );

  if ( $my_posts->have_posts() ) {
    // バッファリング開始(出力をせずバッファに保存)
    ob_start();
    while ( $my_posts->have_posts() ) {
      $my_posts->the_post();
      the_title('<h2><a href="'.esc_url(get_permalink()).'">','</a></h2>');
      the_excerpt();
    }
    wp_reset_postdata();
    // バッファの内容を変数に代入
    $output = ob_get_contents();
    // バッファの内容をクリアしてバッファリングを終了
    ob_end_clean();
    // 変数に代入したバッファの内容を出力(レスポンスを返す)
    echo $output;
    wp_die();
  }
}
add_action('wp_ajax_load_more_custom', 'load_more_custom_callback');
add_action('wp_ajax_nopriv_load_more_custom', 'load_more_custom_callback');

関連ページ:

ページネーション

投稿の一覧の下にページネーションを表示して、ページネーションのリンクをクリックするとそのページ番号の投稿を AJAX で取得して表示する例です。

但し、この例のページネーションは WordPress の paginate_links() などを使って表示する動的なページネーションではなく、以下のように単純にページ数のリンクを表示する静的なページネーションです。

paginate_links() を使ったページネーションの場合、ページネーションも AJAX ハンドラーで更新する必要があります。関連ページ:ChatGPT を使って WordPress の AJAX ページネーション

ページ番号は各リンク要素の data-page 属性に指定しています。

<ul>
  <?php
  for ($i = 1; $i <= $wp_query->max_num_pages; $i++) {
    echo '<li><a href="#" class="pagination" data-page="' . $i . '">'
      . $i . '</a></li>';
  }
  ?>
</ul>

以下は任意のテンプレートにサブループを使って投稿の一覧とページネーションを表示する例です。

テンプレート
<?php
  $args = array(
    'post_type' => 'post',
    'paged' => 1
  );
  $query = new WP_Query($args);
  // ページ総数を取得
  $max_pages = $query->max_num_pages;
  ?>
  <div class="post-container">
    <?php
    if ($query->have_posts()) :
      while ($query->have_posts()) : $query->the_post();
        the_title('<h2><a href="' . esc_url(get_permalink()) . '">', '</a></h2>');
        the_excerpt();
      endwhile;
    endif;
    wp_reset_postdata();
    ?>
  </div>
  <!-- ページ数が1より大きければページネーションを表示 -->
  <?php if ($max_pages > 1) : ?>
    <ul>
      <?php
      for ($i = 1; $i <= $max_pages; $i++) {
        echo '<li ><a href="#" class="pagination" data-page="' . $i . '">'
          . $i . '</a></li>';
      }
      ?>
    </ul>
  <?php endif; ?>
  
functions.php
// AJAX ハンドラ
function load_paged_posts() {
  // nonce の値を検証を追加
  check_ajax_referer('my-ajax-nonce');
  // ページ番号を取得
  $paged = filter_input(INPUT_POST, 'page', FILTER_VALIDATE_INT);
  // 取得する投稿の条件(必要に応じて変更)
  $args = array(
    'post_type' => 'post',
    'post_status' => 'publish',
    'paged' => $paged
  );

  $query = new WP_Query($args);

  // 出力する HTML 文字列(レスポンス)の初期化
  $posts_html = '';

  // 出力をバッファ
  ob_start();

  if ($query->have_posts()) {
    while ($query->have_posts()) {
      $query->the_post();
      the_title('<h2><a href="' . esc_url(get_permalink()) . '">', '</a></h2>');
      the_excerpt();
    }
  }
  wp_reset_postdata();
  // 上記ループの出力を変数に代入
  $posts_html .= ob_get_clean();

  // レスポンスのデータ(ページ総数と出力の HTML)
  $return = array(
    'content' => $posts_html,
    'page' => $paged,
  );

  // レスポンスのデータを JSON に変換して出力
  wp_send_json($return);
}
add_action('wp_ajax_load_paged_posts', 'load_paged_posts');
add_action('wp_ajax_nopriv_load_paged_posts', 'load_paged_posts');

// AJAX の JavaScript ファイルを登録
function load_paged_scripts() {
  wp_enqueue_script(
    'ajax-pagination',
    get_template_directory_uri() . '/js/ajax-pagination.js',
    array(),
    filemtime(get_theme_file_path('/js/ajax-pagination.js')),
    true
  );
  wp_add_inline_script(
    'ajax-pagination',
    'const ajaxPagination = ' . json_encode(array(
      'ajaxurl' => admin_url('admin-ajax.php'),
      'ajax_nonce' => wp_create_nonce('my-ajax-nonce'),
    )),
    'before'
  );
}
add_action('wp_enqueue_scripts', 'load_paged_scripts');
  

ページネーションのリンクがクリックされると data-page 属性からページ番号を取得して、AJAX でそのページ番号を送信して投稿一覧を取得します。

js/ajax-pagination.js
(function () {
  // ページ番号の初期値
  let page = 1;
  // ロード中かどうか
  let loading = false;
  // 投稿一覧のコンテナ要素
  const postContainer = document.querySelector('.post-container');
  // ページネーションの全てのリンク
  const paginations = document.querySelectorAll('.pagination');

  // ボタンとコンテナがあれば
  if (postContainer && paginations.length > 0) {

    // リンクがクリックされるとそのページ番号の投稿を取得する関数
    function loadPagedPosts() {
      if (loading) {
        return;
      }
      // ロード中
      loading = true;
      // ロード中(loading...)と表示
      postContainer.insertAdjacentHTML('beforeend', 'loading...');

      // データを定義
      const data = {
        // アクション名
        action: 'load_paged_posts',
        // nonce(出力された値を参照)
        _ajax_nonce: ajaxPagination.ajax_nonce,
        // ページ番号
        page: page,
      };

      const xhr = new XMLHttpRequest();
      // responseType プロパティに 'json' を設定
      xhr.responseType = 'json';
      xhr.open('POST', ajaxPagination.ajaxurl, true);
      xhr.onreadystatechange = function () {
        if (xhr.readyState === 4 && xhr.status === 200) {
          if (xhr.response) {
            // JSON として解析された受信データ(JSON オブジェクト)
            const data = xhr.response;
            // 投稿の一覧を取得したデータで置き換え
            postContainer.innerHTML = data.content;
            // ロード完了
            loading = false;
          } else {
            postContainer.textContent = 'Error !';
          }
        }
      };
      // データを URLSearchParams に変換してリクエストボディに指定
      xhr.send(new URLSearchParams(data));
    }

    // ページネーションのリンクの click イベント
    paginations.forEach((elem) => {
      elem.addEventListener('click', (e) => {
        // デフォルトの動作をキャンセル(リンク先に移動しない)
        e.preventDefault();
        // active クラスが指定されていれば何もしない
        if(elem.classList.contains('active')) {
          return;
        }
        paginations.forEach((el) => {
          // 一度全ての active クラスを削除
          el.classList.remove('active');
        });
        // 要素の data-page 属性からページ番号を取得して変数 page を更新
        page = parseInt(elem.dataset.page);
        // クリックされた要素に active クラスを追加
        elem.classList.add('active');
        // 投稿を取得
        loadPagedPosts();
      });
    });
  }
})();

以下は一覧ページのテンプレート(index.php や archive.php など)でメインループを使う場合の例です。

前述の例とほぼ同じですが、メインループのパラメータは $wp_query->query_vars から取得しています。

異なる部分にはコメントを入れてあります。

一覧ページのテンプレート
<div class="post-container">
  <?php
  // メインループ
  if (have_posts()) :
    while (have_posts()) : the_post();
      the_title('<h2><a href="' . esc_url(get_permalink()) . '">', '</a></h2>');
      the_excerpt();
    endwhile;
  endif;
  wp_reset_postdata();
  ?>
</div>
<?php
  global $wp_query;
  if ($wp_query->max_num_pages > 1) :
?>
  <ul>
    <?php
    // $wp_query からページ総数を取得
    for ($i = 1; $i <= $wp_query->max_num_pages; $i++) {
      echo '<li><a href="#" class="pagination" data-page="' . $i . '">'
        . $i . '</a></li>';
    }
    ?>
  </ul>
<?php endif; ?>
functions.php
function load_paged_posts() {
  check_ajax_referer('my-ajax-nonce');
  // 受信した$_POST['query']から不要なスラッシュを取り除きPHPの配列に変換
  $args = json_decode( wp_unslash( $_POST['query'] ), true );
  $paged = filter_input(INPUT_POST, 'page', FILTER_VALIDATE_INT);
  // ページ番号(paged)は上記で取得した値を指定
  $args['paged'] = $paged;
  // 投稿の状態(post_status)は公開されているもの(publish)を指定
  $args['post_status'] = 'publish';

  $query = new WP_Query($args);
  $posts_html = '';
  ob_start();
  if ($query->have_posts()) {
    while ($query->have_posts()) {
      $query->the_post();
      the_title('<h2><a href="' . esc_url(get_permalink()) . '">', '</a></h2>');
      the_excerpt();
    }
  }
  wp_reset_postdata();
  $posts_html .= ob_get_clean();
  $return = array(
    'content' => $posts_html,
    'page' => $paged,
  );
  wp_send_json($return);
}
add_action('wp_ajax_load_paged_posts', 'load_paged_posts');
add_action('wp_ajax_nopriv_load_paged_posts', 'load_paged_posts');

function load_paged_scripts() {
  // グローバル変数 $wp_query を使用するための宣言
  global $wp_query;

  wp_enqueue_script(
    'ajax-pagination',
    get_template_directory_uri() . '/js/ajax-pagination.js',
    array(),
    filemtime(get_theme_file_path('/js/ajax-pagination.js')),
    true
  );

  wp_add_inline_script(
    'ajax-pagination',
    'const ajaxPagination = ' . json_encode(array(
      'ajaxurl' => admin_url('admin-ajax.php'),
      'ajax_nonce' => wp_create_nonce('my-ajax-nonce'),
      // ページ(クエリ)の情報
      'query' => json_encode( $wp_query->query_vars ),
    )),
    'before'
  );
}
add_action('wp_enqueue_scripts', 'load_paged_scripts');
js/ajax-pagination.js
(function () {
  let page = 1;
  let loading = false;
  const postContainer = document.querySelector('.post-container');
  const paginations = document.querySelectorAll('.pagination');

  if (postContainer && paginations.length > 0) {

    function loadPagedPosts() {
      if (loading) {
        return;
      }
      loading = true;
      postContainer.insertAdjacentHTML('beforeend', 'loading...');

      const data = {
        action: 'load_paged_posts',
        _ajax_nonce: ajaxPagination.ajax_nonce,
        page: page,
        // クエリの情報
        query: ajaxPagination.posts,
      };

      const xhr = new XMLHttpRequest();
      xhr.responseType = 'json';
      xhr.open('POST', ajaxPagination.ajaxurl, true);
      xhr.onreadystatechange = function () {
        if (xhr.readyState === 4 && xhr.status === 200) {
          if (xhr.response) {
            const data = xhr.response;
            postContainer.innerHTML = data.content;
            loading = false;

          } else {
            postContainer.textContent = 'Error !';
          }
        }
      };

      xhr.send(new URLSearchParams(data));
    }

    paginations.forEach((elem) => {
      elem.addEventListener('click', (e) => {
        e.preventDefault();
        if(elem.classList.contains('active')) {
          return;
        }
        paginations.forEach((el) => {
          el.classList.remove('active');
        });
        page = parseInt(elem.dataset.page);
        elem.classList.add('active');
        loadPagedPosts();
      });
    });
  }
})();
ページ取得時に一覧の先頭にスクロール

ページを取得すると、既存の一覧が置き換わるので、自動的に一覧の先頭にスクロールする例です。

変更があるのは、JavaScript のみです。

この例では AJAX で投稿を取得できたら、requestAnimationFrame() を使って投稿一覧の先頭にスムーススクロールします。追加した部分にはコメントを入れてあります。

前述の JavaScript に 7〜38行目と 63〜70行目のスムーススクロールの処理を追加しています。

js/ajax-pagination.js
(function () {
  let page = 1;
  let loading = false;
  const postContainer = document.querySelector(".post-container");
  const paginations = document.querySelectorAll(".pagination");

  //アニメーション(スムーススクロール)の持続時間
  const duration = 300;
  //開始時刻を代入する変数(最初は未定義)
  let start;
  //スクロール先の Y 座標(ウィンドウの左上からの座標)
  let targetY;
  //現在の垂直(Y)方向のスクロール量(位置)
  let currentY;
  //イージング関数の定義
  const easeInQuad = (x) => {
    return x * x;
  };
  //スクロール先を調整する値(上部の固定メニューの高さなど必要に応じて)
  let offset = 80;
  //requestAnimationFrame() のコールバック関数
  const smoothScroll = (timestamp) => {
    if (start === undefined) {
      start = timestamp;
    }
    //経過時間
    const elapsed = start ? timestamp - start : 0;
    //進捗度を算出してイージングを適用
    const relativeProgress = easeInQuad(Math.min(1, elapsed / duration));
    //移動する量(targetY)に進捗度を適用して scrollTo のY座標へ指定する値を算出
    const scrollY = currentY + targetY * relativeProgress;
    //上記で算出した位置へスクロール(上部の固定メニューの高さ分を offset で調整)
    window.scrollTo(0, scrollY - offset);
    //進捗度が1未満の場合は自身を繰り返す
    if (relativeProgress < 1) {
      requestAnimationFrame(smoothScroll);
    }
  };

  if (postContainer && paginations.length > 0) {
    function loadPagedPosts() {
      if (loading) {
        return;
      }
      loading = true;
      postContainer.insertAdjacentHTML("beforeend", "loading...");
      const data = {
        action: "load_paged_posts",
        _ajax_nonce: ajaxPagination.ajax_nonce,
        page: page,
        query: ajaxPagination.posts,
      };

      const xhr = new XMLHttpRequest();
      xhr.responseType = "json";
      xhr.open("POST", ajaxPagination.ajaxurl, true);
      xhr.onreadystatechange = function () {
        if (xhr.readyState === 4 && xhr.status === 200) {
          if (xhr.response) {
            const data = xhr.response;
            postContainer.innerHTML = data.content;
            loading = false;
            //開始時刻を初期化
            start = undefined;
            //対象(スクロール先)の要素の Y 座標(ウィンドウ座標)
            targetY = postContainer.getBoundingClientRect().y;
            //現在の垂直方向にスクロールされている量
            currentY = window.scrollY;
            //関数を実行
            smoothScroll();
          } else {
            postContainer.textContent = "Error !";
          }
        }
      };
      xhr.send(new URLSearchParams(data));
    }

    paginations.forEach((elem) => {
      elem.addEventListener("click", (e) => {
        e.preventDefault();
        if (elem.classList.contains("active")) {
          return;
        }
        paginations.forEach((el) => {
          el.classList.remove("active");
        });
        page = elem.dataset.page;
        elem.classList.add("active");
        loadPagedPosts();
      });
    });
  }
})();

関連ページ:ChatGPT を使って WordPress の AJAX ページネーション