WordpressでAjaxにて投稿記事を取得する際にfetchを使ってダウンロードの進行状況（progress）を取得する方法

# はじめに
WordpressにてAjaxで投稿記事を取得する際にダウンロード状況（Progress）も取得する場合のサンプルは大体はjQueryかXMLHttpRequestが多いのですが、fetchでも[ReadableStream](https://developer.mozilla.org/ja/docs/Web/API/ReadableStream)を使えば取得できるということで試してみました。

# Wordpressの設定

WordpressでのAjax送受信の設定をしていきます。
この辺りの設定はよくご存知の方は飛ばして、[送信側の設定](#送信側の設定)をお読みください。

## 1.Ajaxの受け取り側の設定

WPがajaxリクエストを受け取るためには、以下のようにwp_ajax_[任意のアクション名]、wp_ajax_nopriv_[任意のアクション名]アクションフックにて設定します。

```PHP:functions.php
function topics_ajax() {
    
    //[ここにJSONを作成する処理を記述する]

    echo $json;

    wp_die();
}

add_action( 'wp_ajax_topics_ajax', 'topics_ajax' );
add_action( 'wp_ajax_nopriv_topics_ajax', 'topics_ajax' );

```

今回はアクション名をtopics_ajaxにて設定しています。

JSONを作成する処理を記述する例として、以下のように投稿の記事を３件ずつ取得してタイトルとアイキャッチ画像と投稿日時、パーマリンクを返すなどを記載していきます。

```php:functions.php
function topics_ajax() {

    //@出力Jsonのための配列
    $json_list_ary = [];

    $paged = stripslashes(esc_attr($_POST['post_paged']));
    $pageSlug = $_POST['page'];
    check_ajax_referer("topics_ajax" . $pageSlug, '_security');
    $posts_per_page = 3;
    $args = array(
        'post_type' => 'post',
        'oderby' => 'date',
        'order' => 'DESC',
        'posts_per_page' => $posts_per_page
    );

    if($_POST['post_paged']) {
        $paged = stripslashes(esc_attr($_POST['post_paged']));
        $args['paged'] = $paged;
    }

    //ポストの総件数
    $count_posts = wp_count_posts();
    $json_list_ary['posts_count'] = (int) $count_posts->publish;
    $json_list_ary['post_paged'] = (int) $paged;
    $count = 0;
    $qry = new WP_Query($args);
    if($qry->have_posts()): while($qry->have_posts()): $qry->the_post();
        $json_list_ary['body'][$count]['post_title'] = get_the_title();
        $json_list_ary['body'][$count]['time'] = get_the_time('Y.m.d');
        $json_list_ary['body'][$count]['link'] = get_the_permalink();
        $json_list_ary['body'][$count]['terms'] = get_the_terms($post,'category');
        if(has_post_thumbnail()) {
            $thumb_id = get_post_thumbnail_id();
            $alt = get_post_meta( $thumb_id, '_wp_attachment_image_alt', true);
            $json_list_ary['body'][$count]['image_src'] = get_the_post_thumbnail_url();
            $json_list_ary['body'][$count]['image_alt'] = $alt;
        }

        $count++;
    endwhile; wp_reset_postdata(); endif;

    $json = json_encode($json_list_ary);
    http_response_code(200);
    header('Cache-Control: no-cache');
    header('Content-Type: application/json; charaset=UTF-8');
    header('Content-Length: '. strlen($json));

    echo $json;

    wp_die();
}

add_action( 'wp_ajax_topics_ajax', 'topics_ajax' );
add_action( 'wp_ajax_nopriv_topics_ajax', 'topics_ajax' );

```

## 2.使用するJavaScriptファイルの読み込み設定

まずはAjax通信にて使用するJavaScriptファイルなどの読み込み設定をfunctions.phpに記述していきます。

```PHP:functions.php
function load_script($hook) {
    if(!is_admin()) {
        //frontでは$hookを$postのスラッグ名にしておく
        $hook = $post->post_name;

        $doc_root = TEMPLATEPATH;
        $themes_uri = get_template_directory_uri();

		//@ WPでファイルを読み込む時の儀礼
		$creds = request_filesystem_credentials('', '', false, false, null);
		if(WP_Filesystem($creds)) {
			global $wp_filesystem;
            //front pageのみに読ませるように設定
			if(is_front_page()) {
                //Ajaxs処理を記述するJSファイルのルートを設定
				$home_inline_js_path = $doc_root . $js_root . '/home.js';
               
                // nonceを生成します
                // アクション名にページのslug名を入れています
				$ajax_nonce = wp_create_nonce("topics_ajax" . $hook);

				if(file_exists($home_inline_js_path)) {

                     //WordpressでAjaxをする場合のお決まりの設定をします
					wp_localize_script( 'mythemescript', 'ajax_obj',
					array(
						'ajax_url' => admin_url( 'admin-ajax.php'),
						'nonce' => $ajax_nonce,
						'page' => $hook
					));

                    //これから作るJSファイルを$wp_filesystemを使って読み込みます
					$home_inline_js = $wp_filesystem->get_contents($home_inline_js_path);
					wp_add_inline_script('mythemescript',$home_inline_js);
				}

			}
		}
    }
}
add_action( 'wp_enqueue_scripts', 'load_script',10,1);
```
:::note warn
Ajaxでの読み込み用のJavaScriptファイルはwp_enqueue_scriptで読み込めば良いのですが、今回はHTMLソースに直接インライン記述できるようにJavascriptファイルを読み込んでwp_add_inline_scriptにて出力しています。
:::

# 送信側の設定

[2.使用するJavaScriptファイルの読み込み設定](#2使用するjavascriptファイルの読み込み設定)の`wp_localize_script()`の記述でフロントには以下のように出力されます。

```HTML
<script id='mythemescript-js-extra'>
var ajax_obj = {"ajax_url":"https:\/\/hogehoge\/ctrl\/wp-admin\/admin-ajax.php","nonce":"5e70f18d15","page":"home"};
</script>
```
各値はajax_urlはアクセスポイントのURL、nonceはnonceの値、pageはページslug名となっていてこれらの値にてfatchの設定をします。

こちらを元にfetchにて通信する場合の基本的な設定は以下のような感じになります。

```JavaScript:home.js
//アクセスポイントの取得
const url = ajax_obj.ajax_url;

/**
* URLSearchParamsインスタンスを作成し、
* 送信するクエリを登録する
*/
const parame = new URLSearchParams();

//wp_ajax_[任意のアクション名]にて設定したAction名を指定する
parame.append('action', 'topics_ajax');
//ページ番号を設定
parame.append('post_paged', post_paged);
//nonceを設定
parame.append('_security', ajax_obj.nonce);
//ページスラッグ名を設定
parame.append('page', ajax_obj.page);

fetch(url, {
    method: 'POST',
    body:parame
})
.then((response) => {
    ...
});

```

これでAjaxにてクライアントから送信してデータを取得するまでの基本的な設定ができました。
これを元にthen（〜）の設定をしていきます。

[ReadableStream](https://developer.mozilla.org/ja/docs/Web/API/ReadableStream)を参考に記述していきます。
fetchをPromiseオブジェクトで包んで返り値（JSONからObjectに変換したもの）を処理が終わったら渡せるようにします。

### 処理の手順

(1) まずはfetchにて受信したデータの総容量が必要になります。
こちらはheadersのContent-Lengthにデータの総容量をセットしていますので、```get()```メソッドで取得します。
```javascript
//...
}).then((response) => {
//parseIntで整数に整形
const total = parseInt(response.headers.get('content-length'), 10);
//ダウンロードしたデータ量の値を格納する変数を初期化設定  
let loaded = 0;
//...
```
(2)  `ReadableStream`コンストラクターを使って読み取り可能なStreamオブジェクトを作成し、```start()```メソッドでストリームのソースへのアクセスの設定していきます。
[参考（mdn web docs ReadableStream()）](https://developer.mozilla.org/ja/docs/Web/API/ReadableStream/ReadableStream)
```javascript
return new ReadableStream({

    start(controller) {
       //...startメソッドで処理を設定していく
    }
});
```
(3)```ReadableStream.getReader()```メソッドを使い、ストリームから次々くる個々のチャンクを読み取りができるリーダー[ ReadableStreamDefaultReader](https://developer.mozilla.org/ja/docs/Web/API/ReadableStreamDefaultReader)を作成します。
```javascript
start(controller) {
    //getReader()メソッドでリーダーを取得してロックします
    const reader = response.body.getReader();
//...(4)の処理へ
}
```
(4)```read()```メソッドを使いストリームの最初のチャンクへのアクセスを提供するPromiseを取得します。async/awaitを指定して処理が終わるまで次の処理を待たせます。
[参考(ReadableStreamDefaultReader.read())](https://developer.mozilla.org/ja/docs/Web/API/ReadableStreamDefaultReader/read)
```javascript
async function read() {
    let result = await reader.read();
    //...(5)の処理へ
}
```
(5) 次にストリームチャンクが完了するまでwhileを使って次々にデータを取得していく処理をします。
取得したチャンクデータは```controller.enqueue```を使ってストリームのキューに入れます。
[参考(mdn web docs ReadableStreamDefaultController.enqueue())](https://developer.mozilla.org/ja/docs/Web/API/ReadableStreamDefaultController/enqueue)
```javascript
/**
 *  ストリームチャンクが完了するまでループ処理
 */
while (!result.done) {

  const value = result.value;//chunkデータを取得
  loaded += value.byteLength;//取得したデータ量を取得してloaded変数に追加
  // chunkデータを取得し、コントローラー経由でブラウザーに送信してストリームのキューに入れます
  controller.enqueue(value);
  //データの確認
  console.log(`${loaded} / ${total} = ${loaded / total * 100}%`);

  //ストリーム内部の次のchunkを取得するためのPromiseを取得する
  result = await reader.read();

}
```
(6) ストリームによる読み込み処理がおわったらクローズします。
クローズするとReadableStreamがPromiseを返しReadableStreamオブジェクトインスタンスを渡します。
[参考(mdn web docs - ReadableStreamDefaultController.close()))](https://developer.mozilla.org/ja/docs/Web/API/ReadableStreamDefaultController/close)
```javascript
    controller.close();
```
(7) ReadableStreamからのデータをResponseコンストラクターでResponseオブジェクトを作成し、Response.json()メソッドでJSON形式からObjectに変換します。
[参考(mdn web docs - Response)](https://developer.mozilla.org/ja/docs/Web/API/Response)

```javascript
//...
}).then((stream) => {
  return new Response(stream, {
    "Content-type": "application/json; charset=UTF-8",
    }).json();
})
```
(8) 今までの処理で得たデータを外のメソッドに渡したいので、fetchをPromiseオブジェクトで囲い処理したデータを渡すようにします。
```javascript
const responsePromise = new Promise((resolve) => {
    //...今までの処理
    })
    .then((result) => {
      /**
       * 結果をPromise（responsePromise）のresolveに渡す処理
       */
      resolve(result);
    })
    .catch(e => console.error(e));
});

```

というながれて記述していきます。
参考までに上記をまとめたものを記載しておきます。

```JavaScript:home.js
/**
   * @param {Number} post_paged ページ番号　初期値は1
   * AjaxにてPostデータを取得するメソッド
   */
  const get_newPost = async (post_paged = 1) => {

    const url = ajax_obj.ajax_url;
    const parame = new URLSearchParams();

    parame.append('action', 'topics_ajax');
    parame.append('post_paged', post_paged);
    parame.append('_security', ajax_obj.nonce);
    parame.append('page', ajax_obj.page);

    //progressバーのNodeを取得
    const progressBar = document.querySelector('progress');
    
    const responsePromise = new Promise((resolve) => {
    
      fetch(url, {
            method: 'POST',
            body:parame
          })
        .then((response) => {
          //responsのheaderからgetメソッドにてトータルのコンテンツのサイズを取得
          const total = parseInt(response.headers.get('content-length'), 10);
          //ダウンロードした値を格納する変数を初期化設定  
          let loaded = 0;

          /**
           * ReadableStreamコンストラクターを使って読み取り可能なStreamオブジェクトを作成して返します。
           */
          return new ReadableStream({

            //startメソッドで処理を定義していきます
            start(controller) {

              //getReader()メソッドを使用することでリーダーを取得してロックします
              const reader = response.body.getReader();
              async function read() {
                /**
                 * read()メソッドでストリームの内部キュー内の次のチャンクへのアクセスを提供するpromiseを取得
                 */
                let result = await reader.read();

                /**
                 *  chunkストリームが完了するまでループ処理
                 */
                while (!result.done) {

                  const value = result.value;//chunkデータを取得
                  loaded += value.byteLength;//取得したデータ量を取得してloaded変数に追加
                  // chunkデータを取得し、コントローラー経由でブラウザーに送信しましストリームのキューに入れます
                  controller.enqueue(value);
                  /**
                   * progress barのアニメーション
                   */
                  console.log(`${loaded} / ${total} = ${loaded / total * 100}%`);
                  //総データ量とダウンロードしたデータ量をProgressアニメーションメソッドに値を渡す
                  progressAnime({
                    target: progressBar,
                    total: total,
                    loaded: loaded
                  });

                  //ストリーム内部の次のchunkを取得する
                  result = await reader.read();

                }
                /**
                 * ストリームによる読み込み処理がおわったらクローズします
                 * （クローズするとReadableStreamがPromiseを返しReadableStreamオブジェクトインスタンスを渡します）
                 */
                controller.close();
                return;
              };
              read();
            }
          });
        })
        .then((stream) => {
          /**
           * ReadableStreamからのデータをResponseコンストラクターでResponseオブジェクトを作成し、
           * Response.json()メソッドでJSON形式からObjectに変換する
           */
          return new Response(stream, {
            "Content-type": "application/json; charset=UTF-8",
            }).json();
        })
        .then((result) => {
          /**
           * 結果をPromise（responsePromise）のresolveに渡す処理
           */
          resolve(result);
        })
        .catch(e => console.error(e));
      
    });

    //受信したオブジェクトデータを格納
    const result = await responsePromise;
    //受信したオブジェクトデータからHTMLに形成して出力するメソッドに渡す
    await show_topics(result);
    //ページの総数を入れておく
    posts_count = result.posts_count;

  };

   //例）クリックイベントにて取得させる
   addEventListener('click',(e) => {
        get_newPost(1);
    });
```

:::note warn
 HTML表示をさせるshow_topicsメソッドとプログレスバーアニメーション用メソッドのprogressAnimeは別途作成する必要があります。
:::

# プログレスバーに関して
とこれでfetchにてダウンロードの進行状況を取得することができるのですが、記事データのJSONでは一瞬でダウンロードされるので進行状況をprogressに反映してもパッと終わってしまいます。

WordpressからJSONを出力する際に画像をbase64データに変換してJSONファイルに埋め込むとデータ容量が増えるのでprogressのアニメーションが確認できますが、画像自体のキャッシュがなくなるので実際には使わないかと思います。
なので自分はよく一瞬でダウンロードされても少しだけアニメーションするように設定したりしています。
参考までに以下に記述しておきます。

```javascript

 /**
   * 
   * @param {Object} settings 
   * @returns animate api
   * fadein/out関数
   */
  const fadeInOut = (args) => {
    const defaultSettings = {
      duration: 600,
      opacityMax: 1,
      opacityMin: 0,
    }
    const settings = {
      ...defaultSettings,
      ...args
    }
    const
    target = settings.target,
    type = settings.type,
    durationNum = settings.duration;
    let
      startNum, endNum;
    
    switch(type) {
      case "fadeOut":
        startNum = settings.opacityMax;
        endNum = settings.opacityMin;
        break;
      case "fadeIn":
        startNum = settings.opacityMin;
        endNum = settings.opacityMax;
        break;
    }

    const fadeObj = target.animate(
      {
        opacity: [startNum, endNum]
      },
      {
        fill: "forwards",
        duration: durationNum
      }
    );

    return fadeObj;

  };

const progressAnime = (args) => {

    if (!args.target) {
      return;
    }
    const
      total = args.total,
      loaded = args.loaded,
      target = args.target;
    let
      percentComplete = 0,
      loadedCalNum = 0,
      timerID;
    
    if (timerID) {
        cancelAnimationFrame(timerID);
    }
    /**
     * ローディングバーのアニメーションループ実行
     */
    const loadedCal = function () {
      loadedCalNum += (loaded - loadedCalNum) * 0.4;
      percentComplete = Math.round((loadedCalNum / total) * 100);
      //console.log(percentComplete + '%');
      target.value = percentComplete;
      if (percentComplete < 100) {
          timerID = requestAnimationFrame(loadedCal);
      } else {
          setTimeout(function () { 
              cancelAnimationFrame(timerID);
              if (args. callback) {
                  callback();
              }
            const targFadeOut = fadeInOut({
              target: target,
              type: "fadeOut"
            });
            targFadeOut.finished.then(animation => {
              animation.effect.target.remove();
            })

          },200);
      }
    }
    loadedCal();
    
  }
```