jQueryを使わないPureなJavaScriptによるWordPressとのAjax通信

ファイル構造 (テーマフォルダ直下)

- functions.php
- js/
  - core.js
- page-example.php

固定ページ(スラッグはexample)のテンプレート「page-example.php」では次のようにボタンを設置している

<?php
$paged = max( 1, get_query_var('paged') );
$args = array(
	'post_type' => 'post',
	'paged' => $paged
);
$post_query = new WP_Query( $args )
if ( $post_query->have_posts() ) {
	while ($post_query->have_posts() ) {
	$post_query->the_post();
	printf(
		'<a href="%s">%s</a>',
		esc_url( get_the_permalink() ),
		esc_html( get_the_title() ),
	);
	}
}
?>
<a href="#!" id="loadPostsButton">記事を読み込む</a>
<?php
wp_reset_postdata();

ではこのページにJavaScriptファイル「js/core.js」を登録しよう。functions.phpに次のコードを追記。

<?php
...
add_action( 'wp_enqueue_scripts', 'core_js_file' );
function core_js_file() {
  wp_enqueue_script( 'core_js', get_theme_file_uri( '/js/core.js'), array(), rand( 1000 ,9999 ) , true );
}
...

バージョン番号をランダムすることでキャッシュ回避をしている荒業。それはさておいて、これでcore.jsが読み込まれるようになった。ではそのcore.jsにAjaxするための準備をコーディング。

let paged = 1;

document.getElementById('loadPostsButton').addEventListener( 'click', ( event ) => {
  event.preventDefault();
  paged++;

  // Ajax処理…

} );

ボタンをクリックすると`paged`がインクリメントしてAjax処理される。pagedの初期値が1なのは、今表示している投稿が1ページ目だから。クリックとともにpagedが2になり、2ページ目を要求するメッセージを送れる寸法。

もし、access-control(CORS関連)で弾かれる場合はwatchなどでプロキシサーバーを使っていないかチェック!localhostを別のサーバーでプロキシして表示している場合(その逆も)は一度WordPressのURLをチェックしよう。

WordPressとAjaxさせるための記事を探すと、ほぼすべての記事でWordPress同梱のjQueryを利用した$.ajax()の例ばかり紹介されていた。たしかにこれが一番確実であるのだが、jQueryを使わずともAjaxできて良いのではないかと思い、今回この記録を執筆した次第。

Ajax処理の部分には`XMLHttpRequest()`を使う方法と`fetch()`を使う方法がある。まずは`XMLHttpRequest()`を使う方法から。

const data = new URLSearchParams();
data.append( 'action', 'load_more_posts' ); // WordPressにある関数名(呼び出し先)
data.append( 'nonce', ajax_obj.nonce ); // 呼び出し先に送りたいデータ1
data.append( 'paged', paged ); // 呼び出し先に送りたいデータ2
data.append( 'blah', 'blah blah ...' ); // 呼び出し先に送りたいデータ3 ...

const xhr = new XMLHttpRequest();
xhr.open( 'POST', `${ ajax_obj.url }?${ data.toString() }`, true );
xhr.onreadystatechange = () => {
  if ( xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200 ) {
    console.log( xhr.response ); // WordPressからの返信メッセージ(文字列)
    // または console.log( xhr.responseText );
  }
}
xhr.send();

この中にはまだ定義していない変数がひとつある。`ajax_obj`だ。これはあるJSONデータを格納したグローバル変数なのだが、この説明はのちほど…。ここでは`URLSearchParams()`をご紹介。このAPIの.append()を使うことで例えば次のような文字列を得ることができる。

const data = new URLSearchParams();
data.append( 's', 'potato' );
data.append( 'p', 'tomato' );
data.toString(); // → "s=potato&p=tomato"

ご覧のようにHTTPのGETメソッド、URLのクエリ文字列をかんたんに生成編集できるAPIである。なのでやっていることはクエリ文字列をつくっているだけである。

通信はPOSTメソッドで。WordPress(サーバー)側がHTTPレスポンスコード200(通信成功)を返してくれれば`xhr`オブジェクトに返信結果が入っている。ただ、まだWordPress側で返信スクリプトを書いていないのでもちろん何も返って来ず、エラーになるがWeb InspectorのNetworkタブなどできちんとデータ送信されていることを確認できると思われる。

このXMLHttpRequest()を使う方法だとWordPress側で`$_GET`か`$_REQUEST`を使いJavaScriptから送られたデータを読める。例えば$_GET['blah']で文字列"blah blah ..."を扱える。

さて、次はfetch()を使った方法。

const data = new URLSearchParams();
data.append( 'action', 'load_more_posts' );
data.append( 'paged', paged );
data.append( 'nonce', ajax_obj.nonce );

const options = {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
  },
  body: data.toString(),
}

const getPosts = async () => {
  const response = await fetch( ajax_obj.url, options ); // 戻り値はPromiseオブジェクト
  const jsonData = await response.json(); // PromiseオブジェクトをJSONへ変換
  console.log( jsonData ); // WordPressからの返信メッセージ(JSON)
};

getPosts();

fetch()はPromiseを返すので次のように.then()を使っても書ける。

const data = new URLSearchParams();
data.append( 'action', 'load_more_posts' );
data.append( 'paged', paged );
data.append( 'nonce', ajax_obj.nonce );

const options = {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
  },
  body: data.toString(),
}

fetch( ajax_obj.url, options )
  .then( ( response ) => { // 返ってきたresponseはPromiseオブジェクト
    if ( !response.ok ) {
      throw new Error( `Ajax response: ${ response }` );
    }
    return response.json(); // PromiseオブジェクトをJSONに変換し、次の.then()へパス
  } )
  .then( ( jsonData ) => { // 成功!
    console.log( jsonData ); // WordPressからの返信メッセージ(JSON)
  } )
  .catch( ( error ) => { // 失敗!
    console.error( error );
  } )
  .finally( () => {
    // 成功、失敗、関係なく、最後に実行させたい処理 (オプショナル)
  } );

