サーバーAPIの呼び出し結果をカスタムイベントで投げて拾ってみる

# APIの結果をイベントで掴もう

カスタムイベント、ヨシ！
ESモジュール、ヨシ！
エイジャックス、ヨシ！ [^0]


## 想定読者

- AjaxはjQueryの$.ajax()とかで呼び出すものだと思ってる人 [^1]
- Promiseで返ってくる結果に`.then()`とか`.catch()`とかぶら下げて処理しようとして疲弊してる人 [^2]
- ReactをやろうとしてReduxとか出て来て訳が分からない状態になってる人 [^3]

[^0]: どうして[洗剤の名前](https://www.google.co.jp/search?q=%E6%B4%97%E5%89%A4+ajax)なんですか
[^1]: ダメとは言わないけど、そのjQuery本当にまだ必要ですか？
[^2]: ayncなイベントハンドラでawaitで繋げてみたりするけど結局大変だよね。
[^3]: Reduxは便利なReactを気軽に使いたい気持ちにさせないためにあるからね。仕方ない。

## 何がおいしいの

- API呼び出しと結果処理が分離できる
    - 「API呼び出し部分はとりあえずスタブにしてボタンとクリックした後の処理を先に書く」とかがやりやすくなる
    - カスタムイベントの仕様が決まってしまえば「イベント使う側（画面寄り）」と「API呼び出してイベント投げる側（サーバー寄り）」で担当者も分担できる
- カスタムイベントなのでどんなデータを持ってるのか型情報を書いておくこともできる [^4]
- イベントなのであちこちでキャッチして使うこともできる
    - ログに吐いたり
    - DOMに出したり
    - 別のイベントのトリガーにして連鎖させたり

[^4]: サンプルは生JSなので型書いてないけど

## 結論

`Promise.then()`の中で受信データを処理するな！誰かにパスしろパス！
そのために`CustomEvent`を使え！
ついでにESモジュールも使え！！ [^5]

[^5]: MS Edgeでもちゃんと使えるぞ！中身がChromeになっちゃう前に試してみよう！

## サンプルコードと解説

以下に全文掲載。
ササッとAPIを用意するために`php`を使ってるのでそのまま試したい人は`Apache`か`IIS`あたりにFastCGIモジュール入れて動かしてください。
そうじゃない人はお好きな言語でサーバーサイドプログラムを書くのもいいし、単に`data.json`とかでもいい。[^6]

[^6]: FirefoxもChromeもhttpかhttpsじゃないとスクリプトを走らせてくれないので何らかのサーバーはあった方がいい

### 画面（index.html）[^7]

[^7]: 本当はindex.phpだけどphp要素はちっともない

```html:index.html
<!DOCTYPE html>
<html>
  <script src="script/main.js" type="module" async></script>
  <body>
    <main id="main">
      <form>
        <button type="button" id="command-button1">CALL[Get-Items]</button>
        <button type="button" id="command-button2">CALL[Get-Weather]</button>
        <hr/>
        <pre id="output"></pre>
      </form>
    </main>
  </body>
</html>
```

特に捻りはない。
スクリプトをESモジュールとして呼び出しているくらい。
↓こんな画面が出る。シンプル。

![sc1.png]

### script/main.js

```js:script/main.js
import {api} from './api.mjs';

document.addEventListener('click', (event) => {
  const target = event.target;
  if (target.id === 'command-button1') api.callGetItems();
  if (target.id === 'command-button2') api.callGetWeather();
});

document.addEventListener('x-respond', (event) => {
  const {eventId, data} = event.detail;
  console.log(`SUCCESS ${eventId} data=${JSON.stringify(data, null, 2)}`);
  document.getElementById('output').textContent = JSON.stringify(data, null, 2);
});

document.addEventListener('x-reject', event => {
  const {eventId, reason} = event.detail;
  console.log(`FAILURE ${eventId} reason=${JSON.stringify(reason, null, 2)}`);
  document.getElementById('output').textContent = JSON.stringify(reason, null, 2);
});
```

API呼び出しをする部分は別モジュールに分割している。
ボタンのクリックイベントはdocumentまでバブリングしてから拾うと楽ができる。 [^8]
クリックイベントではAPIを呼び出すだけ。呼び出しっぱなし。

下二つのイベントリスナでカスタムイベントを見ている。
この例では`x-respond`イベントがデータ受信に成功した時に発生するイベントで、`x-reject`はAPI呼び出しをミスったかサーバー側でしくじった時に発生するイベントになっている。
実用する場合はもっと長ったらしいイベント名を定義してもいい。

[^8]: そもそもESモジュールだとHTMLの属性で`onclick="click_handle()"`とか書いてもモジュール内なので呼べない。

### script/api.mjs

```js:script/api.mjs
export const api = {
  callGetItems() {
    call('get-items', '/api/get-items.php');
  },
  callGetWeather() {
    call('get-weather', '/api/get-weather.php');
  }
};

const call = (eventId, url) => {
  fetch(url)
  .then(response => {
    if (response.ok) return response.json();
    throw 'Wow! Something happen!';
  })
  .then(json =>
    raiseRespondEvent(eventId, json))
  .catch(reason =>
    raiseRejectEvent(eventId, reason));
};

const raiseRespondEvent = (eventId, data) => {
  const event = new CustomEvent('x-respond', {
    detail: {eventId, data}
  });
  document.dispatchEvent(event);
};

const raiseRejectEvent = (eventId, reason) => {
  const event = new CustomEvent('x-reject', {
    detail: {eventId, reason}
  });
  document.dispatchEvent(event);
};
```

API呼び出しをするモジュール。
このサンプルでは「レスポンスがOKならJSONデータを`x-respond`イベントに流す」という雑なやり方をしている。
実用するならデータ型が同じAPI毎にカスタムイベントを作るとかするとよいと思う。

エラー情報はもっと雑に固定メッセージ投げてるが、実際は役に立つ情報を流そう。
「何か起きたようです」はダメだろう常識的に考えて。

### api/get-weather.php

```php:api/get-weather.php
<?php

$geta = rand(0, 100);
$weather = ['weather' =>
 ($geta > 65 ? '晴れ' : ($geta > 30 ? '曇り' : '雨'))
];

header('Cache-Control: no-cache, no-store, must-revalidate');
header('Content-Type: application/json; charset=utf-8');
echo json_encode($weather);
```

サーバーAPIその1
辞書によると下駄は英語でも"geta"らしい。
キャッシュして欲しくないのでレスポンスヘッダで指定。
呼び出し側でキャッシュ回避の小細工を尽くすのは止めよう。

### api/get-items.php

```php:api/get-items.php
<?php

$items = [
  ['id' => '1', 'name' => 'Yamada'],
  ['id' => '2', 'name' => 'Tanaka'],
  ['id' => '3', 'name' => 'Nakano'],
  ['id' => '4', 'name' => 'Koishi'],
  ['id' => '5', 'name' => 'Uehara'],
];

header('Cache-Control: no-cache, no-store, must-revalidate');
header('Content-Type: application/json; charset=utf-8');
echo json_encode($items);
```

サーバーAPIその2
名前が意味深に思うかもしれないが、6文字でテキトーに揃えただけなので特に意味はない。


[sc1.png]:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83175/9331e0b5-8b63-5605-eab1-3c9e847d10a1.png