ちなみに、なぜJSONでなくURLSearchParams()を使ったクエリ文字列で送るのかと言われれば、WordPressがJSONを嫌がるからだ。ヘッダーのContent-Typeを"application/json"にしてbodyに`JSON.stringify({'s': 'potato'})`をセットすると400 Bad Requestエラーが出る。色々試した結果、クエリ文字列で送れば大丈夫だと判った次第。

で、わたしは2番目のfetch()とawait/asyncを使う方法で書いた。するとJSファイルの全体像は次のように書ける。

let paged = 1;

const getPosts = async ( event ) => {
  event.preventDefault();
  paged++;

  const path = ajax_obj.url;
  const nonce = ajax_obj.nonce;
  
  const data = new URLSearchParams();
  data.append( 'action', 'load_more_posts' );
  data.append( 'paged', paged );
  data.append( 'nonce', nonce );

  const options = {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: data.toString(),
  }

  try {
    const response = await fetch( path, options );
    const data = await response.json();
    console.log( data );
  }
  catch ( error ) {
    console.error( error );
  }
};

document.getElementById('loadPostsButton').addEventListener( 'click', getPosts );
さて、`ajax_obj`という変数が何なのかご紹介。この変数名ももちろん任意である。functions.phpに次を追記。
add_action( 'wp_enqueue_scripts', 'core_js_localize_script' );
function core_js_localize_script ( $hook ) {
  $obj = array(
    'url' => admin_url( 'admin-ajax.php' ),
		'nonce' => wp_create_nonce( 'my_nonce' ), // nonce生成時の名前(要メモ!)
	);
	wp_localize_script( 'core_js', 'ajax_obj', $obj );
}

wp_enqueue_scriptsにてjs/core.jsを読み込んだときの名前"core_js"をwp_localize_scriptの第一引数に、ついでJavaScriptで参照したい変数名、ここでは"ajax_obj"を(グローバル変数になるのでユニークな名前に!)、最後にajax_objに代入したいJSONデータをPHPの配列形式で渡す。こうすると'core_js'が読み込まれているページに指定したデータを持つJSONが代入されたグローバル変数'ajax_obj'が自動生成される。

JavaScriptではこのデータを参照していたのだ。ここで'url'にはWordPressがAjax処理するためのPHPファイルのフルパスが、'nonce'にはセッションごとにユニークな文字列が入る(この文字列を使ってセッションを判別する仕組み)。

さて本題。WordPress側でAjaxリクエストを処理するコードをfunctions.phpに追記しよう!

add_action( 'wp_ajax_load_more_posts', 'load_more_posts' ); // ログインユーザー向け
add_action( 'wp_ajax_nopriv_load_more_posts', 'load_more_posts' ); // 一般ユーザー向け

function load_more_posts() {
  check_ajax_referer( 'my_nonce', 'nonce' );
  wp_send_json( array(
    'message1' => 'Hi!',
    'message2' => 'Hello',
    'message3' => 'blah blah ...',
  ) );
}

2つのアクションを使う。ひとつは管理画面にログイン中のユーザー向け、もうひとつはログインしていない、つまりは一般ユーザー向けのAjaxを受け付けるアクションだ。`wp_ajax_関数名`と`wp_ajax_nopriv_関数名`アクションの2つ。そしてアクションコールバックでは`check_ajax_referer`を使ってnonceをチェックし、意図したセッションからのリクエストか否かをチェックしている。nonceが食い違えばfalseとなる。12時間以内に生成されたnonceなら1が、24時間以内に生成されたものなら2を返す関数だ。`check_ajax_referer`の第一引数にはnonce生成時の名前(wp_create_nonce()の第一引数)、第2引数にはJavaScriptが送ったnonceのキー($_REQUEST['キー'])を渡す。キー名を"_ajax_nonce"にすれば第2引数を省くことができる。

例:
const data = new URLSearchParams();
data.append( 'action', 'load_more_posts' );
data.append( '_ajax_nonce', ajax_obj.nonce ); // ←ここ

`wp_send_json()`に連想配列を渡せばそれをJSONとしてAjaxリクエスト先へ返してくれる。wp_send_json()を使う代わりに文字列を`echo`しても良い。echoならば1つの文字列だけを返すシンプルなものにできる。ただその際はきちんと`wp_die()`などで処理を終わらせるようにすること。wp_send_json()は内部でwp_die()かdie()を呼んでいるので書く必要は無い(書くに越したことはないが…)。

例:
function load_more_posts() {
  check_ajax_referer( 'my_nonce', 'nonce' );
  echo 'Hello JavaScript';
  wp_die();
}

echoの場合、返ってくるメッセージはJSONでなくただの文字列なのでfetch()の場合はresponse.text()で受けよう。

例:
const response = await fetch( ajax_obj.url, options );
const textData = await response.text(); // ←ここ
console.log( textData ); // 文字列メッセージ

これで通信完了。「記事を読み込む」ボタンをクリックすればコンソール上にWordPressからの応答(JSONデータ)が表示されるはず。いかがだろう。うまく行かないかたはコメント欄で議論しよう。

7613266116447443481 https://www.storange.jp/2023/05/blog-post.html https://www.storange.jp/2023/05/blog-post.html jQueryを使わないPureなJavaScriptによるWordPressとのAjax通信 2023-05-30T14:01:00+09:00 https://www.storange.jp/2023/05/blog-post.html Hideyuki Tabata 200 200 72 72